From c62c987a8a47498392d102b73b26693dd5a5441e Mon Sep 17 00:00:00 2001 From: Sergej Sakac <73715684+Szegoo@users.noreply.github.com> Date: Fri, 25 Aug 2023 21:12:16 +0200 Subject: [PATCH] Cross-chain: Teleport (#62) * Cross-chain: Teleport * remove * teleport works & tested * make eslint happy --- __tests__/crossChainRouter.test.ts | 6 +- __tests__/teleport.test.ts | 131 ++++++++++++++++++ src/utils/index.ts | 4 +- src/utils/transactionRouter/index.ts | 119 +++++++++++++++- .../transactionRouter/reserveTransfer.test.ts | 35 ++--- .../transactionRouter/reserveTransfer.ts | 126 ++--------------- .../transactionRouter/teleportTransfer.ts | 54 ++++++++ .../transactionRouter/teleportableRoutes.ts | 41 ++++++ 8 files changed, 369 insertions(+), 147 deletions(-) create mode 100644 __tests__/teleport.test.ts create mode 100644 src/utils/transactionRouter/teleportTransfer.ts create mode 100644 src/utils/transactionRouter/teleportableRoutes.ts diff --git a/__tests__/crossChainRouter.test.ts b/__tests__/crossChainRouter.test.ts index 6e3e541..a9edf21 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 { Fungible, Receiver, Sender } from "../src/utils/transactionRouter/types"; import IdentityContractFactory from "../types/constructors/identity"; import IdentityContract from "../types/contracts/identity"; @@ -18,7 +18,7 @@ const WS_ROROCO_LOCAL = "ws://127.0.0.1:9900"; const WS_ASSET_HUB_LOCAL = "ws://127.0.0.1:9910"; const WS_TRAPPIST_LOCAL = "ws://127.0.0.1:9920"; -describe("TransactionRouter Cross-chain", () => { +describe("TransactionRouter Cross-chain reserve transfer", () => { let swankyApi: ApiPromise; let alice: KeyringPair; let bob: KeyringPair; diff --git a/__tests__/teleport.test.ts b/__tests__/teleport.test.ts new file mode 100644 index 0000000..62ae7a0 --- /dev/null +++ b/__tests__/teleport.test.ts @@ -0,0 +1,131 @@ +import { ApiPromise, Keyring, WsProvider } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; + +import TransactionRouter from "../src/utils/transactionRouter"; +import { Fungible, Receiver, Sender } from "../src/utils/transactionRouter/types"; + +import IdentityContractFactory from "../types/constructors/identity"; +import IdentityContract from "../types/contracts/identity"; + +import { AccountType, NetworkInfo } from "../types/types-arguments/identity"; + +const WS_ROROCO_LOCAL = "ws://127.0.0.1:9900"; +const WS_ASSET_HUB_LOCAL = "ws://127.0.0.1:9910"; + +const wsProvider = new WsProvider("ws://127.0.0.1:9944"); +const keyring = new Keyring({ type: "sr25519" }); + +describe("TransactionRouter Cross-chain teleport", () => { + let swankyApi: ApiPromise; + let alice: KeyringPair; + let bob: KeyringPair; + let charlie: KeyringPair; + let identityContract: any; + + beforeEach(async function (): Promise { + swankyApi = await ApiPromise.create({ + provider: wsProvider, + noInitWarn: true, + }); + alice = keyring.addFromUri("//Alice"); + bob = keyring.addFromUri("//Bob"); + charlie = keyring.addFromUri("//Charlie"); + + const factory = new IdentityContractFactory(swankyApi, alice); + identityContract = new IdentityContract( + (await factory.new()).address, + alice, + swankyApi + ); + + await addNetwork(identityContract, alice, { + rpcUrl: WS_ROROCO_LOCAL, + accountType: AccountType.accountId32, + }); + + await addNetwork(identityContract, alice, { + rpcUrl: WS_ASSET_HUB_LOCAL, + accountType: AccountType.accountId32, + }); + }); + + + test("Teleporting ROC 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, + }); + + const senderBalanceBefore = await getBalance(rococoApi, alice.address); + const receiverBalanceBefore = await getBalance(assetHubApi, bob.address); + + const amount = 4 * Math.pow(10, 12); // 4 KSM + const assetReserveChainId = 0; + + const asset: Fungible = { + multiAsset: { + parents: 0, + interior: "Here" + }, + amount + }; + + await TransactionRouter.sendTokens( + identityContract, + sender, + receiver, + assetReserveChainId, + asset + ); + + // Delay a bit just to be safe. + await delay(5000); + + const senderBalanceAfter = await getBalance(rococoApi, alice.address); + const receiverBalanceAfter = await getBalance(assetHubApi, bob.address); + + // Expect the balance to be possibly lower than `senderBalanceBefore - amount` since + // the fees also need to be paid. + expect(Number(senderBalanceAfter)).toBeLessThanOrEqual(senderBalanceBefore - amount); + + // Tolerance for fee payment on the receiver side. + const tolerance = 50000000; + expect(Number(receiverBalanceAfter)).toBeGreaterThanOrEqual((receiverBalanceBefore + amount) - tolerance); + }, 120000); +}); + +const addNetwork = async ( + contract: IdentityContract, + signer: KeyringPair, + network: NetworkInfo +): Promise => { + await contract + .withSigner(signer) + .tx.addNetwork(network); +}; + +const getBalance = async (api: ApiPromise, who: string): Promise => { + const maybeBalance: any = (await api.query.system.account(who)).toPrimitive(); + if (maybeBalance && maybeBalance.data) { + return maybeBalance.data.free; + } + return 0; +} + +const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); diff --git a/src/utils/index.ts b/src/utils/index.ts index 1a007f6..5bf97e3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -27,6 +27,6 @@ export const getParaId = async (api: ApiPromise): Promise => { const response = (await api.query.parachainInfo.parachainId()).toJSON(); return Number(response); } else { - return -1; + return 0; } -} \ No newline at end of file +} diff --git a/src/utils/transactionRouter/index.ts b/src/utils/transactionRouter/index.ts index bf79e27..5378796 100644 --- a/src/utils/transactionRouter/index.ts +++ b/src/utils/transactionRouter/index.ts @@ -1,9 +1,13 @@ import { ApiPromise, WsProvider } from "@polkadot/api"; import ReserveTransfer from "./reserveTransfer"; +import { TeleportableRoute, teleportableRoutes } from "./teleportableRoutes"; +import TeleportTransfer from "./teleportTransfer"; import TransferAsset from "./transferAsset"; import { Fungible, Receiver, Sender } from "./types"; +import { getParaId } from ".."; import IdentityContract from "../../../types/contracts/identity"; +import { AccountType } from "../../../types/types-arguments/identity"; // Responsible for handling all the transfer logic. // @@ -55,12 +59,30 @@ class TransactionRouter { const originApi = await this.getApi(identityContract, sender.network); const destApi = await this.getApi(identityContract, receiver.network); + ensureContainsXcmPallet(destApi); + + const originParaId = await getParaId(originApi); + const destParaId = await getParaId(destApi); + + const maybeTeleportableRoute: TeleportableRoute = { + relayChain: process.env.RELAY_CHAIN ? process.env.RELAY_CHAIN : "rococo", + originParaId: originParaId, + destParaId: destParaId, + multiAsset: asset.multiAsset + }; + + if (teleportableRoutes.some(route => JSON.stringify(route) === JSON.stringify(maybeTeleportableRoute))) { + // The asset is allowed to be teleported between the origin and the destination. + await TeleportTransfer.send(originApi, destApi, sender.keypair, receiver, asset); + return; + } + // The sender chain is the reserve chain of the asset. This will simply use the existing // `limitedReserveTransferAssets` extrinsic if (sender.network == reserveChainId) { await ReserveTransfer.sendFromReserveChain( originApi, - destApi, + destParaId, sender.keypair, receiver, asset @@ -69,7 +91,7 @@ class TransactionRouter { // The destination chain is the reserve chain of the asset: await ReserveTransfer.sendToReserveChain( originApi, - destApi, + destParaId, sender.keypair, receiver, asset @@ -79,11 +101,14 @@ class TransactionRouter { // For this we will have to send tokens accross the reserve chain. const reserveChain = await this.getApi(identityContract, reserveChainId); + ensureContainsXcmPallet(reserveChain); + + const reserveParaId = await getParaId(reserveChain); await ReserveTransfer.sendAcrossReserveChain( originApi, - destApi, - reserveChain, + destParaId, + reserveParaId, sender.keypair, receiver, asset @@ -104,3 +129,89 @@ class TransactionRouter { } export default TransactionRouter; + +// Returns the destination of an xcm transfer. +// +// The destination is an entity that will process the xcm message(i.e a relaychain or a parachain). +export const getDestination = (isOriginPara: boolean, destParaId: number, isDestPara: boolean): any => { + const parents = isOriginPara ? 1 : 0; + + if (isDestPara) { + return { + V2: + { + parents, + interior: { + X1: { Parachain: destParaId } + } + } + } + } else { + // If the destination is not a parachain it is basically a relay chain. + return { + V2: + { + parents, + interior: "Here" + } + } + } +} + +// Returns the beneficiary of an xcm reserve or teleport transfer. +// +// The beneficiary is an interior entity of the destination that will actually receive the tokens. +export const getTransferBeneficiary = (receiver: Receiver): any => { + const receiverAccount = getReceiverAccount(receiver); + + return { + V2: { + parents: 0, + interior: { + X1: { + ...receiverAccount + } + } + } + }; +} + +export const getReceiverAccount = (receiver: Receiver): any => { + if (receiver.type == AccountType.accountId32) { + return { + AccountId32: { + network: "Any", + id: receiver.addressRaw, + }, + }; + } else if (receiver.type == AccountType.accountKey20) { + return { + AccountKey20: { + network: "Any", + id: receiver.addressRaw, + }, + }; + } +} + +// Returns a proper MultiAsset. +export const getMultiAsset = (asset: Fungible): any => { + return { + V2: [ + { + fun: { + Fungible: asset.amount, + }, + id: { + Concrete: asset.multiAsset, + }, + }, + ] + } +} + +const ensureContainsXcmPallet = (api: ApiPromise) => { + if (!(api.tx.xcmPallet || api.tx.polkadotXcm)) { + throw new Error("The blockchain does not support XCM"); + } +} diff --git a/src/utils/transactionRouter/reserveTransfer.test.ts b/src/utils/transactionRouter/reserveTransfer.test.ts index 762e6ed..0734a67 100644 --- a/src/utils/transactionRouter/reserveTransfer.test.ts +++ b/src/utils/transactionRouter/reserveTransfer.test.ts @@ -5,6 +5,7 @@ import { Keyring } from "@polkadot/api"; import { cryptoWaitReady } from "@polkadot/util-crypto"; +import { getDestination, getMultiAsset, getTransferBeneficiary } from "."; import ReserveTransfer from "./reserveTransfer"; import { Fungible, Receiver } from "./types"; import { AccountType } from "../../../types/types-arguments/identity"; @@ -15,18 +16,14 @@ const ecdsaKeyring = new Keyring({ type: "ecdsa" }); describe("TransactionRouter unit tests", () => { describe("getDestination works", () => { it("Works with the destination being the relay chain", () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(ReserveTransfer.getDestination(true, 69, false)).toStrictEqual({ + expect(getDestination(true, 69, false)).toStrictEqual({ V2: { parents: 1, interior: "Here", }, }); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(ReserveTransfer.getDestination(false, 69, false)).toStrictEqual({ + expect(getDestination(false, 69, false)).toStrictEqual({ V2: { parents: 0, interior: "Here", @@ -35,9 +32,7 @@ describe("TransactionRouter unit tests", () => { }); it("Works with the destination being a parachain", () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(ReserveTransfer.getDestination(false, 2000, true)).toStrictEqual({ + expect(getDestination(false, 2000, true)).toStrictEqual({ V2: { parents: 0, interior: { @@ -46,9 +41,7 @@ describe("TransactionRouter unit tests", () => { }, }); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(ReserveTransfer.getDestination(true, 2000, true)).toStrictEqual({ + expect(getDestination(true, 2000, true)).toStrictEqual({ V2: { parents: 1, interior: { @@ -59,7 +52,7 @@ describe("TransactionRouter unit tests", () => { }); }); - describe("getReserveTransferBeneficiary works", () => { + describe("getTransferBeneficiary works", () => { it("Works with AccountId32", async () => { await cryptoWaitReady(); @@ -73,9 +66,7 @@ describe("TransactionRouter unit tests", () => { }; expect( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - ReserveTransfer.getReserveTransferBeneficiary(receiver), + getTransferBeneficiary(receiverAccId32), ).toStrictEqual({ V2: { parents: 0, @@ -97,9 +88,7 @@ describe("TransactionRouter unit tests", () => { }; expect( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - ReserveTransfer.getReserveTransferBeneficiary(receiver), + getTransferBeneficiary(receiverAccKey20), ).toStrictEqual({ V2: { parents: 0, @@ -126,9 +115,7 @@ describe("TransactionRouter unit tests", () => { amount: 200, }; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(ReserveTransfer.getMultiAsset(asset)).toStrictEqual({ + expect(getMultiAsset(asset)).toStrictEqual({ V2: [ { fun: { @@ -614,7 +601,7 @@ describe("TransactionRouter unit tests", () => { it("Works from parachain to relaychain", () => { const bob = ecdsaKeyring.addFromUri("//Bob"); - const destParaId = -1; + const destParaId = 0; const beneficiary: Receiver = { addressRaw: bob.addressRaw, network: 1, @@ -974,7 +961,7 @@ describe("TransactionRouter unit tests", () => { it("Works with relaychain being the reserve chain", () => { const bob = ecdsaKeyring.addFromUri("//Bob"); - const reserveParaId = -1; + const reserveParaId = 0; const destParaId = 2002; const beneficiary: Receiver = { addressRaw: bob.addressRaw, diff --git a/src/utils/transactionRouter/reserveTransfer.ts b/src/utils/transactionRouter/reserveTransfer.ts index e17fbb5..593a5c0 100644 --- a/src/utils/transactionRouter/reserveTransfer.ts +++ b/src/utils/transactionRouter/reserveTransfer.ts @@ -1,9 +1,8 @@ import { ApiPromise } from "@polkadot/api"; import { KeyringPair } from "@polkadot/keyring/types"; +import { getDestination, getMultiAsset, getReceiverAccount, getTransferBeneficiary } from "."; import { Fungible, Receiver } from "./types"; -import { getParaId } from ".."; -import { AccountType } from "../../../types/types-arguments/identity"; class ReserveTransfer { // Transfers assets from the sender to the receiver. @@ -11,22 +10,17 @@ class ReserveTransfer { // This function assumes that the sender chain is the reserve chain of the asset. public static async sendFromReserveChain( originApi: ApiPromise, - destinationApi: ApiPromise, + destParaId: number, sender: KeyringPair, receiver: Receiver, asset: Fungible ): Promise { - this.ensureContainsXcmPallet(originApi); - this.ensureContainsXcmPallet(destinationApi); - - const destParaId = await getParaId(destinationApi); - // eslint-disable-next-line no-prototype-builtins const isOriginPara = originApi.query.hasOwnProperty("parachainInfo"); - const destination = this.getDestination(isOriginPara, destParaId, destParaId >= 0); - const beneficiary = this.getReserveTransferBeneficiary(receiver); - const multiAsset = this.getMultiAsset(asset); + const destination = getDestination(isOriginPara, destParaId, destParaId > 0); + const beneficiary = getTransferBeneficiary(receiver); + const multiAsset = getMultiAsset(asset); const feeAssetItem = 0; const weightLimit = "Unlimited"; @@ -58,19 +52,15 @@ class ReserveTransfer { // reserve chain of the asset. public static async sendToReserveChain( originApi: ApiPromise, - destinationApi: ApiPromise, + destParaId: number, sender: KeyringPair, receiver: Receiver, asset: Fungible ): Promise { - this.ensureContainsXcmPallet(originApi); - this.ensureContainsXcmPallet(destinationApi); - - const destinationParaId = await getParaId(destinationApi); // 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, destParaId, receiver, isOriginPara); const xcmPallet = originApi.tx.xcmPallet || originApi.tx.polkadotXcm; @@ -95,22 +85,16 @@ class ReserveTransfer { // For this reason we are gonna need to transfer the asset across the reserve chain. public static async sendAcrossReserveChain( originApi: ApiPromise, - destinationApi: ApiPromise, - reserveChainApi: ApiPromise, + destParaId: number, + reserveParaId: number, sender: KeyringPair, receiver: Receiver, asset: Fungible ): Promise { - this.ensureContainsXcmPallet(originApi); - this.ensureContainsXcmPallet(destinationApi); - this.ensureContainsXcmPallet(reserveChainApi); - - const reserveParaId = await getParaId(reserveChainApi); - const destinationParaId = await getParaId(destinationApi); // 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, destParaId, receiver, isOriginPara); const xcmPallet = originApi.tx.xcmPallet || originApi.tx.polkadotXcm; @@ -210,7 +194,7 @@ class ReserveTransfer { // 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) { + if (destParaId > 0) { // The location of the asset will always start with the parachain if the reserve is a parachain. this.assetFromReservePerspective(assetFromReservePerspective); } else { @@ -293,7 +277,7 @@ class ReserveTransfer { const beneficiary = { parents: 0, interior: { - X1: this.getReceiverAccount(receiver) + X1: getReceiverAccount(receiver) } }; @@ -326,92 +310,6 @@ class ReserveTransfer { } } - // Returns the destination of an xcm reserve transfer. - // - // The destination is an entity that will process the xcm message(i.e a relaychain or a parachain). - private static getDestination(isOriginPara: boolean, destParaId: number, isDestPara: boolean): any { - const parents = isOriginPara ? 1 : 0; - - if (isDestPara) { - return { - V2: - { - parents, - interior: { - X1: { Parachain: destParaId } - } - } - } - } else { - // If the destination is not a parachain it is basically a relay chain. - return { - V2: - { - parents, - interior: "Here" - } - } - } - } - - // Returns the beneficiary of an xcm reserve transfer. - // - // The beneficiary is an interior entity of the destination that will actually receive the tokens. - private static getReserveTransferBeneficiary(receiver: Receiver) { - const receiverAccount = this.getReceiverAccount(receiver); - - return { - V2: { - parents: 0, - interior: { - X1: { - ...receiverAccount - } - } - } - }; - } - - private static getReceiverAccount(receiver: Receiver): any { - if (receiver.type == AccountType.accountId32) { - return { - AccountId32: { - network: "Any", - id: receiver.addressRaw, - }, - }; - } else if (receiver.type == AccountType.accountKey20) { - return { - AccountKey20: { - network: "Any", - id: receiver.addressRaw, - }, - }; - } - } - - // Returns a proper MultiAsset. - private static getMultiAsset(asset: Fungible): any { - return { - V2: [ - { - fun: { - Fungible: asset.amount, - }, - id: { - Concrete: asset.multiAsset, - }, - }, - ] - } - } - - private static ensureContainsXcmPallet(api: ApiPromise) { - if (!(api.tx.xcmPallet || api.tx.polkadotXcm)) { - throw new Error("The blockchain does not support XCM"); - } - } - // Helper function to remove a specific key from an object. private static assetFromReservePerspective(location: any) { const junctions = this.extractJunctions(location); diff --git a/src/utils/transactionRouter/teleportTransfer.ts b/src/utils/transactionRouter/teleportTransfer.ts new file mode 100644 index 0000000..d15a41d --- /dev/null +++ b/src/utils/transactionRouter/teleportTransfer.ts @@ -0,0 +1,54 @@ +import { ApiPromise } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; + +import { getDestination, getMultiAsset, getTransferBeneficiary } from "."; +import { Fungible, Receiver } from "./types"; +import { getParaId } from ".."; + +class TeleportTransfer { + public static async send( + originApi: ApiPromise, + destApi: ApiPromise, + sender: KeyringPair, + receiver: Receiver, + asset: Fungible + ): Promise { + const xcmPallet = originApi.tx.xcmPallet || originApi.tx.polkadotXcm; + + if (!xcmPallet) { + throw new Error("The blockchain does not support XCM"); + }; + + // eslint-disable-next-line no-prototype-builtins + const isOriginPara = originApi.query.hasOwnProperty("parachainInfo"); + + const destParaId = await getParaId(destApi); + + const destination = getDestination(isOriginPara, destParaId, destParaId > 0); + const beneficiary = getTransferBeneficiary(receiver); + const multiAsset = getMultiAsset(asset); + + const feeAssetItem = 0; + const weightLimit = "Unlimited"; + + const teleport = xcmPallet.limitedTeleportAssets( + destination, + beneficiary, + multiAsset, + feeAssetItem, + weightLimit + ); + + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve) => { + const unsub = await teleport.signAndSend(sender, (result: any) => { + if (result.status.isFinalized) { + unsub(); + resolve(); + } + }) + }); + } +} + +export default TeleportTransfer; diff --git a/src/utils/transactionRouter/teleportableRoutes.ts b/src/utils/transactionRouter/teleportableRoutes.ts new file mode 100644 index 0000000..55e2625 --- /dev/null +++ b/src/utils/transactionRouter/teleportableRoutes.ts @@ -0,0 +1,41 @@ +// File containing all the possible assets on all possible routes that support asset +// teleportation. + +import AssetRegistry from "../assetRegistry"; + +export type TeleportableRoute = { + relayChain: string, + originParaId: number, + destParaId: number, + multiAsset: any +}; + +export const teleportableRoutes: TeleportableRoute[] = [ + { + relayChain: "polkadot", + originParaId: 0, + destParaId: 1000, + multiAsset: AssetRegistry.xcmInteriorToMultiAsset( + JSON.parse('[{"network":"polkadot"},"here"]'), + false, + ), + }, + { + relayChain: "kusama", + originParaId: 0, + destParaId: 1000, + multiAsset: AssetRegistry.xcmInteriorToMultiAsset( + JSON.parse('[{"network":"kusama"},"here"]'), + false, + ), + }, + { + relayChain: "rococo", + originParaId: 0, + destParaId: 1000, + multiAsset: AssetRegistry.xcmInteriorToMultiAsset( + JSON.parse('[{"network":"rocooc"},"here"]'), + false, + ), + }, +];