diff --git a/examples/evm-to-evm-fungible-transfer/src/transfer.ts b/examples/evm-to-evm-fungible-transfer/src/transfer.ts index 371f6718f..d08fa2723 100644 --- a/examples/evm-to-evm-fungible-transfer/src/transfer.ts +++ b/examples/evm-to-evm-fungible-transfer/src/transfer.ts @@ -1,4 +1,4 @@ -import { Eip1193Provider, getSygmaScanLink } from "@buildwithsygma/core"; +import { getSygmaScanLink, type Eip1193Provider } from "@buildwithsygma/core"; import { createFungibleAssetTransfer, FungibleTransferParams, @@ -16,11 +16,12 @@ if (!privateKey) { } const SEPOLIA_CHAIN_ID = 11155111; -const AMOY_CHAIN_ID = 80002; +const AMOY_CHAIN_ID = 84532; const RESOURCE_ID = - "0x0000000000000000000000000000000000000000000000000000000000000300"; + "0x0000000000000000000000000000000000000000000000000000000000001200"; const SEPOLIA_RPC_URL = - process.env.SEPOLIA_RPC_URL || "https://eth-sepolia-public.unifra.io"; + process.env.SEPOLIA_RPC_URL || + "https://eth-sepolia.g.alchemy.com/v2/MeCKDrpxLkGOn4LMlBa3cKy1EzzOzwzG"; const explorerUrls: Record = { [SEPOLIA_CHAIN_ID]: "https://sepolia.etherscan.io", @@ -42,9 +43,10 @@ export async function erc20Transfer(): Promise { destination: AMOY_CHAIN_ID, sourceNetworkProvider: web3Provider as unknown as Eip1193Provider, resource: RESOURCE_ID, - amount: BigInt(1) * BigInt(1e18), + amount: BigInt(1) * BigInt(1e6), recipientAddress: destinationAddress, - sourceAddress, + sourceAddress: sourceAddress, + optionalGas: BigInt(500000), }; const transfer = await createFungibleAssetTransfer(params); diff --git a/examples/substrate-to-evm-fungible-transfer/src/transfer.ts b/examples/substrate-to-evm-fungible-transfer/src/transfer.ts index df1951a51..e6b257c13 100644 --- a/examples/substrate-to-evm-fungible-transfer/src/transfer.ts +++ b/examples/substrate-to-evm-fungible-transfer/src/transfer.ts @@ -44,7 +44,6 @@ const substrateTransfer = async (): Promise => { resource: RESOURCE_ID_SYGMA_USD, amount: BigInt("1"), destinationAddress: recipient, - sourceAddress: account.address, }; const transfer = await createSubstrateFungibleAssetTransfer(transferParams); diff --git a/packages/evm/src/evmAssetTransfer.ts b/packages/evm/src/evmAssetTransfer.ts index 75cc9c573..6dbbab6d2 100644 --- a/packages/evm/src/evmAssetTransfer.ts +++ b/packages/evm/src/evmAssetTransfer.ts @@ -2,11 +2,12 @@ import type { Config } from '@buildwithsygma/core'; import { isValidAddressForNetwork } from '@buildwithsygma/core'; import { Bridge__factory } from '@buildwithsygma/sygma-contracts'; import { Web3Provider } from '@ethersproject/providers'; +import type { PayableOverrides } from 'ethers'; import { constants, utils } from 'ethers'; import { EvmTransfer } from './evmTransfer.js'; import type { EvmAssetTransferParams, EvmFee, TransactionRequest } from './types.js'; -import { assetTransfer } from './utils/index.js'; +import { executeDeposit } from './utils/depositFn.js'; import { createTransactionRequest } from './utils/transaction.js'; /** @@ -62,7 +63,7 @@ export abstract class AssetTransfer extends EvmTransfer implements IAssetTransfe * Get transfer transaction * @returns {Promise} */ - public async getTransferTransaction(): Promise { + public async getTransferTransaction(overrides?: PayableOverrides): Promise { const domainConfig = this.config.getDomainConfig(this.source); const provider = new Web3Provider(this.sourceNetworkProvider); const bridge = Bridge__factory.connect(domainConfig.bridge, provider); @@ -71,13 +72,14 @@ 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 assetTransfer({ - depositData: this.getDepositData(), - bridgeInstance: bridge, - domainId: this.destination.id.toString(), - resourceId: this.resource.resourceId, - feeData: fee, - }); + const transferTx = await executeDeposit( + this.destination.id.toString(), + this.resource.resourceId, + this.getDepositData(), + fee, + bridge, + overrides, + ); return createTransactionRequest(transferTx); } diff --git a/packages/evm/src/fungibleAssetTransfer.ts b/packages/evm/src/fungibleAssetTransfer.ts index 095584500..e08ad5855 100644 --- a/packages/evm/src/fungibleAssetTransfer.ts +++ b/packages/evm/src/fungibleAssetTransfer.ts @@ -5,9 +5,14 @@ import { Web3Provider } from '@ethersproject/providers'; import { BigNumber, constants, type PopulatedTransaction, utils } from 'ethers'; import { AssetTransfer } from './evmAssetTransfer.js'; -import type { EvmFee, FungibleTransferParams, TransactionRequest } from './types.js'; +import type { + EvmFee, + FungibleTransferOptionalMessage, + FungibleTransferParams, + TransactionRequest, +} from './types.js'; import { approve, getERC20Allowance } from './utils/approveAndCheckFns.js'; -import { createERCDepositData } from './utils/helpers.js'; +import { createFungibleDepositData } from './utils/assetTransferHelpers.js'; import { createTransactionRequest } from './utils/transaction.js'; /** @@ -36,6 +41,9 @@ class FungibleAssetTransfer extends AssetTransfer { protected declare adjustedAmount: bigint; protected specifiedAmount: bigint; + protected optionalGas?: bigint; + protected optionalMessage?: FungibleTransferOptionalMessage; + /** * Returns amount to be transferred considering the fee */ @@ -47,6 +55,8 @@ class FungibleAssetTransfer extends AssetTransfer { super(transfer, config); this.specifiedAmount = transfer.amount; this.securityModel = transfer.securityModel ?? SecurityModel.MPC; + this.optionalGas = transfer.optionalGas; + this.optionalMessage = transfer.optionalMessage; } /** @@ -55,7 +65,13 @@ class FungibleAssetTransfer extends AssetTransfer { * @returns {string} */ protected getDepositData(): string { - return createERCDepositData(this.adjustedAmount, this.recipient, this.destination.parachainId); + return createFungibleDepositData({ + destination: this.destination, + recipientAddress: this.recipientAddress, + amount: this.adjustedAmount, + optionalGas: this.optionalGas, + optionalMessage: this.optionalMessage, + }); } /** diff --git a/packages/evm/src/genericMessageTransfer.ts b/packages/evm/src/genericMessageTransfer.ts index f193fada0..4ec67b6d6 100644 --- a/packages/evm/src/genericMessageTransfer.ts +++ b/packages/evm/src/genericMessageTransfer.ts @@ -1,4 +1,4 @@ -import type { EthereumConfig, EvmResource, SubstrateResource } from '@buildwithsygma/core'; +import type { EthereumConfig } from '@buildwithsygma/core'; import { Config, Network, ResourceType } from '@buildwithsygma/core'; import { Bridge__factory } from '@buildwithsygma/sygma-contracts'; import { Web3Provider } from '@ethersproject/providers'; @@ -8,16 +8,14 @@ import type { ExtractAbiFunction, ExtractAbiFunctionNames, } from 'abitype'; -import { constants, ethers } from 'ethers'; +import type { ethers } from 'ethers'; +import { constants } from 'ethers'; import { EvmTransfer } from './evmTransfer.js'; import { getFeeInformation } from './fee/getFeeInformation.js'; import type { GenericMessageTransferParams, TransactionRequest } from './types.js'; -import { - createPermissionlessGenericDepositData, - serializeGenericCallParameters, -} from './utils/helpers.js'; -import { genericMessageTransfer } from './utils/index.js'; +import { createGenericCallDepositData } from './utils/genericTransferHelpers.js'; +import { executeDeposit } from './utils/index.js'; import { createTransactionRequest } from './utils/transaction.js'; /** @@ -57,43 +55,6 @@ class GenericMessageTransfer< this.maxFee = params.maxFee; } - /** - * Get prepared additional deposit data - * in hex string format - * @returns {string} - */ - protected getDepositData(): string { - const { executeFunctionSignature, executionData } = this.prepareFunctionCallEncodings(); - return createPermissionlessGenericDepositData( - executeFunctionSignature, - this.destinationContractAddress, - this.maxFee.toString(), - this.sourceAddress, - executionData, - ); - } - - /** - * Prepare function call encodings - * @returns {{ executionData: string; executionFunctionSignature: string; }} - */ - private prepareFunctionCallEncodings(): { - executionData: string; - executeFunctionSignature: string; - } { - const contractInterface = new ethers.utils.Interface( - JSON.stringify(this.destinationContractAbi), - ); - - let executionData = ``; - if (Array.isArray(this.functionParameters)) { - executionData = serializeGenericCallParameters(this.functionParameters); - } - - const executeFunctionSignature = contractInterface.getSighash(this.functionName); - return { executionData, executeFunctionSignature }; - } - /** * Checks validity of the transfer * @returns {Promise} @@ -166,17 +127,6 @@ class GenericMessageTransfer< ): void { this.functionParameters = parameters; } - - public setResource(resource: EvmResource | SubstrateResource): void { - if ( - resource.type !== ResourceType.PERMISSIONED_GENERIC && - resource.type !== ResourceType.PERMISSIONLESS_GENERIC - ) { - throw new Error('Unsupported Resource type.'); - } - this.transferResource = resource; - } - /** * Get the cross chain generic message transfer * transaction @@ -187,33 +137,39 @@ class GenericMessageTransfer< const isValid = await this.isValidTransfer(); if (!isValid) throw new Error('Invalid Transfer.'); - const { executeFunctionSignature, executionData } = this.prepareFunctionCallEncodings(); - const { resourceId } = this.resource; - - const executeContractAddress = this.destinationContractAddress; const sourceDomain = this.config.getDomainConfig(this.source); - const domainId = this.config.getDomainConfig(this.destination).id.toString(); const provider = new Web3Provider(this.sourceNetworkProvider); - const depositor = this.sourceAddress; - const maxFee = this.maxFee.toString(); const bridgeInstance = Bridge__factory.connect(sourceDomain.bridge, provider); const feeData = await this.getFee(); + const depositData = this.getDepositData(); - const transaction = await genericMessageTransfer({ - executeFunctionSignature, - executeContractAddress, - maxFee, - depositor, - executionData, - bridgeInstance, - domainId, - resourceId, + const transaction = await executeDeposit( + this.destination.id.toString(), + this.resource.resourceId, + depositData, feeData, + bridgeInstance, overrides, - }); + ); return createTransactionRequest(transaction); } + /** + * Get prepared additional deposit data + * in hex string format + * @returns {string} + */ + protected getDepositData(): string { + return createGenericCallDepositData({ + abi: this.destinationContractAbi, + functionName: this.functionName, + functionParams: this.functionParameters, + contractAddress: this.destinationContractAddress, + destination: this.destination, + maxFee: this.maxFee, + depositor: this.sourceAddress as `0x${string}`, + }); + } } /** diff --git a/packages/evm/src/nonFungibleAssetTransfer.ts b/packages/evm/src/nonFungibleAssetTransfer.ts index 87ff8af53..5f74155f2 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 { createERCDepositData } from './utils/helpers.js'; +import { createFungibleDepositData } from './utils/assetTransferHelpers.js'; import { approve, isApproved } from './utils/index.js'; import { createTransactionRequest } from './utils/transaction.js'; @@ -31,7 +31,11 @@ class NonFungibleAssetTransfer extends AssetTransfer { * @returns {string} */ protected getDepositData(): string { - return createERCDepositData(BigInt(this.tokenId), this.recipient, this.destination.parachainId); + return createFungibleDepositData({ + destination: this.destination, + recipientAddress: this.recipientAddress, + tokenId: this.tokenId, + }); } /** diff --git a/packages/evm/src/types.ts b/packages/evm/src/types.ts index 4509740b4..e9b93af1b 100644 --- a/packages/evm/src/types.ts +++ b/packages/evm/src/types.ts @@ -45,6 +45,21 @@ export type EvmFee = { /** An EVM resource is accepted as either the resource object or it's Sygma ID */ export type EvmResourceish = string | EvmResource; +interface FungibleDepositAction { + nativeValue: bigint; + callTo: string; + approveTo: string; + tokenSend: string; + tokenReceive: string; + data: string; +} + +export interface FungibleTransferOptionalMessage { + transactionId: string; + actions: FungibleDepositAction[]; + receiver: string; +} + export interface EvmTransferParams extends BaseTransferParams { sourceAddress: string; sourceNetworkProvider: Eip1193Provider; @@ -59,6 +74,8 @@ export interface EvmAssetTransferParams extends EvmTransferParams { export interface FungibleTransferParams extends EvmAssetTransferParams { amount: bigint; securityModel?: SecurityModel; + optionalGas?: bigint; + optionalMessage?: FungibleTransferOptionalMessage; } export interface NonFungibleTransferParams extends EvmAssetTransferParams { diff --git a/packages/evm/src/utils/__test__/helpers.test.ts b/packages/evm/src/utils/__test__/assetTransferHelpers.test.ts similarity index 55% rename from packages/evm/src/utils/__test__/helpers.test.ts rename to packages/evm/src/utils/__test__/assetTransferHelpers.test.ts index aa24a1460..e1b8f5574 100644 --- a/packages/evm/src/utils/__test__/helpers.test.ts +++ b/packages/evm/src/utils/__test__/assetTransferHelpers.test.ts @@ -1,34 +1,54 @@ -import { BigNumber, utils } from 'ethers'; +import { Network } from '@buildwithsygma/core'; +import { arrayify } from '@ethersproject/bytes'; +import { utils } from 'ethers'; import { - getEVMRecipientAddressInBytes, - getSubstrateRecipientAddressInBytes, - createERCDepositData, - toHex, - constructSubstrateRecipient, - addressToHex, - serializeGenericCallParameters, -} from '../helpers.js'; + createFungibleDepositData, + createSubstrateMultiLocationObject, + serializeEvmAddress, + serializeSubstrateAddress, +} from '../assetTransferHelpers.js'; describe('createERCDepositData', () => { it('should return the correct deposit data', () => { - const tokenAmount = BigInt(100); + const amount = BigInt(100); const recipientAddress = '0x1234567890123456789012345678901234567890'; const expectedDepositData = '0x000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000141234567890123456789012345678901234567890'; - const depositData = createERCDepositData(tokenAmount, recipientAddress); + const depositData = createFungibleDepositData({ + recipientAddress, + amount, + destination: { + name: 'EVM', + type: Network.EVM, + caipId: '11', + chainId: 1, + id: 1, + }, + }); expect(depositData).toEqual(expectedDepositData); }); it('should return the correct deposit data - substrate', () => { - const tokenAmount = BigInt(100); + const amount = BigInt(100); const recipientAddress = '46Hb742ujLfMA1nGsw95xTbjt6SzGSiNgXsjPQXyz3PoQuNQ'; const expectedDepositData = '0x00000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000027010200511f0100fac48520983815e2022ded67ca8d27b73d51b1b022284c48b4eccbb7a328d80f'; - const depositData = createERCDepositData(tokenAmount, recipientAddress, 2004); + const depositData = createFungibleDepositData({ + recipientAddress, + amount, + destination: { + name: 'EVM', + type: Network.SUBSTRATE, + caipId: '11', + chainId: 1, + id: 1, + parachainId: 2004, + }, + }); expect(depositData).toEqual(expectedDepositData); }); @@ -39,15 +59,25 @@ describe('createERCDepositData', () => { const expectedDepositData = '0x0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000002a746231717366797a6c3932707637776b79616a3074666a6474777663736a383430703030346a676c7670'; - const depositData = createERCDepositData(tokenAmount, recipientAddress); + const depositData = createFungibleDepositData({ + recipientAddress, + amount: tokenAmount, + destination: { + name: 'bitcoin', + type: Network.BITCOIN, + caipId: '11', + chainId: 1, + id: 1, + }, + }); expect(depositData).toEqual(expectedDepositData); }); }); -describe('constructSubstrateRecipient', () => { +describe('createSubstrateMultiLocationObject', () => { it('should create a valid Substrate Multilocation Object with parachain id', () => { const substrateAddress = '5CDQJk6kxvBcjauhrogUc9B8vhbdXhRscp1tGEUmniryF1Vt'; - const result = constructSubstrateRecipient(substrateAddress, 2004); + const result = createSubstrateMultiLocationObject(substrateAddress, 2004); const expectedResult = '{"parents":1,"interior":{"X2":[{"parachain":2004},{"AccountId32":{"network":{"any":null},"id":"0x06a220edf5f82b84fc5f9270f8a30a17636bf29c05a5c16279405ca20918aa39"}}]}}'; expect(result).toEqual(expectedResult); @@ -55,38 +85,19 @@ describe('constructSubstrateRecipient', () => { it('should create a valid Substrate Multilocation Object', () => { const substrateAddress = '5CDQJk6kxvBcjauhrogUc9B8vhbdXhRscp1tGEUmniryF1Vt'; - const result = constructSubstrateRecipient(substrateAddress); + const result = createSubstrateMultiLocationObject(substrateAddress); const expectedResult = '{"parents":0,"interior":{"X1":{"AccountId32":{"network":{"any":null},"id":"0x06a220edf5f82b84fc5f9270f8a30a17636bf29c05a5c16279405ca20918aa39"}}}}'; expect(result).toEqual(expectedResult); }); }); -describe('serializeGenericCallParameters', () => { - it('should seriailze parameters correctly', () => { - const result = - '0x0000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064'; - - const params = [ - '0x98729c03c4D5e820F5e8c45558ae07aE63F97461', - BigInt(50), - true, - false, - BigNumber.from(100), - ]; - - const serialized = serializeGenericCallParameters(params); - - expect(serialized).toEqual(result); - }); -}); - -describe('getEVMRecipientAddressInBytes', () => { +describe('serializeEvmAddress', () => { it('should convert an EVM address to a Uint8Array of bytes', () => { const evmAddress = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'; expect(utils.isAddress(evmAddress)).toBeTruthy(); - const result = getEVMRecipientAddressInBytes(evmAddress); + const result = serializeEvmAddress(evmAddress); const expectedResult = utils.arrayify(evmAddress); expect(result).toEqual(expectedResult); @@ -100,7 +111,7 @@ describe('getSubstrateRecipientAddressInBytes', () => { expect(utils.isAddress(substrateAddress)).toBeFalsy(); - const result = getSubstrateRecipientAddressInBytes(substrateAddress); + const result = arrayify(serializeSubstrateAddress(substrateAddress)); const expectedResult = Uint8Array.from([ 0, 1, 1, 0, 6, 162, 32, 237, 245, 248, 43, 132, 252, 95, 146, 112, 248, 163, 10, 23, 99, 107, 242, 156, 5, 165, 193, 98, 121, 64, 92, 162, 9, 24, 170, 57, @@ -115,7 +126,7 @@ describe('getSubstrateRecipientAddressInBytes', () => { expect(utils.isAddress(substrateAddress)).toBeFalsy(); - const result = getSubstrateRecipientAddressInBytes(substrateAddress, 1001); + const result = arrayify(serializeSubstrateAddress(substrateAddress, 1001)); const expectedResult = Uint8Array.from([ 1, 2, 0, 165, 15, 1, 0, 6, 162, 32, 237, 245, 248, 43, 132, 252, 95, 146, 112, 248, 163, 10, 23, 99, 107, 242, 156, 5, 165, 193, 98, 121, 64, 92, 162, 9, 24, 170, 57, @@ -125,41 +136,3 @@ describe('getSubstrateRecipientAddressInBytes', () => { expect(result).toBeInstanceOf(Uint8Array); }); }); - -describe('toHex', () => { - test('should convert string to hex', () => { - const result = toHex('1234', 6); - expect(result).toBe('0x0000000004d2'); - }); - - test('should convert number to hex', () => { - const result = toHex(5678, 8); - expect(result).toBe('0x000000000000162e'); - }); - - test('should convert BigNumber to hex', () => { - const num = BigNumber.from('900000000000000000000000'); - const result = toHex(num, 32); - expect(result).toBe('0x00000000000000000000000000000000000000000000be951906eba2aa800000'); - }); -}); - -describe('addressToHex', () => { - test('should convert p2tr address to hex', () => { - const address = 'tb1pnyh5nayrmwux72guec3xy7qryjjww6tu9mev3d5347lqcwgus4jsd95d2r'; - const expectedHex = - '0x746231706e7968356e6179726d777578373267756563337879377172796a6a7777367475396d65763364353334376c716377677573346a73643935643272'; - - const result = addressToHex(address); - expect(result).toEqual(expectedHex); - }); - - test('should convert p2wpkh address to hex', () => { - const address = 'tb1qsfyzl92pv7wkyaj0tfjdtwvcsj840p004jglvp'; - const expectedHex = - '0x746231717366797a6c3932707637776b79616a3074666a6474777663736a383430703030346a676c7670'; - - const result = addressToHex(address); - expect(result).toEqual(expectedHex); - }); -}); diff --git a/packages/evm/src/utils/__test__/depositFns.test.ts b/packages/evm/src/utils/__test__/depositFns.test.ts index a335b926b..dae0b70d1 100644 --- a/packages/evm/src/utils/__test__/depositFns.test.ts +++ b/packages/evm/src/utils/__test__/depositFns.test.ts @@ -1,9 +1,9 @@ import { FeeHandlerType } from '@buildwithsygma/core'; import type { Bridge, ERC721MinterBurnerPauser } from '@buildwithsygma/sygma-contracts'; -import { type ethers, type ContractReceipt, type PopulatedTransaction } from 'ethers'; +import { type ContractReceipt, type PopulatedTransaction } from 'ethers'; import type { EvmFee } from '../../types.js'; -import * as EVM from '../depositFns.js'; +import * as EVM from '../depositFn.js'; jest.mock( '@buildwithsygma/sygma-contracts', @@ -25,7 +25,6 @@ describe('deposit functions', () => { let resourceId: string; let depositData: string; let feeData: EvmFee; - let bridgeInstance: Bridge; beforeEach(() => { domainId = 'domainId'; @@ -37,7 +36,6 @@ describe('deposit functions', () => { tokenAddress: '0x00', handlerAddress: '0x9867', }; - bridgeInstance = { deposit: jest.fn() } as unknown as Bridge; jest.clearAllMocks(); }); @@ -143,38 +141,4 @@ describe('deposit functions', () => { ).rejects.toThrowError('Deposit failed'); }); }); - - describe('erc20Transfer', () => { - it('should successfully execute', async () => { - jest.spyOn(EVM, 'executeDeposit').mockResolvedValueOnce({} as ethers.PopulatedTransaction); - bridgeInstance = { - signer: { - getAddress: jest.fn().mockResolvedValue('0xMyaddress'), - }, - } as unknown as Bridge; - - const depositData = - '0x0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000001498729c03c4d5e820f5e8c45558ae07ae63f97461'; - - const erc20Params = { - amount: BigInt('100'), - recipientAddress: '0x98729c03c4D5e820F5e8c45558ae07aE63F97461', - bridgeInstance, - domainId, - resourceId, - feeData, - depositData, - }; - await EVM.assetTransfer(erc20Params); - - expect(EVM.executeDeposit).toBeCalledWith( - domainId, - resourceId, - depositData, - feeData, - bridgeInstance, - undefined, - ); - }); - }); }); diff --git a/packages/evm/src/utils/__test__/genericTransferHelpers.test.ts b/packages/evm/src/utils/__test__/genericTransferHelpers.test.ts new file mode 100644 index 000000000..13bc1dd79 --- /dev/null +++ b/packages/evm/src/utils/__test__/genericTransferHelpers.test.ts @@ -0,0 +1,78 @@ +import { Network } from '@buildwithsygma/core'; + +import { createGenericCallDepositData } from '../genericTransferHelpers.js'; + +const CONTRACT_ABI = [ + { + inputs: [ + { + internalType: 'address', + name: '_depositer', + type: 'address', + }, + { + internalType: 'address', + name: '_index', + type: 'address', + }, + { + internalType: 'uint256', + name: '_value', + type: 'uint256', + }, + ], + name: 'store', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '_index', + type: 'address', + }, + ], + name: 'retrieve', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const; + +describe('createGenericCallDepositData', () => { + it('should create correct generic call deposit data', () => { + const depositor = '0x98729c03c4D5e820F5e8c45558ae07aE63F97461' as const; + + const genericCallParams = { + abi: CONTRACT_ABI, + functionName: 'store', + functionParams: [depositor, depositor, BigInt(42069)] as const, + contractAddress: '0x4bE595ab5A070663B314970Fc10C049BBA0ad489', + destination: { + name: 'EVM', + type: Network.EVM, + caipId: '11', + chainId: 1, + id: 1, + }, + maxFee: BigInt(3000000), + depositor: depositor, + }; + + const expectedDepositData = + '0x00000000000000000000000000000000000000000000000000000000002dc6c00004ba154fea144be595ab5a070663b314970fc10c049bba0ad4891498729c03c4d5e820f5e8c45558ae07ae63f9746100000000000000000000000098729c03c4d5e820f5e8c45558ae07ae63f97461000000000000000000000000000000000000000000000000000000000000a455'; + + const depositData = createGenericCallDepositData( + genericCallParams, + ); + expect(depositData.toLowerCase()).toEqual(expectedDepositData.toLowerCase()); + }); +}); diff --git a/packages/evm/src/utils/assetTransferHelpers.ts b/packages/evm/src/utils/assetTransferHelpers.ts new file mode 100644 index 000000000..9629e2657 --- /dev/null +++ b/packages/evm/src/utils/assetTransferHelpers.ts @@ -0,0 +1,141 @@ +import type { Domain } from '@buildwithsygma/core'; +import { Network } from '@buildwithsygma/core'; +import { AbiCoder } from '@ethersproject/abi'; +import { arrayify, concat, hexlify, hexZeroPad } from '@ethersproject/bytes'; +import { TypeRegistry } from '@polkadot/types'; +import { decodeAddress } from '@polkadot/util-crypto'; +import { BigNumber, utils } from 'ethers'; + +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 { + destination: Domain; + recipientAddress: string; + amount?: bigint; + tokenId?: string; + optionalGas?: bigint; + optionalMessage?: FungibleTransferOptionalMessage; +} + +export function serializeEvmAddress(evmAddress: `0x${string}`): Uint8Array { + return arrayify(evmAddress); +} + +export function createSubstrateMultiLocationObject( + substrateAddress: string, + parachainId?: number, +): string { + const decodedAddress = decodeAddress(substrateAddress); + const hexlifiedAddress = hexlify(decodedAddress); + const parents = parachainId ? 1 : 0; + const interior = parachainId + ? { + X2: [ + { + parachain: parachainId, + }, + { + AccountId32: { + network: { any: null }, + id: hexlifiedAddress, + }, + }, + ], + } + : { + X1: { + AccountId32: { + network: { any: null }, + id: hexlifiedAddress, + }, + }, + }; + + return JSON.stringify({ parents, interior }); +} + +export function serializeSubstrateAddress( + substrateAddress: string, + parachainId?: number, +): Uint8Array { + const multilocationObject = createSubstrateMultiLocationObject(substrateAddress, parachainId); + const registry = new TypeRegistry(); + 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) { + case Network.EVM: { + recipientAddressSerialized = serializeEvmAddress(recipientAddress as `0x${string}`); + break; + } + case Network.SUBSTRATE: { + recipientAddressSerialized = serializeSubstrateAddress( + recipientAddress, + destination.parachainId, + ); + break; + } + case Network.BITCOIN: { + recipientAddressSerialized = utils.arrayify( + `${utils.hexlify(utils.toUtf8Bytes(recipientAddress))}`, + ); + break; + } + default: { + throw new Error('Unsupported destination network type.'); + } + } + + const val = amount !== undefined ? amount : tokenId !== undefined ? tokenId : null; + if (val === null) throw new Error('Token Amount Or ID is required.'); + + const HEX_PADDING = 32; + const tokenAmountOrIdInHex = BigNumber.from(val).toHexString(); + 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 (optionalGas) { + const optionalGasInHex = BigNumber.from(optionalGas).toHexString(); + const zeroPaddedOptionalGas = hexZeroPad(optionalGasInHex, HEX_PADDING); + depositData = concat([depositData, zeroPaddedOptionalGas]); + } + + if (optionalMessage) { + 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 optionalMessageSeriailzed = arrayify(optionalMessageEncoded); + const optionalMsgLenInHex = BigNumber.from(optionalMessageSeriailzed.length).toHexString(); + const zeroPaddedOptionalMsgLen = hexZeroPad(optionalMsgLenInHex, HEX_PADDING); + + depositData = concat([depositData, zeroPaddedOptionalMsgLen, optionalMessageSeriailzed]); + } + + return hexlify(depositData); +} diff --git a/packages/evm/src/utils/depositFn.ts b/packages/evm/src/utils/depositFn.ts new file mode 100644 index 000000000..a1dd054a4 --- /dev/null +++ b/packages/evm/src/utils/depositFn.ts @@ -0,0 +1,53 @@ +import { FeeHandlerType } from '@buildwithsygma/core'; +import type { Bridge } from '@buildwithsygma/sygma-contracts'; +import type { PopulatedTransaction, 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, + gasLimit: ASSET_TRANSFER_GAS_LIMIT, + }; + + const payableOverrides = { + ...transactionSettings, + ...overrides, + }; + + const depositTransaction = await bridgeInstance.populateTransaction.deposit( + domainId, + resourceId, + depositData, + '0x', + payableOverrides, + ); + + return depositTransaction; +}; diff --git a/packages/evm/src/utils/depositFns.ts b/packages/evm/src/utils/depositFns.ts deleted file mode 100644 index 1501ab11a..000000000 --- a/packages/evm/src/utils/depositFns.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { FeeHandlerType } from '@buildwithsygma/core'; -import type { Bridge } from '@buildwithsygma/sygma-contracts'; -import type { PopulatedTransaction, ethers } from 'ethers'; -import { BigNumber } from 'ethers'; - -import type { EvmFee } from '../types.js'; - -import { createPermissionlessGenericDepositData } from './helpers.js'; - -export const ASSET_TRANSFER_GAS_LIMIT: BigNumber = BigNumber.from(300000); - -/** @internal */ -type AssetTransferParams = { - /** The unique identifier for the destination network on the bridge. */ - domainId: string; - /** The unique identifier for the resource being transferred. */ - resourceId: string; - /** The bridge instance used for the transfer. */ - bridgeInstance: Bridge; - /** The fee data associated with the ERC20 token transfer, including the gas price and gas limit. */ - feeData: EvmFee; - /** Deposit data including amount of tokens, length and recipient address */ - depositData: string; - /** Optional overrides for the transaction, such as gas price, gas limit, or value. */ - overrides?: ethers.PayableOverrides; -}; - -/** - * Perform an erc20 transfer - * - * @example - * const params = { - * amount: '100', - * recipientAddress: '0x1234567890123456789012345678901234567890', - * bridgeInstance: new Bridge(), // Bridge instance from the sygma-contracts - * domainId: '1', - * resourceId: '0x000000000000000001', - * feeData: { ... }, // fee data - * } - * const transaction = await erc20Transfer(params) - * - * @category Bridge deposit - * @param {Erc20TransferParamsType} params - The parameters for the erc20 transfer function. - * @returns {Promise} - The populated transaction. - */ -export const assetTransfer = async ({ - bridgeInstance, - domainId, - resourceId, - feeData, - depositData, - overrides, -}: AssetTransferParams): Promise => { - // pass data to smartcontract function and create a transaction - return executeDeposit(domainId, resourceId, depositData, feeData, bridgeInstance, overrides); -}; - -type GenericMessageParams = { - executeFunctionSignature: string; - executeContractAddress: string; - maxFee: string; - depositor: string; - executionData: string; - domainId: string; - resourceId: string; - bridgeInstance: Bridge; - feeData: EvmFee; - overrides?: ethers.PayableOverrides; -}; - -/** - * Create a generic cross chain message transaction - * using typechain and ethers - * @category Generic Transfer - * @param {GenericMessageParams} param0 - * @returns {Promise} - */ -export const genericMessageTransfer = async ({ - executeFunctionSignature, - executeContractAddress, - maxFee, - depositor, - executionData, - bridgeInstance, - domainId, - resourceId, - feeData, - overrides, -}: GenericMessageParams): Promise => { - const depositData = createPermissionlessGenericDepositData( - executeFunctionSignature, - executeContractAddress, - maxFee, - depositor, - executionData, - ); - return executeDeposit(domainId, resourceId, depositData, feeData, bridgeInstance, overrides); -}; - -/** - * 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 = { - // * "twap" and "basic" both deduct in native currency - value: feeData.type == FeeHandlerType.PERCENTAGE ? 0 : feeData.fee, - gasLimit: ASSET_TRANSFER_GAS_LIMIT, - }; - - const payableOverrides = { - ...transactionSettings, - ...overrides, - }; - - const depositTransaction = await bridgeInstance.populateTransaction.deposit( - domainId, - resourceId, - depositData, - '0x', - payableOverrides, - ); - - return depositTransaction; -}; diff --git a/packages/evm/src/utils/genericTransferHelpers.ts b/packages/evm/src/utils/genericTransferHelpers.ts new file mode 100644 index 000000000..654c3d285 --- /dev/null +++ b/packages/evm/src/utils/genericTransferHelpers.ts @@ -0,0 +1,67 @@ +import type { Domain } from '@buildwithsygma/core'; +import { Network } from '@buildwithsygma/core'; +import { hexZeroPad } from '@ethersproject/bytes'; +import type { + Abi, + AbiParametersToPrimitiveTypes, + ExtractAbiFunction, + ExtractAbiFunctionNames, +} from 'abitype'; +import { BigNumber, ethers } from 'ethers'; + +interface GenericDepositParams< + ContractAbi extends Abi, + FunctionName extends ExtractAbiFunctionNames, +> { + abi: Abi; + functionName: string; + functionParams: AbiParametersToPrimitiveTypes< + ExtractAbiFunction['inputs'], + 'inputs' + >; + contractAddress: string; + destination: Domain; + maxFee: bigint; + depositor: `0x${string}`; +} + +const getZeroPaddedLength = (hexString: string, padding: number): string => + hexZeroPad(BigNumber.from(hexString.substring(2).length / 2).toHexString(), padding).substring(2); + +export function createGenericCallDepositData< + ContractAbi extends Abi, + FunctionName extends ExtractAbiFunctionNames, +>(genericTransferParams: GenericDepositParams): string { + const { abi, functionName, functionParams, contractAddress, maxFee, destination, depositor } = + genericTransferParams; + + if (destination.type === Network.EVM) { + const contractInterface = new ethers.utils.Interface(JSON.stringify(abi)); + + const paddedMaxFee = hexZeroPad(BigNumber.from(maxFee).toHexString(), 32); + const funcData = contractInterface.encodeFunctionData( + functionName, + functionParams as unknown as Array, + ); + const funcSig = funcData.substring(0, 10); + /** 0x (2) + function signature (8) + first param which is always set to depositer by relayer (64) */ + const funcParamEncoded = funcData.substring(74); + + const funcSigLen = getZeroPaddedLength(funcSig, 2); + const contractAddrLen = getZeroPaddedLength(contractAddress, 1); + const dataDepositorLen = getZeroPaddedLength(depositor, 1); + + return ( + paddedMaxFee + + funcSigLen + + funcSig.substring(2) + + contractAddrLen + + contractAddress.substring(2) + + dataDepositorLen + + depositor.substring(2) + + funcParamEncoded + ); + } + + throw new Error('Unsupported destination network type.'); +} diff --git a/packages/evm/src/utils/helpers.ts b/packages/evm/src/utils/helpers.ts deleted file mode 100644 index 5e0830552..000000000 --- a/packages/evm/src/utils/helpers.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { TypeRegistry } from '@polkadot/types/create'; -import { decodeAddress } from '@polkadot/util-crypto'; -import { utils, BigNumber } from 'ethers'; -/** - * Constructs the deposit data for a erc20 transaction. - * - * @example - * // Amount of tokens: - * const amount = '1'; - * // EVM address - * const evmAddress = '0x1234567890123456789012345678901234567890'; - * createERCDepositData(amount, evmAddress); - * - * - * @param {bigint} tokenAmount - The amount of tokens to be transferred. - * @param {string} recipientAddress - The address of the recipient. - * @param {number} parachainId - Optional parachain id if the substrate destination targets another parachain. - * @returns {string} The deposit data as hex string - */ -export const createERCDepositData = ( - tokenAmount: bigint, - recipientAddress: string, - parachainId?: number, -): string => { - let recipientAddressInBytes; - if (utils.isAddress(recipientAddress)) { - recipientAddressInBytes = getEVMRecipientAddressInBytes(recipientAddress); - } else if (parachainId) { - recipientAddressInBytes = getSubstrateRecipientAddressInBytes(recipientAddress, parachainId); - } else { - const hexAddress = addressToHex(recipientAddress); - recipientAddressInBytes = utils.arrayify(`${hexAddress}`); - } - - const depositDataBytes = constructMainDepositData( - BigNumber.from(tokenAmount), - recipientAddressInBytes, - ); - const depositData = utils.hexlify(depositDataBytes); - return depositData; -}; -/** - * Constructs the main deposit data for a given token and recipient. - * - * @category Helpers - * @param {BigNumber} tokenStats - The amount of ERC20 tokens or the token ID of ERC721 tokens. - * @param {Uint8Array} destRecipient - The recipient address in bytes array - * @returns {Uint8Array} The main deposit data in bytes array - */ -const constructMainDepositData = (tokenStats: BigNumber, destRecipient: Uint8Array): Uint8Array => { - const data: Uint8Array = utils.concat([ - utils.hexZeroPad(tokenStats.toHexString(), 32), // Amount (ERC20) or Token Id (ERC721) - utils.hexZeroPad(BigNumber.from(destRecipient.length).toHexString(), 32), // length of recipient - destRecipient, // Recipient - ]); - return data; -}; -/** - * Converts a Substrate recipient address to a JSON multilocation. - * - * @param {string} recipientAddress - The recipient address as a string. - * @returns {string} The recipient address as a stringified Substrate Multilocation Object - */ -export const constructSubstrateRecipient = ( - recipientAddress: string, - parachainId?: number, -): string => { - const addressPublicKeyBytes = decodeAddress(recipientAddress); - const addressPublicKeyHexString = utils.hexlify(addressPublicKeyBytes); - if (parachainId) { - return JSON.stringify({ - parents: 1, - interior: { - X2: [ - { - parachain: parachainId, - }, - { - AccountId32: { - network: { any: null }, - id: addressPublicKeyHexString, - }, - }, - ], - }, - }); - } - - return JSON.stringify({ - parents: 0, - interior: { - X1: { - AccountId32: { - network: { any: null }, - id: addressPublicKeyHexString, - }, - }, - }, - }); -}; -/** - * Converts a EVM recipient address to a Uint8Array of bytes. - * - * @param {string} recipientAddress - The recipient address, as a string. - * @returns {Uint8Array} The recipient address as a Uint8Array of bytes - */ -export const getEVMRecipientAddressInBytes = (recipientAddress: string): Uint8Array => { - return utils.arrayify(recipientAddress); -}; -/** - * Converts a Substrate recipient multilocation to a Uint8Array of bytes. - * - * @param {string} recipientAddress - The recipient address, as a string - * @returns {Uint8Array} The recipient address as a Uint8Array of bytes - */ -export const getSubstrateRecipientAddressInBytes = ( - recipientAddress: string, - parachainId?: number, -): Uint8Array => { - const registry = new TypeRegistry(); - const result = registry - .createType( - 'MultiLocation', - JSON.parse(constructSubstrateRecipient(recipientAddress, parachainId)), - ) - .toU8a(); - - return result; -}; -/** - * Return hex data padded to the number defined as padding - * based on ethers.utils.hexZeroPad - * - * @category Helpers - * @param covertThis - data to convert - * @param padding - number to padd the data - * @returns {string} - */ -export const toHex = ( - covertThis: string | number | BigNumber | bigint, - padding: number, -): string => { - const amount = covertThis instanceof BigNumber ? covertThis : BigNumber.from(covertThis); - return utils.hexZeroPad(utils.hexlify(amount), padding); -}; -/** - * JS types to 0x Hex string - * required to initiate contract - * calls on destination EVM chain - * @param {string | BigNumber | number | boolean | bigint} param - * @returns {`0x{string}`} - */ -function createGenericCallParameter(param: string | BigNumber | number | boolean | bigint): string { - const DEFAULT_PADDING = 32; - switch (typeof param) { - case 'boolean': - return toHex(Number(param), DEFAULT_PADDING).substring(2); - case 'bigint': - case 'number': - case 'string': - return toHex(param, DEFAULT_PADDING).substring(2); - case 'object': - if (param instanceof BigNumber) { - return toHex(param, DEFAULT_PADDING).substring(2); - } - throw new Error('Unsupported parameter type.'); - case 'symbol': - case 'undefined': - case 'function': - throw new Error('Unsupported parameter type.'); - } -} -/** - * Convert JS primitive types to hex encoded - * strings for EVM function calls - * @param {Array} params - * @returns {string} - */ -export function serializeGenericCallParameters( - params: Array, -): string { - /** - * .slice(1) is used because first parameter will always be an - * address by default, and this parameter is not specified by - * the user, relayers add it so this param is discarded by SDK - * However, this param should still be part of ABI otherwise - * messages won't be passed correctly - */ - const serialized = params - .slice(1) - .map(item => createGenericCallParameter(item)) - .join(''); - return `0x${serialized}`; -} - -/** - * Return the address transformed to hex for bitcoin deposits - * - * @category Helpers - * @param address - bitcoin address - * @returns {string} - */ -export const addressToHex = (address: string): string => { - return utils.hexlify(utils.toUtf8Bytes(address)); -}; - -/** - * Creates the data for permissionless generic handler - * - * @category Helpers - * @param executeFunctionSignature - execution function signature - * @param executeContractAddress - execution contract address - * @param maxFee - max fee defined - * @param depositor - address of depositor on source chain - * @param executionData - the data to pass as parameter of the function being called on destination chain - * @returns {string} - */ -export const createPermissionlessGenericDepositData = ( - executeFunctionSignature: string, - executeContractAddress: string, - maxFee: string, - depositor: string, - executionData: string, -): string => { - return ( - '0x' + - toHex(maxFee, 32).substring(2) + // uint256 - toHex(executeFunctionSignature.substring(2).length / 2, 2).substring(2) + // uint16 - executeFunctionSignature.substring(2) + // bytes - toHex(executeContractAddress.substring(2).length / 2, 1).substring(2) + // uint8 - executeContractAddress.substring(2) + // bytes - toHex(depositor.substring(2).length / 2, 1).substring(2) + // uint8 - depositor.substring(2) + - executionData.substring(2) - ) // bytes - .toLowerCase(); -}; diff --git a/packages/evm/src/utils/index.ts b/packages/evm/src/utils/index.ts index 92d64e49a..baf460d6d 100644 --- a/packages/evm/src/utils/index.ts +++ b/packages/evm/src/utils/index.ts @@ -1,5 +1,4 @@ export * from './approveAndCheckFns.js'; export * from './balances.js'; -export * from './depositFns.js'; +export * from './depositFn.js'; export * from './transaction.js'; -export * from './helpers.js';