From 8292ac425b909b740bb43c9865bae95cd0682227 Mon Sep 17 00:00:00 2001 From: Sergej Sakac Date: Tue, 22 Aug 2023 11:22:19 +0200 Subject: [PATCH 1/2] Reserve transfer: Fee payment with relay chain native token --- src/utils/transactionRouter/index.ts | 14 ++-- .../transactionRouter/reserveTransfer.ts | 80 ++++++++++++------- src/utils/transactionRouter/types.ts | 11 +++ 3 files changed, 72 insertions(+), 33 deletions(-) diff --git a/src/utils/transactionRouter/index.ts b/src/utils/transactionRouter/index.ts index bf79e27..655e46a 100644 --- a/src/utils/transactionRouter/index.ts +++ b/src/utils/transactionRouter/index.ts @@ -2,7 +2,7 @@ import { ApiPromise, WsProvider } from "@polkadot/api"; import ReserveTransfer from "./reserveTransfer"; import TransferAsset from "./transferAsset"; -import { Fungible, Receiver, Sender } from "./types"; +import { FeePayment, Fungible, Receiver, Sender } from "./types"; import IdentityContract from "../../../types/contracts/identity"; // Responsible for handling all the transfer logic. @@ -32,7 +32,8 @@ class TransactionRouter { sender: Sender, receiver: Receiver, reserveChainId: number, - asset: Fungible + asset: Fungible, + feePayment: FeePayment = FeePayment.Asset, ): Promise { if (sender.network === receiver.network && sender.keypair.addressRaw === receiver.addressRaw) { throw new Error("Cannot send tokens to yourself"); @@ -63,7 +64,8 @@ class TransactionRouter { destApi, sender.keypair, receiver, - asset + asset, + feePayment ); } else if (receiver.network == reserveChainId) { // The destination chain is the reserve chain of the asset: @@ -72,7 +74,8 @@ class TransactionRouter { destApi, sender.keypair, receiver, - asset + asset, + feePayment ); } else { // The most complex case, the reserve chain is neither the sender or the destination chain. @@ -86,7 +89,8 @@ class TransactionRouter { reserveChain, sender.keypair, receiver, - asset + asset, + feePayment ); } } diff --git a/src/utils/transactionRouter/reserveTransfer.ts b/src/utils/transactionRouter/reserveTransfer.ts index e17fbb5..f84f0e0 100644 --- a/src/utils/transactionRouter/reserveTransfer.ts +++ b/src/utils/transactionRouter/reserveTransfer.ts @@ -1,7 +1,7 @@ import { ApiPromise } from "@polkadot/api"; import { KeyringPair } from "@polkadot/keyring/types"; -import { Fungible, Receiver } from "./types"; +import { FeePayment, Fungible, Receiver } from "./types"; import { getParaId } from ".."; import { AccountType } from "../../../types/types-arguments/identity"; @@ -14,7 +14,8 @@ class ReserveTransfer { destinationApi: ApiPromise, sender: KeyringPair, receiver: Receiver, - asset: Fungible + asset: Fungible, + feePayment: FeePayment ): Promise { this.ensureContainsXcmPallet(originApi); this.ensureContainsXcmPallet(destinationApi); @@ -61,7 +62,8 @@ class ReserveTransfer { destinationApi: ApiPromise, sender: KeyringPair, receiver: Receiver, - asset: Fungible + asset: Fungible, + feePayment: FeePayment ): Promise { this.ensureContainsXcmPallet(originApi); this.ensureContainsXcmPallet(destinationApi); @@ -70,7 +72,7 @@ class ReserveTransfer { // eslint-disable-next-line no-prototype-builtins const isOriginPara = originApi.query.hasOwnProperty("parachainInfo"); - const xcmProgram = this.getSendToReserveChainInstructions(asset, destinationParaId, receiver, isOriginPara); + const xcmProgram = this.getSendToReserveChainInstructions(asset, destinationParaId, receiver, isOriginPara, feePayment); const xcmPallet = originApi.tx.xcmPallet || originApi.tx.polkadotXcm; @@ -99,7 +101,8 @@ class ReserveTransfer { reserveChainApi: ApiPromise, sender: KeyringPair, receiver: Receiver, - asset: Fungible + asset: Fungible, + feePayment: FeePayment ): Promise { this.ensureContainsXcmPallet(originApi); this.ensureContainsXcmPallet(destinationApi); @@ -110,7 +113,14 @@ class ReserveTransfer { // eslint-disable-next-line no-prototype-builtins const isOriginPara = originApi.query.hasOwnProperty("parachainInfo"); - const xcmProgram = this.getTwoHopTransferInstructions(asset, reserveParaId, destinationParaId, receiver, isOriginPara); + const xcmProgram = this.getTwoHopTransferInstructions( + asset, + reserveParaId, + destinationParaId, + receiver, + isOriginPara, + feePayment + ); const xcmPallet = originApi.tx.xcmPallet || originApi.tx.polkadotXcm; @@ -147,19 +157,12 @@ class ReserveTransfer { reserveParaId: number, destParaId: number, beneficiary: Receiver, - isOriginPara: boolean + isOriginPara: boolean, + feePayment: FeePayment ): any { const reserve = this.getReserve(reserveParaId, isOriginPara); - // NOTE: we use parse and stringify to make a hard copy of the asset. - const assetFromReservePerspective = JSON.parse(JSON.stringify(asset.multiAsset)); - if (reserveParaId > 0) { - // The location of the asset will always start with the parachain if the reserve is a parachain. - this.assetFromReservePerspective(assetFromReservePerspective); - } else { - // The reserve is the relay chain. - assetFromReservePerspective.parents = 0; - } + const feeAsset = this.getFeePaymentAsset(feePayment, asset, isOriginPara, reserveParaId); return { V2: [ @@ -172,7 +175,7 @@ class ReserveTransfer { reserve, xcm: [ // TODO: the hardcoded number isn't really accurate to what we actually need. - this.buyExecution(assetFromReservePerspective, 450000000000), + this.buyExecution(feeAsset, 4500000000000), this.depositReserveAsset({ Wild: "All" }, 1, { parents: 1, interior: { @@ -204,19 +207,12 @@ class ReserveTransfer { asset: Fungible, destParaId: number, beneficiary: Receiver, - isOriginPara: boolean + isOriginPara: boolean, + feePayment: FeePayment ): any { const reserve = this.getReserve(destParaId, isOriginPara); - // NOTE: we use parse and stringify to make a hard copy of the asset. - const assetFromReservePerspective = JSON.parse(JSON.stringify(asset.multiAsset)); - if (destParaId >= 0) { - // The location of the asset will always start with the parachain if the reserve is a parachain. - this.assetFromReservePerspective(assetFromReservePerspective); - } else { - // The reserve is the relay chain. - assetFromReservePerspective.parents = 0; - } + const feeAsset = this.getFeePaymentAsset(feePayment, asset, isOriginPara, destParaId); return { V2: [ @@ -229,7 +225,7 @@ class ReserveTransfer { reserve, xcm: [ // TODO: the hardcoded number isn't really accurate to what we actually need. - this.buyExecution(assetFromReservePerspective, 450000000000), + this.buyExecution(feeAsset, 450000000000), this.depositAsset({ Wild: "All" }, 1, beneficiary) ] } @@ -428,6 +424,34 @@ class ReserveTransfer { location.parents = 0; } + private static getFeePaymentAsset(feePayment: FeePayment, asset: Fungible, isOriginPara: boolean, reserveParaId: number): any { + if (feePayment == FeePayment.RelayChainNative) { + if (isOriginPara) { + return { + parents: 1, + interior: "Here" + }; + } else { + return { + parents: 0, + interior: "Here" + } + } + } else if (feePayment == FeePayment.Asset) { + // NOTE: we use parse and stringify to make a hard copy of the asset. + const assetFromReservePerspective = JSON.parse(JSON.stringify(asset.multiAsset)); + if (reserveParaId > 0) { + // The location of the asset will always start with the parachain if the reserve is a parachain. + this.assetFromReservePerspective(assetFromReservePerspective); + } else { + // The reserve is the relay chain. + assetFromReservePerspective.parents = 0; + } + + return assetFromReservePerspective; + } + } + private static extractJunctions(location: any): any { const keyPattern = /^X\d$/; diff --git a/src/utils/transactionRouter/types.ts b/src/utils/transactionRouter/types.ts index 0cb3604..f4827bc 100644 --- a/src/utils/transactionRouter/types.ts +++ b/src/utils/transactionRouter/types.ts @@ -20,3 +20,14 @@ export type Fungible = { multiAsset: any, amount: number } + +export enum FeePayment { + // Pay with the asset you are transfering. + // + // NOTE: the asset has to be sufficient to be able to pay for fees. + Asset, + // Pay with the relay chain token. + // + // The relay chain native token is usually allowed for fee payment on parachains. + RelayChainNative, +} From a461145b4c7c27dc5fd12c2ca91321e371b777ed Mon Sep 17 00:00:00 2001 From: Sergej Sakac Date: Tue, 22 Aug 2023 13:50:12 +0200 Subject: [PATCH 2/2] add tests --- __tests__/crossChainRouter.test.ts | 691 +++++++++++------- .../transactionRouter/reserveTransfer.ts | 56 +- 2 files changed, 470 insertions(+), 277 deletions(-) diff --git a/__tests__/crossChainRouter.test.ts b/__tests__/crossChainRouter.test.ts index 6e3e541..6a9da57 100644 --- a/__tests__/crossChainRouter.test.ts +++ b/__tests__/crossChainRouter.test.ts @@ -2,8 +2,8 @@ import { ApiPromise, Keyring, WsProvider } from "@polkadot/api"; import { KeyringPair } from "@polkadot/keyring/types"; import { u8aToHex } from '@polkadot/util'; -import TransactionRouter from "@/utils/transactionRouter"; -import { Fungible, Receiver, Sender } from "@/utils/transactionRouter/types"; +import TransactionRouter from "../src/utils/transactionRouter"; +import { FeePayment, Fungible, Receiver, Sender } from "../src/utils/transactionRouter/types"; import IdentityContractFactory from "../types/constructors/identity"; import IdentityContract from "../types/contracts/identity"; @@ -13,6 +13,7 @@ const wsProvider = new WsProvider("ws://127.0.0.1:9944"); const keyring = new Keyring({ type: "sr25519" }); const USDT_ASSET_ID = 1984; +const DUMMY_ASSET_ID = 42; const WS_ROROCO_LOCAL = "ws://127.0.0.1:9900"; const WS_ASSET_HUB_LOCAL = "ws://127.0.0.1:9910"; @@ -57,295 +58,473 @@ describe("TransactionRouter Cross-chain", () => { }); }); - test("Transferring cross-chain from asset's reserve chain works", async () => { - const sender: Sender = { - keypair: alice, - network: 0 - }; - - const receiver: Receiver = { - addressRaw: bob.addressRaw, - type: AccountType.accountId32, - network: 1, - }; - - const rococoProvider = new WsProvider(WS_ROROCO_LOCAL); - const rococoApi = await ApiPromise.create({ - provider: rococoProvider, - }); - - const assetHubProvider = new WsProvider(WS_ASSET_HUB_LOCAL); - const assetHubApi = await ApiPromise.create({ - provider: assetHubProvider, - }); + describe("Transferring cross-chain from asset's reserve chain", () => { + test("Transferring USDT cross-chain and paying with it for fees works", async () => { + const sender: Sender = { + keypair: alice, + network: 0 + }; + + const receiver: Receiver = { + addressRaw: bob.addressRaw, + type: AccountType.accountId32, + network: 1, + }; + + const rococoProvider = new WsProvider(WS_ROROCO_LOCAL); + const rococoApi = await ApiPromise.create({ + provider: rococoProvider, + }); - const trappistProvider = new WsProvider(WS_TRAPPIST_LOCAL); - const trappistApi = await ApiPromise.create({ - provider: trappistProvider, - }); + const assetHubProvider = new WsProvider(WS_ASSET_HUB_LOCAL); + const assetHubApi = await ApiPromise.create({ + provider: assetHubProvider, + }); - const lockdownMode = await getLockdownMode(trappistApi); - if (lockdownMode) { - await deactivateLockdown(trappistApi, alice); - } - - // Create assets on both networks - - if (!(await getAsset(assetHubApi, USDT_ASSET_ID))) { - await forceCreateAsset(rococoApi, assetHubApi, 1000, alice, USDT_ASSET_ID); - } - - if (!(await getAsset(trappistApi, USDT_ASSET_ID))) { - await createAsset(trappistApi, alice, USDT_ASSET_ID); - } - - // If the asset is not already registered in the registry make sure we add it. - if (!(await getAssetIdMultiLocation(trappistApi, USDT_ASSET_ID))) { - await registerReserveAsset(trappistApi, alice, USDT_ASSET_ID, { - parents: 1, - interior: { - X3: [ - { Parachain: 1000 }, - { PalletInstance: 50 }, - { GeneralIndex: USDT_ASSET_ID } - ] - } + const trappistProvider = new WsProvider(WS_TRAPPIST_LOCAL); + const trappistApi = await ApiPromise.create({ + provider: trappistProvider, }); - } - const mintAmount = 5000000000000; - // Mint some assets to the creator. - await mintAsset(assetHubApi, sender.keypair, USDT_ASSET_ID, mintAmount); + const lockdownMode = await getLockdownMode(trappistApi); + if (lockdownMode) { + await deactivateLockdown(trappistApi, alice); + } + + // Create assets on both networks - const senderBalanceBefore = await getAssetBalance(assetHubApi, USDT_ASSET_ID, alice.address); - const receiverBalanceBefore = await getAssetBalance(trappistApi, USDT_ASSET_ID, bob.address); + if (!(await getAsset(assetHubApi, DUMMY_ASSET_ID))) { + await forceCreateAsset(rococoApi, assetHubApi, 1000, alice, USDT_ASSET_ID); + } - const amount = 4000000000000; - const assetReserveChainId = 0; + if (!(await getAsset(trappistApi, USDT_ASSET_ID))) { + await createAsset(trappistApi, alice, USDT_ASSET_ID); + } + + // If the asset is not already registered in the registry make sure we add it. + if (!(await getAssetIdMultiLocation(trappistApi, USDT_ASSET_ID))) { + await registerReserveAsset(trappistApi, alice, USDT_ASSET_ID, { + parents: 1, + interior: { + X3: [ + { Parachain: 1000 }, + { PalletInstance: 50 }, + { GeneralIndex: USDT_ASSET_ID } + ] + } + }); + } - const asset: Fungible = { - multiAsset: { - interior: { - X2: [ - { PalletInstance: 50 }, - { GeneralIndex: USDT_ASSET_ID } - ] + const mintAmount = 5000000000000; + // Mint some assets to the creator. + await mintAsset(assetHubApi, sender.keypair, USDT_ASSET_ID, mintAmount); + + const senderBalanceBefore = await getAssetBalance(assetHubApi, USDT_ASSET_ID, alice.address); + const receiverBalanceBefore = await getAssetBalance(trappistApi, USDT_ASSET_ID, bob.address); + + const amount = 4000000000000; + const assetReserveChainId = 0; + + const asset: Fungible = { + multiAsset: { + interior: { + X2: [ + { PalletInstance: 50 }, + { GeneralIndex: USDT_ASSET_ID } + ] + }, + parents: 0, }, - parents: 0, - }, - amount - }; - - await TransactionRouter.sendTokens( - identityContract, - sender, - receiver, - assetReserveChainId, - asset - ); + amount + }; + + await TransactionRouter.sendTokens( + identityContract, + sender, + receiver, + assetReserveChainId, + asset + ); + + const senderBalanceAfter = await getAssetBalance(assetHubApi, USDT_ASSET_ID, alice.address); + const receiverBalanceAfter = await getAssetBalance(trappistApi, USDT_ASSET_ID, bob.address); + + expect(senderBalanceAfter).toBe(senderBalanceBefore - amount); + // The `receiverBalanceAfter` won't be exactly equal to `receiverBalanceBefore + amount` since some of the tokens are + // used for `BuyExecution`. + expect(receiverBalanceAfter).toBeGreaterThanOrEqual(receiverBalanceBefore); + }, 180000); + + test("Transferring cross-chain from asset's reserve and pay for fees with ROC", async () => { + const sender: Sender = { + keypair: alice, + network: 0 + }; + + const receiver: Receiver = { + addressRaw: bob.addressRaw, + type: AccountType.accountId32, + network: 1, + }; + + const rococoProvider = new WsProvider(WS_ROROCO_LOCAL); + const rococoApi = await ApiPromise.create({ + provider: rococoProvider, + }); - const senderBalanceAfter = await getAssetBalance(assetHubApi, USDT_ASSET_ID, alice.address); - const receiverBalanceAfter = await getAssetBalance(trappistApi, USDT_ASSET_ID, bob.address); + const assetHubProvider = new WsProvider(WS_ASSET_HUB_LOCAL); + const assetHubApi = await ApiPromise.create({ + provider: assetHubProvider, + }); - expect(senderBalanceAfter).toBe(senderBalanceBefore - amount); - // The `receiverBalanceAfter` won't be exactly equal to `receiverBalanceBefore + amount` since some of the tokens are - // used for `BuyExecution`. - expect(receiverBalanceAfter).toBeGreaterThan(receiverBalanceBefore); - }, 180000); + const trappistProvider = new WsProvider(WS_TRAPPIST_LOCAL); + const trappistApi = await ApiPromise.create({ + provider: trappistProvider, + }); - test("Transferring cross-chain to asset's reserve chain works", async () => { - // NOTE this test depends on the success of the first test. + const lockdownMode = await getLockdownMode(trappistApi); + if (lockdownMode) { + await deactivateLockdown(trappistApi, alice); + } - const rococoProvider = new WsProvider(WS_ROROCO_LOCAL); - const rococoApi = await ApiPromise.create({ - provider: rococoProvider, - }); + // Create assets on both networks - const assetHubProvider = new WsProvider(WS_ASSET_HUB_LOCAL); - const assetHubApi = await ApiPromise.create({ - provider: assetHubProvider, - }); + if (!(await getAsset(assetHubApi, DUMMY_ASSET_ID))) { + await forceCreateAsset(rococoApi, assetHubApi, 1000, alice, DUMMY_ASSET_ID); + } - const trappistProvider = new WsProvider(WS_TRAPPIST_LOCAL); - const trappistApi = await ApiPromise.create({ - provider: trappistProvider, - }); + if (!(await getAsset(trappistApi, DUMMY_ASSET_ID))) { + await createAsset(trappistApi, alice, DUMMY_ASSET_ID); + } - const lockdownMode = await getLockdownMode(trappistApi); - if (lockdownMode) { - await deactivateLockdown(trappistApi, alice); - } - - // Create assets on both networks. - - if (!(await getAsset(assetHubApi, USDT_ASSET_ID))) { - await forceCreateAsset(rococoApi, assetHubApi, 1000, alice, USDT_ASSET_ID); - } - - if (!(await getAsset(trappistApi, USDT_ASSET_ID))) { - await createAsset(trappistApi, alice, USDT_ASSET_ID); - } - - // If the asset is not already registered in the registry make sure we add it. - if (!(await getAssetIdMultiLocation(trappistApi, USDT_ASSET_ID))) { - await registerReserveAsset(trappistApi, alice, USDT_ASSET_ID, { - parents: 1, - interior: { - X3: [ - { Parachain: 1000 }, - { PalletInstance: 50 }, - { GeneralIndex: USDT_ASSET_ID } - ] - } + // If the asset is not already registered in the registry make sure we add it. + if (!(await getAssetIdMultiLocation(trappistApi, DUMMY_ASSET_ID))) { + await registerReserveAsset(trappistApi, alice, DUMMY_ASSET_ID, { + parents: 1, + interior: { + X3: [ + { Parachain: 1000 }, + { PalletInstance: 50 }, + { GeneralIndex: DUMMY_ASSET_ID } + ] + } + }); + } + + const mintAmount = 5000; + // Mint some assets to the creator. + await mintAsset(assetHubApi, sender.keypair, DUMMY_ASSET_ID, mintAmount); + + const senderBalanceBefore = await getAssetBalance(assetHubApi, DUMMY_ASSET_ID, alice.address); + const receiverBalanceBefore = await getAssetBalance(trappistApi, DUMMY_ASSET_ID, bob.address); + + const amount = 500; + const assetReserveChainId = 0; + + const asset: Fungible = { + multiAsset: { + interior: { + X2: [ + { PalletInstance: 50 }, + { GeneralIndex: DUMMY_ASSET_ID } + ] + }, + parents: 0, + }, + amount + }; + + await TransactionRouter.sendTokens( + identityContract, + sender, + receiver, + assetReserveChainId, + asset, + FeePayment.RelayChainNative + ); + + const senderBalanceAfter = await getAssetBalance(assetHubApi, DUMMY_ASSET_ID, alice.address); + const receiverBalanceAfter = await getAssetBalance(trappistApi, DUMMY_ASSET_ID, bob.address); + + expect(senderBalanceAfter).toBe(senderBalanceBefore - amount); + expect(receiverBalanceAfter).toBe(receiverBalanceBefore); + }, 180000); + }); + + describe("Transferring cross-chain to asset's reserve chain", () => { + test("Transferring USDT and paying with it for fees works", async () => { + // NOTE this test depends on the success of the first test. + + const rococoProvider = new WsProvider(WS_ROROCO_LOCAL); + const rococoApi = await ApiPromise.create({ + provider: rococoProvider, + }); + + const assetHubProvider = new WsProvider(WS_ASSET_HUB_LOCAL); + const assetHubApi = await ApiPromise.create({ + provider: assetHubProvider, + }); + + const trappistProvider = new WsProvider(WS_TRAPPIST_LOCAL); + const trappistApi = await ApiPromise.create({ + provider: trappistProvider, }); - } - - const amount = 950000000000; - - const sender: Sender = { - keypair: bob, - network: 1 - }; - - const receiver: Receiver = { - addressRaw: charlie.addressRaw, - type: AccountType.accountId32, - network: 0, - }; - - const asset: Fungible = { - multiAsset: { - interior: { - X3: [ - { Parachain: 1000 }, - { PalletInstance: 50 }, - { GeneralIndex: USDT_ASSET_ID } - ] + + const lockdownMode = await getLockdownMode(trappistApi); + if (lockdownMode) { + await deactivateLockdown(trappistApi, alice); + } + + // Create assets on both networks. + + if (!(await getAsset(assetHubApi, USDT_ASSET_ID))) { + await forceCreateAsset(rococoApi, assetHubApi, 1000, alice, USDT_ASSET_ID); + } + + if (!(await getAsset(trappistApi, USDT_ASSET_ID))) { + await createAsset(trappistApi, alice, USDT_ASSET_ID); + } + + // If the asset is not already registered in the registry make sure we add it. + if (!(await getAssetIdMultiLocation(trappistApi, USDT_ASSET_ID))) { + await registerReserveAsset(trappistApi, alice, USDT_ASSET_ID, { + parents: 1, + interior: { + X3: [ + { Parachain: 1000 }, + { PalletInstance: 50 }, + { GeneralIndex: USDT_ASSET_ID } + ] + } + }); + } + + const amount = 950000000000; + + const sender: Sender = { + keypair: bob, + network: 1 + }; + + const receiver: Receiver = { + addressRaw: charlie.addressRaw, + type: AccountType.accountId32, + network: 0, + }; + + const asset: Fungible = { + multiAsset: { + interior: { + X3: [ + { Parachain: 1000 }, + { PalletInstance: 50 }, + { GeneralIndex: USDT_ASSET_ID } + ] + }, + parents: 1, }, - parents: 1, - }, - amount - }; + amount + }; - const senderBalanceBefore = await getAssetBalance(trappistApi, USDT_ASSET_ID, bob.address); - const receiverBalanceBefore = await getAssetBalance(assetHubApi, USDT_ASSET_ID, charlie.address); + const senderBalanceBefore = await getAssetBalance(trappistApi, USDT_ASSET_ID, bob.address); + const receiverBalanceBefore = await getAssetBalance(assetHubApi, USDT_ASSET_ID, charlie.address); - // Transfer the tokens to charlies's account on asset hub: - await TransactionRouter.sendTokens(identityContract, sender, receiver, receiver.network, asset); + // Transfer the tokens to charlies's account on asset hub: + await TransactionRouter.sendTokens(identityContract, sender, receiver, receiver.network, asset); - // We need to wait a bit more to actually receive the assets on the base chain. - await delay(5000); + // We need to wait a bit more to actually receive the assets on the base chain. + await delay(5000); - const senderBalanceAfter = await getAssetBalance(trappistApi, USDT_ASSET_ID, bob.address); - const receiverBalanceAfter = await getAssetBalance(assetHubApi, USDT_ASSET_ID, charlie.address); + const senderBalanceAfter = await getAssetBalance(trappistApi, USDT_ASSET_ID, bob.address); + const receiverBalanceAfter = await getAssetBalance(assetHubApi, USDT_ASSET_ID, charlie.address); - // Some tolerance since part of the tokens will be used for fee payment. - const tolerance = 100000; - expect(senderBalanceAfter).toBeLessThanOrEqual(senderBalanceBefore - amount); - expect(receiverBalanceAfter).toBeGreaterThanOrEqual(receiverBalanceBefore + amount - tolerance); - }, 120000); + // Some tolerance since part of the tokens will be used for fee payment. + const tolerance = 100000; + expect(senderBalanceAfter).toBeLessThanOrEqual(senderBalanceBefore - amount); + expect(receiverBalanceAfter).toBeGreaterThanOrEqual(receiverBalanceBefore + amount - tolerance); + }, 120000); - test("Transferring cross-chain accross reserve chain works", async () => { - // NOTE this test depends on the success of the first test. + test("Transferring USDT and paying with ROC for fees works", async () => { + /* + const assetHubProvider = new WsProvider(WS_ASSET_HUB_LOCAL); + const assetHubApi = await ApiPromise.create({ + provider: assetHubProvider, + }); - const rococoProvider = new WsProvider(WS_ROROCO_LOCAL); - const rococoApi = await ApiPromise.create({ - provider: rococoProvider, - }); + const trappistProvider = new WsProvider(WS_TRAPPIST_LOCAL); + const trappistApi = await ApiPromise.create({ + provider: trappistProvider, + }); - const assetHubProvider = new WsProvider(WS_ASSET_HUB_LOCAL); - const assetHubApi = await ApiPromise.create({ - provider: assetHubProvider, - }); + const lockdownMode = await getLockdownMode(trappistApi); + if (lockdownMode) { + await deactivateLockdown(trappistApi, alice); + } - const trappistProvider = new WsProvider(WS_TRAPPIST_LOCAL); - const trappistApi = await ApiPromise.create({ - provider: trappistProvider, - }); + // Create dummy assets on both networks. - const baseProvider = new WsProvider("ws://127.0.0.1:9930"); - const baseApi = await ApiPromise.create({ - provider: baseProvider, - }); + if (!(await getAsset(assetHubApi, DUMMY_ASSET_ID))) { + await createAsset(assetHubApi, alice, DUMMY_ASSET_ID); + } - const lockdownMode = await getLockdownMode(trappistApi); - if (lockdownMode) { - await deactivateLockdown(trappistApi, alice); - } - - // Create assets on all networks. - - if (!(await getAsset(assetHubApi, USDT_ASSET_ID))) { - await forceCreateAsset(rococoApi, assetHubApi, 1000, alice, USDT_ASSET_ID); - } - - if (!(await getAsset(trappistApi, USDT_ASSET_ID))) { - await createAsset(trappistApi, alice, USDT_ASSET_ID); - } - - if (!(await getAsset(baseApi, USDT_ASSET_ID))) { - await createAsset(baseApi, alice, USDT_ASSET_ID); - } - - // If the asset is not already registered in the registry make sure we add it. - if (!(await getAssetIdMultiLocation(trappistApi, USDT_ASSET_ID))) { - await registerReserveAsset(trappistApi, alice, USDT_ASSET_ID, { - parents: 1, - interior: { - X3: [ - { Parachain: 1000 }, - { PalletInstance: 50 }, - { GeneralIndex: USDT_ASSET_ID } - ] - } + if (!(await getAsset(trappistApi, DUMMY_ASSET_ID))) { + await createAsset(trappistApi, alice, DUMMY_ASSET_ID); + } + + const mintAmount = 50000; + // Mint some assets to the alice. + await mintAsset(assetHubApi, alice, DUMMY_ASSET_ID, mintAmount); + + // If the asset is not already registered in the registry make sure we add it. + if (!(await getAssetIdMultiLocation(trappistApi, DUMMY_ASSET_ID))) { + await registerReserveAsset(trappistApi, alice, DUMMY_ASSET_ID, { + parents: 1, + interior: { + X3: [ + { Parachain: 1000 }, + { PalletInstance: 50 }, + { GeneralIndex: DUMMY_ASSET_ID } + ] + } + }); + } + + const amount = 500; + + const sender: Sender = { + keypair: alice, + network: 0 + }; + + const receiver: Receiver = { + addressRaw: bob.addressRaw, + type: AccountType.accountId32, + network: 1, + }; + + const asset: Fungible = { + multiAsset: { + interior: { + X3: [ + { Parachain: 1000 }, + { PalletInstance: 50 }, + { GeneralIndex: DUMMY_ASSET_ID } + ] + }, + parents: 0, + }, + amount + }; + + // Transfer the tokens to charlies's account on asset hub: + await TransactionRouter.sendTokens(identityContract, sender, receiver, receiver.network, asset, FeePayment.RelayChainNative); + */ + }, 180000) + }); + + describe("Transferring cross-chain accross reserve chain ", () => { + test("Transferring USDT and paying with it for fees works", async () => { + // NOTE this test depends on the success of the first test. + + const rococoProvider = new WsProvider(WS_ROROCO_LOCAL); + const rococoApi = await ApiPromise.create({ + provider: rococoProvider, + }); + + const assetHubProvider = new WsProvider(WS_ASSET_HUB_LOCAL); + const assetHubApi = await ApiPromise.create({ + provider: assetHubProvider, + }); + + const trappistProvider = new WsProvider(WS_TRAPPIST_LOCAL); + const trappistApi = await ApiPromise.create({ + provider: trappistProvider, }); - } - - const amount = 950000000000; - const assetReserveChainId = 0; - - const sender: Sender = { - keypair: bob, - network: 1 - }; - - const receiver: Receiver = { - addressRaw: bob.addressRaw, - type: AccountType.accountId32, - network: 2, - }; - - const asset: Fungible = { - multiAsset: { - interior: { - X3: [ - { Parachain: 1000 }, - { PalletInstance: 50 }, - { GeneralIndex: USDT_ASSET_ID } - ] + + const baseProvider = new WsProvider("ws://127.0.0.1:9930"); + const baseApi = await ApiPromise.create({ + provider: baseProvider, + }); + + const lockdownMode = await getLockdownMode(trappistApi); + if (lockdownMode) { + await deactivateLockdown(trappistApi, alice); + } + + // Create assets on all networks. + + if (!(await getAsset(assetHubApi, USDT_ASSET_ID))) { + await forceCreateAsset(rococoApi, assetHubApi, 1000, alice, USDT_ASSET_ID); + } + + if (!(await getAsset(trappistApi, USDT_ASSET_ID))) { + await createAsset(trappistApi, alice, USDT_ASSET_ID); + } + + if (!(await getAsset(baseApi, USDT_ASSET_ID))) { + await createAsset(baseApi, alice, USDT_ASSET_ID); + } + + // If the asset is not already registered in the registry make sure we add it. + if (!(await getAssetIdMultiLocation(trappistApi, USDT_ASSET_ID))) { + await registerReserveAsset(trappistApi, alice, USDT_ASSET_ID, { + parents: 1, + interior: { + X3: [ + { Parachain: 1000 }, + { PalletInstance: 50 }, + { GeneralIndex: USDT_ASSET_ID } + ] + } + }); + } + + const amount = 950000000000; + const assetReserveChainId = 0; + + const sender: Sender = { + keypair: bob, + network: 1 + }; + + const receiver: Receiver = { + addressRaw: bob.addressRaw, + type: AccountType.accountId32, + network: 2, + }; + + const asset: Fungible = { + multiAsset: { + interior: { + X3: [ + { Parachain: 1000 }, + { PalletInstance: 50 }, + { GeneralIndex: USDT_ASSET_ID } + ] + }, + parents: 1, }, - parents: 1, - }, - amount - }; + amount + }; - const senderBalanceBefore = await getAssetBalance(trappistApi, USDT_ASSET_ID, bob.address); - const receiverBalanceBefore = await getAssetBalance(baseApi, USDT_ASSET_ID, bob.address); + const senderBalanceBefore = await getAssetBalance(trappistApi, USDT_ASSET_ID, bob.address); + const receiverBalanceBefore = await getAssetBalance(baseApi, USDT_ASSET_ID, bob.address); - // Transfer the tokens to bob's account on base: - await TransactionRouter.sendTokens(identityContract, sender, receiver, assetReserveChainId, asset); + // Transfer the tokens to bob's account on base: + await TransactionRouter.sendTokens(identityContract, sender, receiver, assetReserveChainId, asset); - // We need to wait a bit more to actually receive the assets on the base chain. - await delay(12000); + // We need to wait a bit more to actually receive the assets on the base chain. + await delay(12000); - const senderBalanceAfter = await getAssetBalance(trappistApi, USDT_ASSET_ID, bob.address); - const receiverBalanceAfter = await getAssetBalance(baseApi, USDT_ASSET_ID, bob.address); + const senderBalanceAfter = await getAssetBalance(trappistApi, USDT_ASSET_ID, bob.address); + const receiverBalanceAfter = await getAssetBalance(baseApi, USDT_ASSET_ID, bob.address); - // Some tolerance since part of the tokens will be used for fee payment. - const tolerance = 100000; - expect(senderBalanceAfter).toBeLessThanOrEqual(senderBalanceBefore - amount); - expect(receiverBalanceAfter).toBeGreaterThanOrEqual(receiverBalanceBefore + amount - tolerance); - }, 180000); + // Some tolerance since part of the tokens will be used for fee payment. + const tolerance = 100000; + expect(senderBalanceAfter).toBeLessThanOrEqual(senderBalanceBefore - amount); + expect(receiverBalanceAfter).toBeGreaterThanOrEqual(receiverBalanceBefore + amount - tolerance); + }, 180000); + }); }); const addNetwork = async ( diff --git a/src/utils/transactionRouter/reserveTransfer.ts b/src/utils/transactionRouter/reserveTransfer.ts index f84f0e0..6b2ae3d 100644 --- a/src/utils/transactionRouter/reserveTransfer.ts +++ b/src/utils/transactionRouter/reserveTransfer.ts @@ -27,9 +27,15 @@ class ReserveTransfer { const destination = this.getDestination(isOriginPara, destParaId, destParaId >= 0); const beneficiary = this.getReserveTransferBeneficiary(receiver); - const multiAsset = this.getMultiAsset(asset); + const multiAsset = this.getMultiAsset(asset, { + multiAsset: { + parents: isOriginPara ? 1 : 0, + interior: "Here" + }, + amount: 4500000000 + }); - const feeAssetItem = 0; + const feeAssetItem = feePayment == FeePayment.RelayChainNative ? 1 : 0; const weightLimit = "Unlimited"; const xcmPallet = (originApi.tx.xcmPallet || originApi.tx.polkadotXcm); @@ -175,7 +181,7 @@ class ReserveTransfer { reserve, xcm: [ // TODO: the hardcoded number isn't really accurate to what we actually need. - this.buyExecution(feeAsset, 4500000000000), + this.buyExecution(feeAsset, 450000000000), this.depositReserveAsset({ Wild: "All" }, 1, { parents: 1, interior: { @@ -387,18 +393,33 @@ class ReserveTransfer { } // Returns a proper MultiAsset. - private static getMultiAsset(asset: Fungible): any { - return { - V2: [ + private static getMultiAsset(asset: Fungible, feeAsset?: Fungible): any { + const assets = [ + { + fun: { + Fungible: asset.amount, + }, + id: { + Concrete: asset.multiAsset, + }, + }, + ]; + + if (feeAsset) { + assets.push( { fun: { - Fungible: asset.amount, + Fungible: feeAsset.amount, }, id: { - Concrete: asset.multiAsset, + Concrete: feeAsset.multiAsset, }, - }, - ] + } + ) + } + + return { + V2: assets } } @@ -426,17 +447,10 @@ class ReserveTransfer { private static getFeePaymentAsset(feePayment: FeePayment, asset: Fungible, isOriginPara: boolean, reserveParaId: number): any { if (feePayment == FeePayment.RelayChainNative) { - if (isOriginPara) { - return { - parents: 1, - interior: "Here" - }; - } else { - return { - parents: 0, - interior: "Here" - } - } + return { + parents: isOriginPara ? 1 : 0, + interior: "Here" + }; } else if (feePayment == FeePayment.Asset) { // NOTE: we use parse and stringify to make a hard copy of the asset. const assetFromReservePerspective = JSON.parse(JSON.stringify(asset.multiAsset));