diff --git a/examples/evm-to-substrate-fungible-transfer/src/transfer.ts b/examples/evm-to-substrate-fungible-transfer/src/transfer.ts index f1e8fdb97..76ac3e919 100644 --- a/examples/evm-to-substrate-fungible-transfer/src/transfer.ts +++ b/examples/evm-to-substrate-fungible-transfer/src/transfer.ts @@ -2,10 +2,9 @@ import { EVMAssetTransfer, Environment } from "@buildwithsygma/sygma-sdk-core"; import { Wallet, providers } from "ethers"; import dotenv from "dotenv"; -dotenv.config() +dotenv.config(); const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { throw new Error("Missing environment variable: PRIVATE_KEY"); } @@ -19,20 +18,17 @@ export async function erc20Transfer(): Promise { const provider = new providers.JsonRpcProvider( "https://rpc.goerli.eth.gateway.fm/" ); - const wallet = new Wallet( - privateKey as string, - provider - ); + const wallet = new Wallet(privateKey, provider); const assetTransfer = new EVMAssetTransfer(); await assetTransfer.init(provider, Environment.TESTNET); - const transfer = assetTransfer.createFungibleTransfer( await wallet.getAddress(), ROCOCO_PHALA_CHAIN_ID, DESTINATION_ADDRESS, RESOURCE_ID, "5000000000000000000" // 18 decimal places + // optional parachainID (e.g. KusamaParachain.SHIDEN) ); const fee = await assetTransfer.getFee(transfer); @@ -53,4 +49,4 @@ export async function erc20Transfer(): Promise { console.log("Sent transfer with hash: ", response.hash); } -erc20Transfer().finally(() => { }); +erc20Transfer().finally(() => {}); diff --git a/packages/sdk/src/chains/BaseAssetTransfer.ts b/packages/sdk/src/chains/BaseAssetTransfer.ts index 41bc53632..4a4f32f86 100644 --- a/packages/sdk/src/chains/BaseAssetTransfer.ts +++ b/packages/sdk/src/chains/BaseAssetTransfer.ts @@ -1,5 +1,6 @@ import { Environment, Fungible, Transfer } from '../types'; import { Config } from '../config'; +import { ParachainID } from './Substrate'; export abstract class BaseAssetTransfer { public config!: Config; @@ -15,6 +16,7 @@ export abstract class BaseAssetTransfer { * @param {string} destinationAddress - The address of the recipient on the destination chain * @param {string} resourceId - The ID of the resource being transferred * @param {string} amount - The amount of tokens to be transferred. The amount should be in the lowest denomination possible on the source chain. If the token on source chain is configured to use 12 decimals and the amount to be transferred is 1 ETH, then amount should be passed in as 1000000000000 + * @param {string} parachainId - Optional parachain id if the substrate destination parachain differs from the target domain. * @returns {Transfer} - The populated transfer object * @throws {Error} - Source domain not supported, Destination domain not supported, Resource not supported */ @@ -24,6 +26,7 @@ export abstract class BaseAssetTransfer { destinationAddress: string, resourceId: string, amount: string, + parachainId?: ParachainID, ): Transfer { const { sourceDomain, destinationDomain, resource } = this.config.getBaseTransferParams( destinationChainId, @@ -35,6 +38,7 @@ export abstract class BaseAssetTransfer { details: { amount, recipient: destinationAddress, + parachainId: parachainId, }, from: sourceDomain, to: destinationDomain, diff --git a/packages/sdk/src/chains/EVM/__test__/helpers.test.ts b/packages/sdk/src/chains/EVM/__test__/helpers.test.ts index 748fc278a..e046879ad 100644 --- a/packages/sdk/src/chains/EVM/__test__/helpers.test.ts +++ b/packages/sdk/src/chains/EVM/__test__/helpers.test.ts @@ -1,7 +1,8 @@ import { BigNumber, utils } from 'ethers'; import { - getRecipientAddressInBytes, + getEVMRecipientAddressInBytes, + getSubstrateRecipientAddressInBytes, createERCDepositData, toHex, createPermissionedGenericDepositData, @@ -23,6 +24,14 @@ describe('createERCDepositData', () => { }); describe('constructSubstrateRecipient', () => { + it('should create a valid Substrate Multilocation Object with parachain id', () => { + const substrateAddress = '5CDQJk6kxvBcjauhrogUc9B8vhbdXhRscp1tGEUmniryF1Vt'; + const result = constructSubstrateRecipient(substrateAddress, 2004); + const expectedResult = + '{"parents":1,"interior":{"X2":[{"parachain":2004},{"AccountId32":{"network":{"any":null},"id":"0x06a220edf5f82b84fc5f9270f8a30a17636bf29c05a5c16279405ca20918aa39"}}]}}'; + expect(result).toEqual(expectedResult); + }); + it('should create a valid Substrate Multilocation Object', () => { const substrateAddress = '5CDQJk6kxvBcjauhrogUc9B8vhbdXhRscp1tGEUmniryF1Vt'; const result = constructSubstrateRecipient(substrateAddress); @@ -32,24 +41,26 @@ describe('constructSubstrateRecipient', () => { }); }); -describe('getRecipientAddressInBytes', () => { +describe('getEVMRecipientAddressInBytes', () => { it('should convert an EVM address to a Uint8Array of bytes', () => { const evmAddress = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'; expect(utils.isAddress(evmAddress)).toBeTruthy(); - const result = getRecipientAddressInBytes(evmAddress); + const result = getEVMRecipientAddressInBytes(evmAddress); const expectedResult = utils.arrayify(evmAddress); expect(result).toEqual(expectedResult); expect(result).toBeInstanceOf(Uint8Array); }); +}); +describe('getSubstrateRecipientAddressInBytes', () => { it('should convert a Substrate address to a Uint8Array of bytes', () => { const substrateAddress = '5CDQJk6kxvBcjauhrogUc9B8vhbdXhRscp1tGEUmniryF1Vt'; expect(utils.isAddress(substrateAddress)).toBeFalsy(); - const result = getRecipientAddressInBytes(substrateAddress); + const result = getSubstrateRecipientAddressInBytes(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, @@ -58,6 +69,21 @@ describe('getRecipientAddressInBytes', () => { expect(result).toEqual(expectedResult); expect(result).toBeInstanceOf(Uint8Array); }); + + it('should convert a Substrate address on a different parachain to a Uint8Array of bytes', () => { + const substrateAddress = '5CDQJk6kxvBcjauhrogUc9B8vhbdXhRscp1tGEUmniryF1Vt'; + + expect(utils.isAddress(substrateAddress)).toBeFalsy(); + + const result = getSubstrateRecipientAddressInBytes(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, + ]); + + expect(result).toEqual(expectedResult); + expect(result).toBeInstanceOf(Uint8Array); + }); }); describe('toHex', () => { diff --git a/packages/sdk/src/chains/EVM/assetTransfer.ts b/packages/sdk/src/chains/EVM/assetTransfer.ts index ebbf1ed48..2ae105e91 100644 --- a/packages/sdk/src/chains/EVM/assetTransfer.ts +++ b/packages/sdk/src/chains/EVM/assetTransfer.ts @@ -202,9 +202,11 @@ export class EVMAssetTransfer extends BaseAssetTransfer { const bridge = Bridge__factory.connect(this.config.getDomainConfig().bridge, this.provider); switch (transfer.resource.type) { case ResourceType.FUNGIBLE: { + const fungibleTransfer = transfer as Transfer; return await erc20Transfer({ - amount: (transfer.details as Fungible).amount, - recipientAddress: (transfer.details as Fungible).recipient, + amount: fungibleTransfer.details.amount, + recipientAddress: fungibleTransfer.details.recipient, + parachainId: fungibleTransfer.details.parachainId, bridgeInstance: bridge, domainId: transfer.to.id.toString(), resourceId: transfer.resource.resourceId, @@ -212,9 +214,11 @@ export class EVMAssetTransfer extends BaseAssetTransfer { }); } case ResourceType.NON_FUNGIBLE: { + const nonfungibleTransfer = transfer as Transfer; return await erc721Transfer({ - id: (transfer.details as NonFungible).tokenId, - recipientAddress: (transfer.details as NonFungible).recipient, + id: nonfungibleTransfer.details.tokenId, + recipientAddress: nonfungibleTransfer.details.recipient, + parachainId: nonfungibleTransfer.details.parachainId, bridgeInstance: bridge, domainId: transfer.to.id.toString(), resourceId: transfer.resource.resourceId, diff --git a/packages/sdk/src/chains/EVM/helpers.ts b/packages/sdk/src/chains/EVM/helpers.ts index 5e3309e59..e3db6ce4b 100644 --- a/packages/sdk/src/chains/EVM/helpers.ts +++ b/packages/sdk/src/chains/EVM/helpers.ts @@ -17,10 +17,20 @@ const registry = new TypeRegistry(); * * @param {string} 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: string, recipientAddress: string): string => { - const recipientAddressInBytes = getRecipientAddressInBytes(recipientAddress); +export const createERCDepositData = ( + tokenAmount: string, + recipientAddress: string, + parachainId?: number, +): string => { + let recipientAddressInBytes; + if (utils.isAddress(recipientAddress)) { + recipientAddressInBytes = getEVMRecipientAddressInBytes(recipientAddress); + } else { + recipientAddressInBytes = getSubstrateRecipientAddressInBytes(recipientAddress, parachainId); + } const depositDataBytes = constructMainDepositData( BigNumber.from(tokenAmount), recipientAddressInBytes, @@ -97,10 +107,32 @@ export const createPermissionlessGenericDepositData = ( * @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): string => { +export const constructSubstrateRecipient = ( + recipientAddress: string, + parachainId?: number, +): string => { const addressPublicKeyBytes = decodeAddress(recipientAddress); const addressPublicKeyHexString = utils.hexlify(addressPublicKeyBytes); - const substrateMultilocation = JSON.stringify({ + if (parachainId) { + return JSON.stringify({ + parents: 1, + interior: { + X2: [ + { + parachain: parachainId, + }, + { + AccountId32: { + network: { any: null }, + id: addressPublicKeyHexString, + }, + }, + ], + }, + }); + } + + return JSON.stringify({ parents: 0, interior: { X1: { @@ -111,24 +143,33 @@ export const constructSubstrateRecipient = (recipientAddress: string): string => }, }, }); - - return substrateMultilocation; }; /** - * Converts a recipient address to a Uint8Array of bytes. + * Converts a EVM recipient address to a Uint8Array of bytes. * - * @param {string} recipientAddress - The recipient address, as a string. If the address passed in is not an Ethereum address, a Substrate Multilocation object will be constructed and serialized. + * @param {string} recipientAddress - The recipient address, as a string. * @returns {Uint8Array} The recipient address as a Uint8Array of bytes */ -export const getRecipientAddressInBytes = (recipientAddress: string): Uint8Array => { - if (utils.isAddress(recipientAddress)) { - // EVM address - return utils.arrayify(recipientAddress); - } +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 result = registry - .createType('MultiLocation', JSON.parse(constructSubstrateRecipient(recipientAddress))) + .createType( + 'MultiLocation', + JSON.parse(constructSubstrateRecipient(recipientAddress, parachainId)), + ) .toU8a(); return result; diff --git a/packages/sdk/src/chains/EVM/types/index.ts b/packages/sdk/src/chains/EVM/types/index.ts index 66d061ace..94655f26d 100644 --- a/packages/sdk/src/chains/EVM/types/index.ts +++ b/packages/sdk/src/chains/EVM/types/index.ts @@ -27,6 +27,8 @@ export type OracleResource = { export type Erc20TransferParamsType = { /** The unique identifier for the destination network on the bridge. */ domainId: string; + /** Identifier of the substrate destination parachain */ + parachainId?: number; /** The unique identifier for the resource being transferred. */ resourceId: string; /** The amount of tokens to transfer */ @@ -44,6 +46,8 @@ export type Erc20TransferParamsType = { export type Erc721TransferParamsType = { /** The unique identifier for the destination network on the bridge. */ domainId: string; + /** Identifier of the substrate destination parachain */ + parachainId?: number; /** The unique identifier for the resource being transferred. */ resourceId: string; /** The tokenId for a specific ERC721 token being transferred. */ diff --git a/packages/sdk/src/chains/EVM/utils/depositFns.ts b/packages/sdk/src/chains/EVM/utils/depositFns.ts index 8e68973a9..be8d2a692 100644 --- a/packages/sdk/src/chains/EVM/utils/depositFns.ts +++ b/packages/sdk/src/chains/EVM/utils/depositFns.ts @@ -29,6 +29,7 @@ export const ASSET_TRANSFER_GAS_LIMIT: BigNumber = BigNumber.from(300000); export const erc20Transfer = async ({ amount, recipientAddress, + parachainId, bridgeInstance, domainId, resourceId, @@ -36,7 +37,7 @@ export const erc20Transfer = async ({ overrides, }: Erc20TransferParamsType): Promise => { // construct the deposit data - const depositData = createERCDepositData(amount, recipientAddress); + const depositData = createERCDepositData(amount, recipientAddress, parachainId); // pass data to smartcontract function and create a transaction return executeDeposit(domainId, resourceId, depositData, feeData, bridgeInstance, overrides); @@ -63,6 +64,7 @@ export const erc20Transfer = async ({ export const erc721Transfer = async ({ id: tokenId, recipientAddress, + parachainId, bridgeInstance, domainId, resourceId, @@ -70,7 +72,7 @@ export const erc721Transfer = async ({ overrides, }: Erc721TransferParamsType): Promise => { // construct the deposit data - const depositData = createERCDepositData(tokenId, recipientAddress); + const depositData = createERCDepositData(tokenId, recipientAddress, parachainId); // pass data to smartcontract function and create a transaction return executeDeposit(domainId, resourceId, depositData, feeData, bridgeInstance, overrides); diff --git a/packages/sdk/src/chains/Substrate/types/index.ts b/packages/sdk/src/chains/Substrate/types/index.ts index 4f329a6bd..9b08e0cd8 100644 --- a/packages/sdk/src/chains/Substrate/types/index.ts +++ b/packages/sdk/src/chains/Substrate/types/index.ts @@ -19,3 +19,30 @@ export type SubstrateFee = { }; export type SubstrateAccountInfo = AccountInfo; + +export enum KusamaParachain { + STATEMINE = 1000, + BIFROST = 2001, + SHIDEN = 2007, + MOONRIVER = 2023, + KARURA = 2000, + PARALLEL_HEIKO = 2085, + BASILISK = 2090, + CRAB = 2105, + CALAMARI = 2084, + TURING = 2114, +} + +export enum PolkadotParachain { + STATEMINT = 1000, + ASTAR = 2006, + MOONBEAM = 2004, + BIFROST = 2030, + PARALLEL = 2012, + HYDRADX = 2034, + DARWINIA = 2046, + EQUILIBRIUM = 2011, + COMPOSABLE = 2019, +} + +export type ParachainID = KusamaParachain | PolkadotParachain | number; diff --git a/packages/sdk/src/types/types.ts b/packages/sdk/src/types/types.ts index 0db331e99..003e4498f 100644 --- a/packages/sdk/src/types/types.ts +++ b/packages/sdk/src/types/types.ts @@ -1,4 +1,4 @@ -import { XcmMultiAssetIdType } from 'chains/Substrate/types'; +import { ParachainID, XcmMultiAssetIdType } from 'chains/Substrate/types'; export type Domain = { id: number; @@ -6,6 +6,11 @@ export type Domain = { name: string; }; +export type Recipient = { + address: string; + parachainId?: number; +}; + export enum ResourceType { FUNGIBLE = 'fungible', NON_FUNGIBLE = 'nonfungible', @@ -44,6 +49,7 @@ export enum FeeHandlerType { type AssetTransfer = { recipient: string; + parachainId?: ParachainID; }; export type Fungible = AssetTransfer & { diff --git a/packages/sdk/test/chains/EVM/assetTransfer.test.ts b/packages/sdk/test/chains/EVM/assetTransfer.test.ts index b7e74cdae..e436bd9f7 100644 --- a/packages/sdk/test/chains/EVM/assetTransfer.test.ts +++ b/packages/sdk/test/chains/EVM/assetTransfer.test.ts @@ -21,33 +21,33 @@ const resourceHandlerFunction = jest.fn(); jest.mock( '@buildwithsygma/sygma-contracts', () => - ({ - ...jest.requireActual('@buildwithsygma/sygma-contracts'), - FeeHandlerRouter__factory: { - connect: () => { - return { - _domainResourceIDToFeeHandlerAddress: feeHandlerAddressFunction, - }; + ({ + ...jest.requireActual('@buildwithsygma/sygma-contracts'), + FeeHandlerRouter__factory: { + connect: () => { + return { + _domainResourceIDToFeeHandlerAddress: feeHandlerAddressFunction, + }; + }, }, - }, - ERC20__factory: { - connect: () => { - return {}; + ERC20__factory: { + connect: () => { + return {}; + }, }, - }, - ERC721MinterBurnerPauser__factory: { - connect: () => { - return {}; + ERC721MinterBurnerPauser__factory: { + connect: () => { + return {}; + }, }, - }, - Bridge__factory: { - connect: () => { - return { - _resourceIDToHandlerAddress: resourceHandlerFunction, - }; + Bridge__factory: { + connect: () => { + return { + _resourceIDToHandlerAddress: resourceHandlerFunction, + }; + }, }, - }, - } as unknown), + } as unknown), ); const axiosMock = new MockAdapter(axios); const mockProvider: Partial = { diff --git a/packages/sdk/test/chains/Substrate/assetTransfer.test.ts b/packages/sdk/test/chains/Substrate/assetTransfer.test.ts index bcc2072d0..cbdaaef17 100644 --- a/packages/sdk/test/chains/Substrate/assetTransfer.test.ts +++ b/packages/sdk/test/chains/Substrate/assetTransfer.test.ts @@ -73,6 +73,7 @@ describe('Substrate asset transfer', () => { details: { recipient: '0x557abEc0cb31Aa925577441d54C090987c2ED818', amount: '200', + parachainId: 1001, }, }; @@ -82,6 +83,7 @@ describe('Substrate asset transfer', () => { '0x557abEc0cb31Aa925577441d54C090987c2ED818', '0x0000000000000000000000000000000000000000000000000000000000001000', '200', + 1001, ); expect(actualVal).toStrictEqual(expectedVal);