From eb5742473dbb21bc0c18da7e73dcea50b0c769cd Mon Sep 17 00:00:00 2001 From: Saad Ahmed <48211799+saadahmsiddiqui@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:26:36 +0200 Subject: [PATCH] =?UTF-8?q?feat(evm):=20Expanding=20SDK=20to=20support=20N?= =?UTF-8?q?ative=20transfer=20with=20optional=20con=E2=80=A6=20(#545)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Implementation details With [PR#266](https://github.com/sygmaprotocol/sygma-solidity/pull/266), we introduced the ability to define contract calls together with native deposit. As SDK didn't integrate the initial implementation of native handlers, it needs to be expanded so it can be used to create: Deposit of native currency (tokens) Deposit of native currency (tokens) + contract call definition The Native flow differs slightly from regular ERC20/721/1155, as the deposit transaction should not land on Bridge.sol but on NativeAdapter.sol contract. We have already expanded the testnet [shared configuration to have this as a new property of the domain](https://github.com/sygmaprotocol/sygma-shared-configuration/blob/main/testnet/shared-config-test.json#L10) - nativeTokenAdapter. ## Closes: #521 # Testing details Unit Tests: Develop unit tests that cover scenarios where both Native transfers and contract calls are involved. Error Handling: Test how the SDK handles invalid or failed contract calls within the transaction flow. # Acceptance Criteria - [ ] The SDK supports interactions with the new NativeAdapter->NativeHandler, allowing for both Native transfers and optional contract calls within the same transaction. - [ ] The SDK maintains backward compatibility and continues to function as expected with existing handlers. - [ ] All new functionality is covered by tests, ensuring reliable and consistent behavior. --- packages/core/src/config/localConfig.ts | 2 + packages/core/src/types.ts | 1 + packages/evm/package.json | 2 +- packages/evm/src/evmAssetTransfer.ts | 13 +- packages/evm/src/fungibleAssetTransfer.ts | 64 +++++++- packages/evm/src/genericMessageTransfer.ts | 11 +- packages/evm/src/nonFungibleAssetTransfer.ts | 4 +- packages/evm/src/types.ts | 9 ++ .../__test__/assetTransferHelpers.test.ts | 8 +- .../evm/src/utils/__test__/depositFns.test.ts | 142 +----------------- .../evm/src/utils/assetTransferHelpers.ts | 83 +++++----- packages/evm/src/utils/depositFn.ts | 51 ++----- .../src/utils/nativeTokenDepositHelpers.ts | 125 +++++++++++++++ yarn.lock | 18 ++- 14 files changed, 292 insertions(+), 241 deletions(-) create mode 100644 packages/evm/src/utils/nativeTokenDepositHelpers.ts diff --git a/packages/core/src/config/localConfig.ts b/packages/core/src/config/localConfig.ts index e75f07a19..e233e6d94 100644 --- a/packages/core/src/config/localConfig.ts +++ b/packages/core/src/config/localConfig.ts @@ -8,6 +8,7 @@ export const localConfig: SygmaConfig = { caipId: '', chainId: 1337, name: 'Ethereum 1', + nativeTokenAdapter: '', type: Network.EVM, bridge: '0x6CdE2Cd82a4F8B74693Ff5e194c19CA08c2d1c68', handlers: [ @@ -76,6 +77,7 @@ export const localConfig: SygmaConfig = { caipId: '', chainId: 1338, name: 'evm2', + nativeTokenAdapter: '', type: Network.EVM, bridge: '0x6CdE2Cd82a4F8B74693Ff5e194c19CA08c2d1c68', handlers: [ diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 0f05deb04..dfd1d4c7c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -132,6 +132,7 @@ export type FeeHandler = { export interface EthereumConfig extends BaseConfig { handlers: Array; + nativeTokenAdapter: string; feeRouter: string; feeHandlers: Array; } diff --git a/packages/evm/package.json b/packages/evm/package.json index 3172dd749..7f484f1b8 100644 --- a/packages/evm/package.json +++ b/packages/evm/package.json @@ -57,7 +57,7 @@ }, "dependencies": { "@buildwithsygma/core": "workspace:^", - "@buildwithsygma/sygma-contracts": "^2.8.0", + "@buildwithsygma/sygma-contracts": "^2.10.1", "@ethersproject/abi": "^5.7.0", "@ethersproject/bytes": "^5.7.0", "@ethersproject/contracts": "^5.7.0", diff --git a/packages/evm/src/evmAssetTransfer.ts b/packages/evm/src/evmAssetTransfer.ts index 615164f6f..22a14bf10 100644 --- a/packages/evm/src/evmAssetTransfer.ts +++ b/packages/evm/src/evmAssetTransfer.ts @@ -7,7 +7,7 @@ import { constants, utils } from 'ethers'; import { EvmTransfer } from './evmTransfer.js'; import type { EvmAssetTransferParams, EvmFee, TransactionRequest } from './types.js'; -import { executeDeposit } from './utils/depositFn.js'; +import { getTransactionOverrides } from './utils/depositFn.js'; import { createTransactionRequest } from './utils/transaction.js'; /** @@ -72,16 +72,15 @@ export abstract class AssetTransfer extends EvmTransfer implements IAssetTransfe const hasBalance = await this.hasEnoughBalance(fee); if (!hasBalance) throw new Error('Insufficient token balance'); - const transferTx = await executeDeposit( - this.destination.id.toString(), + const transferTransaction = await bridge.populateTransaction.deposit( + this.destinationDomain.id.toString(), this.resource.resourceId, this.getDepositData(), - fee, - bridge, - overrides, + '0x', + getTransactionOverrides(fee, overrides), ); - return createTransactionRequest(transferTx); + return createTransactionRequest(transferTransaction); } /** diff --git a/packages/evm/src/fungibleAssetTransfer.ts b/packages/evm/src/fungibleAssetTransfer.ts index cf85f46fc..b27f0dd41 100644 --- a/packages/evm/src/fungibleAssetTransfer.ts +++ b/packages/evm/src/fungibleAssetTransfer.ts @@ -1,6 +1,10 @@ -import type { EvmResource } from '@buildwithsygma/core'; +import type { EthereumConfig, EvmResource } from '@buildwithsygma/core'; import { Config, FeeHandlerType, ResourceType, SecurityModel } from '@buildwithsygma/core'; -import { Bridge__factory, ERC20__factory } from '@buildwithsygma/sygma-contracts'; +import { + Bridge__factory, + ERC20__factory, + NativeTokenAdapter__factory, +} from '@buildwithsygma/sygma-contracts'; import { Web3Provider } from '@ethersproject/providers'; import { BigNumber, constants, utils } from 'ethers'; import type { ethers, PopulatedTransaction } from 'ethers'; @@ -13,7 +17,8 @@ import type { TransactionRequest, } from './types.js'; import { approve, getERC20Allowance } from './utils/approveAndCheckFns.js'; -import { createFungibleDepositData } from './utils/assetTransferHelpers.js'; +import { createAssetDepositData } from './utils/assetTransferHelpers.js'; +import { getNativeTokenDepositTransaction } from './utils/nativeTokenDepositHelpers.js'; import { createTransactionRequest } from './utils/transaction.js'; /** @@ -60,13 +65,22 @@ class FungibleAssetTransfer extends AssetTransfer { this.optionalMessage = transfer.optionalMessage; } + protected isNativeTransfer(): boolean { + const { symbol, type } = this.resource; + const { nativeTokenSymbol } = this.config.getDomainConfig(this.sourceDomain); + + return ( + type === ResourceType.FUNGIBLE && symbol?.toLowerCase() === nativeTokenSymbol.toLowerCase() + ); + } + /** * Returns encoded deposit * data * @returns {string} */ protected getDepositData(): string { - return createFungibleDepositData({ + return createAssetDepositData({ destination: this.destination, recipientAddress: this.recipientAddress, amount: this.adjustedAmount, @@ -130,6 +144,10 @@ class FungibleAssetTransfer extends AssetTransfer { public async getApprovalTransactions( overrides?: ethers.Overrides, ): Promise> { + if (this.isNativeTransfer()) { + return []; + } + const provider = new Web3Provider(this.sourceNetworkProvider); const sourceDomainConfig = this.config.getDomainConfig(this.source); const bridge = Bridge__factory.connect(sourceDomainConfig.bridge, provider); @@ -163,6 +181,44 @@ class FungibleAssetTransfer extends AssetTransfer { return approvals.map(approval => createTransactionRequest(approval)); } + + protected async getNativeTokenDepositTransaction( + overrides?: ethers.Overrides, + ): Promise { + const domainConfig = this.config.getDomainConfig(this.source) as EthereumConfig; + const provider = new Web3Provider(this.sourceNetworkProvider); + const nativeTokenAdapter = NativeTokenAdapter__factory.connect( + domainConfig.nativeTokenAdapter, + provider, + ); + + const fee = await this.getFee(); + fee.fee += this.transferAmount; + + const payableOverrides: ethers.PayableOverrides = { ...overrides, value: fee.fee }; + + return await getNativeTokenDepositTransaction( + { + destinationNetworkId: this.destination.id.toString(), + destinationNetworkType: this.destination.type, + parachainId: this.destination.parachainId, + recipientAddress: this.recipientAddress, + optionalMessage: this.optionalMessage, + optionalGas: this.optionalGas, + depositData: this.getDepositData(), + }, + nativeTokenAdapter, + payableOverrides, + ); + } + + public async getTransferTransaction(overrides?: ethers.Overrides): Promise { + if (this.isNativeTransfer()) { + return await this.getNativeTokenDepositTransaction(overrides); + } + + return super.getTransferTransaction(overrides); + } } /** diff --git a/packages/evm/src/genericMessageTransfer.ts b/packages/evm/src/genericMessageTransfer.ts index 4ec67b6d6..ad6c49ccf 100644 --- a/packages/evm/src/genericMessageTransfer.ts +++ b/packages/evm/src/genericMessageTransfer.ts @@ -14,8 +14,8 @@ import { constants } from 'ethers'; import { EvmTransfer } from './evmTransfer.js'; import { getFeeInformation } from './fee/getFeeInformation.js'; import type { GenericMessageTransferParams, TransactionRequest } from './types.js'; +import { getTransactionOverrides } from './utils/depositFn.js'; import { createGenericCallDepositData } from './utils/genericTransferHelpers.js'; -import { executeDeposit } from './utils/index.js'; import { createTransactionRequest } from './utils/transaction.js'; /** @@ -143,16 +143,15 @@ class GenericMessageTransfer< const feeData = await this.getFee(); const depositData = this.getDepositData(); - const transaction = await executeDeposit( + const transferTransaction = await bridgeInstance.populateTransaction.deposit( this.destination.id.toString(), this.resource.resourceId, depositData, - feeData, - bridgeInstance, - overrides, + '0x', + getTransactionOverrides(feeData, overrides), ); - return createTransactionRequest(transaction); + return createTransactionRequest(transferTransaction); } /** * Get prepared additional deposit data diff --git a/packages/evm/src/nonFungibleAssetTransfer.ts b/packages/evm/src/nonFungibleAssetTransfer.ts index ab1285c76..52b1dfd89 100644 --- a/packages/evm/src/nonFungibleAssetTransfer.ts +++ b/packages/evm/src/nonFungibleAssetTransfer.ts @@ -9,7 +9,7 @@ import { providers } from 'ethers'; import { AssetTransfer } from './evmAssetTransfer.js'; import type { EvmFee, NonFungibleTransferParams, TransactionRequest } from './types.js'; -import { createFungibleDepositData } from './utils/assetTransferHelpers.js'; +import { createAssetDepositData } from './utils/assetTransferHelpers.js'; import { approve, isApproved } from './utils/index.js'; import { createTransactionRequest } from './utils/transaction.js'; @@ -31,7 +31,7 @@ class NonFungibleAssetTransfer extends AssetTransfer { * @returns {string} */ protected getDepositData(): string { - return createFungibleDepositData({ + return createAssetDepositData({ destination: this.destination, recipientAddress: this.recipientAddress, tokenId: this.tokenId, diff --git a/packages/evm/src/types.ts b/packages/evm/src/types.ts index 10858b69a..443a3d91e 100644 --- a/packages/evm/src/types.ts +++ b/packages/evm/src/types.ts @@ -102,3 +102,12 @@ export interface GenericMessageTransferParams< destinationContractAddress: string; maxFee: bigint; } + +export type NativeTokenDepositArgsWithoutMessage = [string, string]; +export type NativeTokenDepositArgsWithGeneralMessage = [string, string]; +export type NativeTokenDepositArgsWithEVMMessage = [string, string, bigint, string]; +export type NativeTokenDepositMethods = + | 'deposit' + | 'depositToEVM' + | 'depositGeneral' + | 'depositToEVMWithMessage'; diff --git a/packages/evm/src/utils/__test__/assetTransferHelpers.test.ts b/packages/evm/src/utils/__test__/assetTransferHelpers.test.ts index e1b8f5574..fbe6b9946 100644 --- a/packages/evm/src/utils/__test__/assetTransferHelpers.test.ts +++ b/packages/evm/src/utils/__test__/assetTransferHelpers.test.ts @@ -3,7 +3,7 @@ import { arrayify } from '@ethersproject/bytes'; import { utils } from 'ethers'; import { - createFungibleDepositData, + createAssetDepositData, createSubstrateMultiLocationObject, serializeEvmAddress, serializeSubstrateAddress, @@ -16,7 +16,7 @@ describe('createERCDepositData', () => { const expectedDepositData = '0x000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000141234567890123456789012345678901234567890'; - const depositData = createFungibleDepositData({ + const depositData = createAssetDepositData({ recipientAddress, amount, destination: { @@ -37,7 +37,7 @@ describe('createERCDepositData', () => { const expectedDepositData = '0x00000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000027010200511f0100fac48520983815e2022ded67ca8d27b73d51b1b022284c48b4eccbb7a328d80f'; - const depositData = createFungibleDepositData({ + const depositData = createAssetDepositData({ recipientAddress, amount, destination: { @@ -59,7 +59,7 @@ describe('createERCDepositData', () => { const expectedDepositData = '0x0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000002a746231717366797a6c3932707637776b79616a3074666a6474777663736a383430703030346a676c7670'; - const depositData = createFungibleDepositData({ + const depositData = createAssetDepositData({ recipientAddress, amount: tokenAmount, destination: { diff --git a/packages/evm/src/utils/__test__/depositFns.test.ts b/packages/evm/src/utils/__test__/depositFns.test.ts index dae0b70d1..420490f3f 100644 --- a/packages/evm/src/utils/__test__/depositFns.test.ts +++ b/packages/evm/src/utils/__test__/depositFns.test.ts @@ -1,144 +1,18 @@ import { FeeHandlerType } from '@buildwithsygma/core'; -import type { Bridge, ERC721MinterBurnerPauser } from '@buildwithsygma/sygma-contracts'; -import { type ContractReceipt, type PopulatedTransaction } from 'ethers'; import type { EvmFee } from '../../types.js'; -import * as EVM from '../depositFn.js'; - -jest.mock( - '@buildwithsygma/sygma-contracts', - () => - ({ - ...jest.requireActual('@buildwithsygma/sygma-contracts'), - Bridge__factory: { - connect: jest.fn() as unknown as Bridge, - }, - ERC721MinterBurnerPauser__factory: { - connect: jest.fn() as unknown as ERC721MinterBurnerPauser, - }, - }) as unknown, -); +import { getTransactionOverrides } from '../depositFn.js'; // Define a test suite -describe('deposit functions', () => { - let domainId: string; - let resourceId: string; - let depositData: string; - let feeData: EvmFee; - - beforeEach(() => { - domainId = 'domainId'; - resourceId = 'resourceId'; - depositData = 'depositData'; - feeData = { +describe('getTransactionOverrides', () => { + it('should assign correct values', () => { + const evmFee: EvmFee = { + fee: 1n, type: FeeHandlerType.BASIC, - fee: BigInt('100'), - tokenAddress: '0x00', - handlerAddress: '0x9867', + handlerAddress: '', }; - jest.clearAllMocks(); - }); - describe('executeDeposit', () => { - it('should successfully execute deposit', async () => { - const bridgeInstance = { - populateTransaction: { - deposit: jest.fn().mockResolvedValueOnce({ - wait: jest.fn().mockResolvedValueOnce({} as ContractReceipt), - } as unknown as PopulatedTransaction), - }, - }; - - const result = await EVM.executeDeposit( - domainId, - resourceId, - depositData, - feeData, - bridgeInstance as unknown as Bridge, - ); - - expect(result).toBeDefined(); - expect(bridgeInstance.populateTransaction.deposit).toHaveBeenCalledTimes(1); - }); - - it('should successfully call deposit method on contract without overrides', async () => { - const bridgeInstance = { - populateTransaction: { - deposit: jest.fn().mockResolvedValueOnce({ - wait: jest.fn().mockResolvedValueOnce({} as ContractReceipt), - } as unknown as PopulatedTransaction), - }, - }; - - const result = await EVM.executeDeposit( - domainId, - resourceId, - depositData, - feeData, - bridgeInstance as unknown as Bridge, - ); - - expect(result).toBeDefined(); - expect(bridgeInstance.populateTransaction.deposit).toHaveBeenCalledTimes(1); - expect(bridgeInstance.populateTransaction.deposit).toHaveBeenCalledWith( - 'domainId', - 'resourceId', - 'depositData', - '0x', - { value: feeData.fee, gasLimit: EVM.ASSET_TRANSFER_GAS_LIMIT }, - ); - }); - - it('should successfully call deposit method on contract without overrides and with dynamic (oracle) fee', async () => { - feeData = { - type: FeeHandlerType.PERCENTAGE, - fee: BigInt('100'), - tokenAddress: '0x00', - handlerAddress: '0x123', - }; - const bridgeInstance = { - populateTransaction: { - deposit: jest.fn().mockResolvedValueOnce({ - wait: jest.fn().mockResolvedValueOnce({} as ContractReceipt), - } as unknown as PopulatedTransaction), - }, - }; - - const result = await EVM.executeDeposit( - domainId, - resourceId, - depositData, - feeData, - bridgeInstance as unknown as Bridge, - ); - - expect(result).toBeDefined(); - expect(bridgeInstance.populateTransaction.deposit).toHaveBeenCalledTimes(1); - expect(bridgeInstance.populateTransaction.deposit).toHaveBeenCalledWith( - 'domainId', - 'resourceId', - 'depositData', - '0x', - { gasLimit: EVM.ASSET_TRANSFER_GAS_LIMIT, value: 0 }, - ); - }); - - it('should handle error on execute deposit', async () => { - const bridgeInstance = { - populateTransaction: { - deposit: jest.fn().mockRejectedValueOnce(new Error('Deposit failed')), - }, - }; - - await expect( - EVM.executeDeposit( - domainId, - resourceId, - depositData, - feeData, - bridgeInstance as unknown as Bridge, - ), - ).rejects.toThrowError('Deposit failed'); - }); + const overrides = getTransactionOverrides(evmFee); + expect(overrides.value).toEqual(evmFee.fee); }); }); diff --git a/packages/evm/src/utils/assetTransferHelpers.ts b/packages/evm/src/utils/assetTransferHelpers.ts index 5a071fe21..3ba96ed4d 100644 --- a/packages/evm/src/utils/assetTransferHelpers.ts +++ b/packages/evm/src/utils/assetTransferHelpers.ts @@ -11,7 +11,7 @@ import type { FungibleTransferOptionalMessage } from '../types.js'; const ACTIONS_ARRAY_ABI = 'tuple(uint256 nativeValue, address callTo, address approveTo, address tokenSend, address tokenReceive, bytes data)[]'; -interface FungbileDepositParams { +interface AssetDepositParams { destination: Domain; recipientAddress: string; amount?: bigint; @@ -66,33 +66,59 @@ export function serializeSubstrateAddress( return registry.createType('MultiLocation', JSON.parse(multilocationObject)).toU8a(); } -export function createFungibleDepositData(depositParams: FungbileDepositParams): string { - const { recipientAddress, destination, amount, tokenId, optionalGas, optionalMessage } = - depositParams; - let recipientAddressSerialized: Uint8Array; - - switch (destination.type) { +export function serializeDestinationAddress( + address: string, + type: Network, + parachainId?: number, +): Uint8Array { + switch (type) { case Network.EVM: { - recipientAddressSerialized = serializeEvmAddress(recipientAddress as `0x${string}`); - break; + return serializeEvmAddress(address as `0x${string}`); } case Network.SUBSTRATE: { - recipientAddressSerialized = serializeSubstrateAddress( - recipientAddress, - destination.parachainId, - ); - break; + return serializeSubstrateAddress(address, parachainId); } case Network.BITCOIN: { - recipientAddressSerialized = utils.arrayify( - `${utils.hexlify(utils.toUtf8Bytes(recipientAddress))}`, - ); - break; + return utils.arrayify(`${utils.hexlify(utils.toUtf8Bytes(address))}`); } default: { throw new Error('Unsupported destination network type.'); } } +} + +export function encodeOptionalMessage(optionalMessage: FungibleTransferOptionalMessage): string { + const { transactionId, actions, receiver } = optionalMessage; + const abiCoder = new AbiCoder(); + + const optionalMessageEncoded = abiCoder.encode( + ['bytes32', ACTIONS_ARRAY_ABI, 'address'], + [ + transactionId, + actions.map(action => [ + action.nativeValue, + action.callTo, + action.approveTo, + action.tokenSend, + action.tokenReceive, + action.data, + ]), + receiver, + ], + ); + + return optionalMessageEncoded; +} + +export function createAssetDepositData(depositParams: AssetDepositParams): string { + const { recipientAddress, destination, amount, tokenId, optionalGas, optionalMessage } = + depositParams; + + const recipientAddressSerialized: Uint8Array = serializeDestinationAddress( + recipientAddress, + destination.type, + destination.parachainId, + ); const val = amount !== undefined ? amount : tokenId !== undefined ? tokenId : null; if (val === null) throw new Error('Token Amount Or ID is required.'); @@ -102,6 +128,7 @@ export function createFungibleDepositData(depositParams: FungbileDepositParams): const zeroPaddedAmount = hexZeroPad(tokenAmountOrIdInHex, HEX_PADDING); const addressLenInHex = BigNumber.from(recipientAddressSerialized.length).toHexString(); const zeroPaddedAddrLen = hexZeroPad(addressLenInHex, HEX_PADDING); + let depositData = concat([zeroPaddedAmount, zeroPaddedAddrLen, recipientAddressSerialized]); if (optionalMessage !== undefined && optionalGas !== undefined) { @@ -109,25 +136,7 @@ export function createFungibleDepositData(depositParams: FungbileDepositParams): const zeroPaddedOptionalGas = hexZeroPad(optionalGasInHex, HEX_PADDING); depositData = concat([depositData, zeroPaddedOptionalGas]); - const { transactionId, actions, receiver } = optionalMessage; - const abiCoder = new AbiCoder(); - - const optionalMessageEncoded = abiCoder.encode( - ['bytes32', ACTIONS_ARRAY_ABI, 'address'], - [ - transactionId, - actions.map(action => [ - action.nativeValue, - action.callTo, - action.approveTo, - action.tokenSend, - action.tokenReceive, - action.data, - ]), - receiver, - ], - ); - + const optionalMessageEncoded = encodeOptionalMessage(optionalMessage); const optionalMessageSeriailzed = arrayify(optionalMessageEncoded); const optionalMsgLenInHex = BigNumber.from(optionalMessageSeriailzed.length).toHexString(); const zeroPaddedOptionalMsgLen = hexZeroPad(optionalMsgLenInHex, HEX_PADDING); diff --git a/packages/evm/src/utils/depositFn.ts b/packages/evm/src/utils/depositFn.ts index a1dd054a4..68b6f2bb4 100644 --- a/packages/evm/src/utils/depositFn.ts +++ b/packages/evm/src/utils/depositFn.ts @@ -1,53 +1,22 @@ import { FeeHandlerType } from '@buildwithsygma/core'; -import type { Bridge } from '@buildwithsygma/sygma-contracts'; -import type { PopulatedTransaction, ethers } from 'ethers'; +import type { ethers } from 'ethers'; import { BigNumber } from 'ethers'; import type { EvmFee } from '../types.js'; export const ASSET_TRANSFER_GAS_LIMIT: BigNumber = BigNumber.from(300000); -/** - * Executes a deposit operation using the specified parameters and returns a populated transaction. - * - * - * @category Bridge deposit - * @param {string} domainId - The unique identifier for destination network. - * @param {string} resourceId - The resource ID associated with the token. - * @param {string} depositData - The deposit data required for the operation. - * @param {FeeDataResult} feeData - The fee data result for the deposit operation. - * @param {Bridge} bridgeInstance - The bridge instance used to perform the deposit operation. - * @returns {Promise} Unsigned transaction - */ -export const executeDeposit = async ( - domainId: string, - resourceId: string, - depositData: string, - feeData: EvmFee, - bridgeInstance: Bridge, - overrides?: ethers.PayableOverrides, -): Promise => { - const transactionSettings = { - /** - * @remarks - * "twap" and "basic" both deduct in native currency - */ - value: feeData.type == FeeHandlerType.PERCENTAGE ? 0 : feeData.fee, +export function getTransactionOverrides( + fee: EvmFee, + overrides?: ethers.Overrides, +): ethers.PayableOverrides { + const sygmaOverrides = { gasLimit: ASSET_TRANSFER_GAS_LIMIT, + value: fee.type === FeeHandlerType.PERCENTAGE ? 0 : fee.fee, }; - const payableOverrides = { - ...transactionSettings, + return { + ...sygmaOverrides, ...overrides, }; - - const depositTransaction = await bridgeInstance.populateTransaction.deposit( - domainId, - resourceId, - depositData, - '0x', - payableOverrides, - ); - - return depositTransaction; -}; +} diff --git a/packages/evm/src/utils/nativeTokenDepositHelpers.ts b/packages/evm/src/utils/nativeTokenDepositHelpers.ts new file mode 100644 index 000000000..5d9acde68 --- /dev/null +++ b/packages/evm/src/utils/nativeTokenDepositHelpers.ts @@ -0,0 +1,125 @@ +import type { ParachainId } from '@buildwithsygma/core'; +import { Network } from '@buildwithsygma/core'; +import type { NativeTokenAdapter } from '@buildwithsygma/sygma-contracts'; +import { hexlify } from '@ethersproject/bytes'; +import type { ethers } from 'ethers'; + +import type { + FungibleTransferOptionalMessage, + NativeTokenDepositArgsWithEVMMessage, + NativeTokenDepositArgsWithGeneralMessage, + NativeTokenDepositArgsWithoutMessage, + NativeTokenDepositMethods, + TransactionRequest, +} from '../types.js'; + +import { serializeDestinationAddress, encodeOptionalMessage } from './assetTransferHelpers.js'; +import { createTransactionRequest } from './transaction.js'; + +export function getNativeTokenDepositMethod( + destinationNetworkType: Network, + optionalMessage?: FungibleTransferOptionalMessage, +): NativeTokenDepositMethods { + if (!optionalMessage) { + if (destinationNetworkType === Network.EVM) { + return 'depositToEVM'; + } + return 'deposit'; + } else { + if (destinationNetworkType === Network.EVM) { + return 'depositToEVMWithMessage'; + } + + return 'depositGeneral'; + } +} + +interface NativeDepositParams { + method: NativeTokenDepositMethods; + recipientAddress: string; + destinationNetworkType: Network; + destinationNetworkId: string; + parachainId?: ParachainId; + optionalMessage?: FungibleTransferOptionalMessage; + optionalGas?: bigint; + depositData: string; +} + +export function getNativeTokenDepositContractArgs( + args: NativeDepositParams, +): + | NativeTokenDepositArgsWithoutMessage + | NativeTokenDepositArgsWithEVMMessage + | NativeTokenDepositArgsWithGeneralMessage { + const { + method, + destinationNetworkId, + recipientAddress, + destinationNetworkType, + parachainId, + optionalMessage, + optionalGas, + depositData, + } = args; + + switch (method) { + case 'deposit': + case 'depositToEVM': + return [ + destinationNetworkId, + hexlify(serializeDestinationAddress(recipientAddress, destinationNetworkType, parachainId)), + ]; + case 'depositToEVMWithMessage': + return [ + destinationNetworkId, + recipientAddress, + optionalGas!, + encodeOptionalMessage(optionalMessage!), + ]; + case 'depositGeneral': + return [destinationNetworkId, `0x${depositData.substring(66)}`]; + } +} + +export async function getNativeTokenDepositTransaction( + depositParams: Omit, + nativeTokenAdapter: NativeTokenAdapter, + overrides?: ethers.PayableOverrides, +): Promise { + const method = getNativeTokenDepositMethod( + depositParams.destinationNetworkType, + depositParams.optionalMessage, + ); + + const args = getNativeTokenDepositContractArgs({ + method, + ...depositParams, + }); + + switch (method) { + case 'deposit': + case 'depositToEVM': + return createTransactionRequest( + await nativeTokenAdapter.populateTransaction[method]( + ...(args as NativeTokenDepositArgsWithoutMessage), + overrides, + ), + ); + case 'depositToEVMWithMessage': + return createTransactionRequest( + await nativeTokenAdapter.populateTransaction[method]( + ...(args as NativeTokenDepositArgsWithEVMMessage), + overrides, + ), + ); + case 'depositGeneral': + return createTransactionRequest( + await nativeTokenAdapter.populateTransaction[method]( + ...(args as NativeTokenDepositArgsWithGeneralMessage), + overrides, + ), + ); + default: + throw new Error('Unsupported method'); + } +} diff --git a/yarn.lock b/yarn.lock index f369eeba7..446a2f8eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -553,7 +553,7 @@ __metadata: resolution: "@buildwithsygma/evm@workspace:packages/evm" dependencies: "@buildwithsygma/core": "workspace:^" - "@buildwithsygma/sygma-contracts": "npm:^2.8.0" + "@buildwithsygma/sygma-contracts": "npm:^2.10.1" "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/bytes": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" @@ -642,15 +642,16 @@ __metadata: languageName: node linkType: hard -"@buildwithsygma/sygma-contracts@npm:^2.8.0": - version: 2.9.0 - resolution: "@buildwithsygma/sygma-contracts@npm:2.9.0" +"@buildwithsygma/sygma-contracts@npm:^2.10.1": + version: 2.10.1 + resolution: "@buildwithsygma/sygma-contracts@npm:2.10.1" dependencies: "@openzeppelin/contracts": "npm:4.5.0" "@uniswap/v3-periphery": "npm:^1.4.4" + solmate: "npm:^6.2.0" peerDependencies: ethers: ">= 5.0.0" - checksum: 0f98a64da674e1264caaf429665a23b7db63365d7a8c5a38d8e23f0b728faecd6bfd3a0c43616bd4aa43133229324f14d4c3d0e2e24225d223574d5e94af9263 + checksum: c6cddf6a427a4d3ab1321fbc21f9e47a64661823ffdb55db230ce9874878125735aad260d3aef454f7681f2d2f7d446882f82f1cf3e7f69209e183b69f375c4a languageName: node linkType: hard @@ -10573,6 +10574,13 @@ __metadata: languageName: node linkType: hard +"solmate@npm:^6.2.0": + version: 6.7.0 + resolution: "solmate@npm:6.7.0" + checksum: 7c51286ac486de2cace00fd136a210d01e2dc28403373583ce5acd49178f1cf4421d3f3a945f28af36e7fcedcea194238b2b5eb53be78a04b9e04f32ea8d286f + languageName: node + linkType: hard + "source-map-support@npm:0.5.13": version: 0.5.13 resolution: "source-map-support@npm:0.5.13"