From eed8b827d4457aecb9e9c1697104999a3581ad7f Mon Sep 17 00:00:00 2001 From: Sergej Sakac Date: Fri, 25 Aug 2023 14:00:24 +0200 Subject: [PATCH 1/4] Cross-chain: Teleport --- __tests__/crossChainRouter.test.ts | 6 +- __tests__/teleport.test.ts | 114 ++++++++++++++++ routes.json | 9 ++ scripts/index.ts | 121 +++++++++++++++++ src/utils/index.ts | 2 +- src/utils/transactionRouter/index.ts | 115 +++++++++++++++- .../transactionRouter/reserveTransfer.test.ts | 31 ++--- .../transactionRouter/reserveTransfer.ts | 123 ++---------------- .../transactionRouter/teleportTransfer.ts | 53 ++++++++ teleportableRoutes.ts | 30 +++++ 10 files changed, 462 insertions(+), 142 deletions(-) create mode 100644 __tests__/teleport.test.ts create mode 100644 routes.json create mode 100644 scripts/index.ts create mode 100644 src/utils/transactionRouter/teleportTransfer.ts create mode 100644 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..4edfac7 --- /dev/null +++ b/__tests__/teleport.test.ts @@ -0,0 +1,114 @@ +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 receiverBalanceBefore = await getBalance(rococoApi, bob.address); + const senderBalanceBefore = await getBalance(assetHubApi, alice.address); + + const amount = 4000000000000; + const assetReserveChainId = 0; + + const asset: Fungible = { + multiAsset: { + interior: "Here" + }, + amount + }; + + await TransactionRouter.sendTokens( + identityContract, + sender, + receiver, + assetReserveChainId, + asset + ); + }); +}); + +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)).toJSON(); + if (maybeBalance && maybeBalance.data) { + return maybeBalance.data.free; + } + return 0; +} \ No newline at end of file diff --git a/routes.json b/routes.json new file mode 100644 index 0000000..11720dd --- /dev/null +++ b/routes.json @@ -0,0 +1,9 @@ +[ + { + "xcAsset": "[{\"network\":\"polkadot\"},\"here\"]", + "originParaId": 0, + "destParaId": 1000, + "outcome": "success", + "lastTest": "1692949324972" + } +] \ No newline at end of file diff --git a/scripts/index.ts b/scripts/index.ts new file mode 100644 index 0000000..f92ddfe --- /dev/null +++ b/scripts/index.ts @@ -0,0 +1,121 @@ +const axios = require("axios"); + +const API_KEY = "b6c669d34069413ab246c1a03b2f1c12"; +const apiUrl = 'https://polkadot.api.subscan.io/api/scan/extrinsics'; + +const endpoints = [ + 'polkadot.api.subscan.io', + 'kusama.api.subscan.io', + 'darwinia.api.subscan.io', + 'assethub-polkadot.api.subscan.io', + 'assethub-kusama.api.subscan.io', + 'assethub-rococo.api.subscan.io', + 'acala.api.subscan.io', + 'acala-testnet.api.subscan.io', + 'alephzero.api.subscan.io', + 'altair.api.subscan.io', + 'astar.api.subscan.io', + 'bajun.api.subscan.io', + 'basilisk.api.subscan.io', + 'bifrost.api.subscan.io', + 'bifrost-kusama.api.subscan.io', + 'bifrost-testnet.api.subscan.io', + 'calamari.api.subscan.io', + 'centrifuge.api.subscan.io', + 'centrifuge-standalone-history.api.subscan.io', + 'chainx.api.subscan.io', + 'clover.api.subscan.io', + 'clv.api.subscan.io', + 'clover-testnet.api.subscan.io', + 'composable.api.subscan.io', + 'crab.api.subscan.io', + 'crust.api.subscan.io', + 'maxwell.api.subscan.io', + 'shadow.api.subscan.io', + 'dbc.api.subscan.io', + 'dock.api.subscan.io', + 'dolphin.api.subscan.io', + 'edgeware.api.subscan.io', + 'efinity.api.subscan.io', + 'encointer.api.subscan.io', + 'equilibrium.api.subscan.io', + 'genshiro.api.subscan.io', + 'humanode.api.subscan.io', + 'hydradx.api.subscan.io', + 'integritee.api.subscan.io', + 'interlay.api.subscan.io', + 'karura.api.subscan.io', + 'kintsugi.api.subscan.io', + 'khala.api.subscan.io', + 'krest.api.subscan.io', + 'kilt-testnet.api.subscan.io', + 'spiritnet.api.subscan.io', + 'litmus.api.subscan.io', + 'mangatax.api.subscan.io', + 'moonbase.api.subscan.io', + 'moonbeam.api.subscan.io', + 'moonriver.api.subscan.io', + 'nodle.api.subscan.io', + 'origintrail.api.subscan.io', + 'origintrail-testnet.api.subscan.io', + 'pangolin.api.subscan.io', + 'pangolin-parachain.api.subscan.io', + 'pangoro.api.subscan.io', + 'parallel.api.subscan.io', + 'parallel-heiko.api.subscan.io', + 'peaq-testnet.api.subscan.io', + 'phala.api.subscan.io', + 'picasso.api.subscan.io', + 'picasso-rococo.api.subscan.io', + 'pioneer.api.subscan.io', + 'polkadex.api.subscan.io', + 'polymesh.api.subscan.io', + 'polymesh-testnet.api.subscan.io', + 'plasm.api.subscan.io', + 'quartz.api.subscan.io', + 'reef.api.subscan.io', + 'robonomics.api.subscan.io', + 'rococo.api.subscan.io', + 'sakura.api.subscan.io', + 'shibuya.api.subscan.io', + 'shiden.api.subscan.io', + 'sora.api.subscan.io', + 'subspace.api.subscan.io', + 'stafi.api.subscan.io', + 'datahighway.api.subscan.io', + 'turing.api.subscan.io', + 'unique.api.subscan.io', + 'vara.api.subscan.io', + 'westend.api.subscan.io', + 'zeitgeist.api.subscan.io' +] + +const requestData = { + row: 50, + page: 0, + module: 'polkadotXcm', + call: 'execute', + // success: true +}; + +const headers = { + 'Content-Type': 'application/json', + 'X-API-Key': API_KEY +}; + +endpoints.forEach((endpoint) => { + const url = `https://${endpoint}/api/scan/extrinsics`; + + console.log(url); + axios.post(url, requestData, { headers }) + .then((response: any) => { + console.log('Response:', response.data); + if (response.data.data.count > 0) { + console.log(endpoint); + console.log(response.data.data.extrinsics); + } + }) + .catch((_error: any) => { + //console.error('Error:', _error); + }); +}); diff --git a/src/utils/index.ts b/src/utils/index.ts index 1a007f6..fe746e7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -29,4 +29,4 @@ export const getParaId = async (api: ApiPromise): Promise => { } else { return -1; } -} \ No newline at end of file +} diff --git a/src/utils/transactionRouter/index.ts b/src/utils/transactionRouter/index.ts index bf79e27..9ada579 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 TeleportTransfer from "./teleportTransfer"; import TransferAsset from "./transferAsset"; import { Fungible, Receiver, Sender } from "./types"; import IdentityContract from "../../../types/contracts/identity"; +import { AccountType } from "../../../types/types-arguments/identity"; +import { TeleportableRoute, teleportableRoutes } from "../../../teleportableRoutes"; +import { getParaId } from ".."; // Responsible for handling all the transfer logic. // @@ -55,12 +59,26 @@ 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 teleportableRoute: TeleportableRoute = { + relayChain: process.env.RELAY_CHAIN ? process.env.RELAY_CHAIN : "", + destParaId: destParaId, + originParaId: originParaId, + xcAsset: asset.multiAsset + }; + + // if (TeleportableRoutes.indexOf()) + // 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 +87,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 +97,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 +125,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..8b0b345 100644 --- a/src/utils/transactionRouter/reserveTransfer.test.ts +++ b/src/utils/transactionRouter/reserveTransfer.test.ts @@ -8,6 +8,7 @@ import { cryptoWaitReady } from "@polkadot/util-crypto"; import ReserveTransfer from "./reserveTransfer"; import { Fungible, Receiver } from "./types"; import { AccountType } from "../../../types/types-arguments/identity"; +import { getDestination, getMultiAsset, getTransferBeneficiary } from "."; const sr25519Keyring = new Keyring({ type: "sr25519" }); const ecdsaKeyring = new Keyring({ type: "ecdsa" }); @@ -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: { diff --git a/src/utils/transactionRouter/reserveTransfer.ts b/src/utils/transactionRouter/reserveTransfer.ts index e17fbb5..9073a3c 100644 --- a/src/utils/transactionRouter/reserveTransfer.ts +++ b/src/utils/transactionRouter/reserveTransfer.ts @@ -3,7 +3,7 @@ import { KeyringPair } from "@polkadot/keyring/types"; import { Fungible, Receiver } from "./types"; import { getParaId } from ".."; -import { AccountType } from "../../../types/types-arguments/identity"; +import { getDestination, getMultiAsset, getReceiverAccount, getTransferBeneficiary } from "."; class ReserveTransfer { // Transfers assets from the sender to the receiver. @@ -11,22 +11,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 +53,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 +86,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; @@ -293,7 +278,7 @@ class ReserveTransfer { const beneficiary = { parents: 0, interior: { - X1: this.getReceiverAccount(receiver) + X1: getReceiverAccount(receiver) } }; @@ -326,92 +311,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..4890b79 --- /dev/null +++ b/src/utils/transactionRouter/teleportTransfer.ts @@ -0,0 +1,53 @@ +import { ApiPromise } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { Fungible, Receiver } from "./types"; +import { getParaId } from ".."; +import { getDestination, getMultiAsset, getTransferBeneficiary } from "."; + +class TeleportTransfer { + public static async send( + originApi: ApiPromise, + destinationApi: 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(destinationApi); + + 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/teleportableRoutes.ts b/teleportableRoutes.ts new file mode 100644 index 0000000..12f9c42 --- /dev/null +++ b/teleportableRoutes.ts @@ -0,0 +1,30 @@ +// File containing all the possible assets on all possible routes that support asset +// teleportation. + +export type TeleportableRoute = { + relayChain: string, + originParaId: number, + destParaId: number, + xcAsset: string +}; + +export const teleportableRoutes: TeleportableRoute[] = [ + { + relayChain: "polkadot", + originParaId: 0, + destParaId: 1000, + xcAsset: "[{\"network\":\"polkadot\"},\"here\"]" + }, + { + relayChain: "kusama", + originParaId: 0, + destParaId: 1000, + xcAsset: "[{\"network\":\"kusama\"},\"here\"]" + }, + { + relayChain: "rococo", + originParaId: 0, + destParaId: 1000, + xcAsset: "[{\"network\":\"kusama\"},\"here\"]" + } +]; From 5ad8b1ca3281effb80436aa2a25c0b5b26376f56 Mon Sep 17 00:00:00 2001 From: Sergej Sakac Date: Fri, 25 Aug 2023 14:01:53 +0200 Subject: [PATCH 2/4] remove --- routes.json | 9 ---- scripts/index.ts | 121 ----------------------------------------------- 2 files changed, 130 deletions(-) delete mode 100644 routes.json delete mode 100644 scripts/index.ts diff --git a/routes.json b/routes.json deleted file mode 100644 index 11720dd..0000000 --- a/routes.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "xcAsset": "[{\"network\":\"polkadot\"},\"here\"]", - "originParaId": 0, - "destParaId": 1000, - "outcome": "success", - "lastTest": "1692949324972" - } -] \ No newline at end of file diff --git a/scripts/index.ts b/scripts/index.ts deleted file mode 100644 index f92ddfe..0000000 --- a/scripts/index.ts +++ /dev/null @@ -1,121 +0,0 @@ -const axios = require("axios"); - -const API_KEY = "b6c669d34069413ab246c1a03b2f1c12"; -const apiUrl = 'https://polkadot.api.subscan.io/api/scan/extrinsics'; - -const endpoints = [ - 'polkadot.api.subscan.io', - 'kusama.api.subscan.io', - 'darwinia.api.subscan.io', - 'assethub-polkadot.api.subscan.io', - 'assethub-kusama.api.subscan.io', - 'assethub-rococo.api.subscan.io', - 'acala.api.subscan.io', - 'acala-testnet.api.subscan.io', - 'alephzero.api.subscan.io', - 'altair.api.subscan.io', - 'astar.api.subscan.io', - 'bajun.api.subscan.io', - 'basilisk.api.subscan.io', - 'bifrost.api.subscan.io', - 'bifrost-kusama.api.subscan.io', - 'bifrost-testnet.api.subscan.io', - 'calamari.api.subscan.io', - 'centrifuge.api.subscan.io', - 'centrifuge-standalone-history.api.subscan.io', - 'chainx.api.subscan.io', - 'clover.api.subscan.io', - 'clv.api.subscan.io', - 'clover-testnet.api.subscan.io', - 'composable.api.subscan.io', - 'crab.api.subscan.io', - 'crust.api.subscan.io', - 'maxwell.api.subscan.io', - 'shadow.api.subscan.io', - 'dbc.api.subscan.io', - 'dock.api.subscan.io', - 'dolphin.api.subscan.io', - 'edgeware.api.subscan.io', - 'efinity.api.subscan.io', - 'encointer.api.subscan.io', - 'equilibrium.api.subscan.io', - 'genshiro.api.subscan.io', - 'humanode.api.subscan.io', - 'hydradx.api.subscan.io', - 'integritee.api.subscan.io', - 'interlay.api.subscan.io', - 'karura.api.subscan.io', - 'kintsugi.api.subscan.io', - 'khala.api.subscan.io', - 'krest.api.subscan.io', - 'kilt-testnet.api.subscan.io', - 'spiritnet.api.subscan.io', - 'litmus.api.subscan.io', - 'mangatax.api.subscan.io', - 'moonbase.api.subscan.io', - 'moonbeam.api.subscan.io', - 'moonriver.api.subscan.io', - 'nodle.api.subscan.io', - 'origintrail.api.subscan.io', - 'origintrail-testnet.api.subscan.io', - 'pangolin.api.subscan.io', - 'pangolin-parachain.api.subscan.io', - 'pangoro.api.subscan.io', - 'parallel.api.subscan.io', - 'parallel-heiko.api.subscan.io', - 'peaq-testnet.api.subscan.io', - 'phala.api.subscan.io', - 'picasso.api.subscan.io', - 'picasso-rococo.api.subscan.io', - 'pioneer.api.subscan.io', - 'polkadex.api.subscan.io', - 'polymesh.api.subscan.io', - 'polymesh-testnet.api.subscan.io', - 'plasm.api.subscan.io', - 'quartz.api.subscan.io', - 'reef.api.subscan.io', - 'robonomics.api.subscan.io', - 'rococo.api.subscan.io', - 'sakura.api.subscan.io', - 'shibuya.api.subscan.io', - 'shiden.api.subscan.io', - 'sora.api.subscan.io', - 'subspace.api.subscan.io', - 'stafi.api.subscan.io', - 'datahighway.api.subscan.io', - 'turing.api.subscan.io', - 'unique.api.subscan.io', - 'vara.api.subscan.io', - 'westend.api.subscan.io', - 'zeitgeist.api.subscan.io' -] - -const requestData = { - row: 50, - page: 0, - module: 'polkadotXcm', - call: 'execute', - // success: true -}; - -const headers = { - 'Content-Type': 'application/json', - 'X-API-Key': API_KEY -}; - -endpoints.forEach((endpoint) => { - const url = `https://${endpoint}/api/scan/extrinsics`; - - console.log(url); - axios.post(url, requestData, { headers }) - .then((response: any) => { - console.log('Response:', response.data); - if (response.data.data.count > 0) { - console.log(endpoint); - console.log(response.data.data.extrinsics); - } - }) - .catch((_error: any) => { - //console.error('Error:', _error); - }); -}); From bcc0d91a3a36c13974c119174a9080f2b3ab1fd0 Mon Sep 17 00:00:00 2001 From: Sergej Sakac Date: Fri, 25 Aug 2023 18:15:10 +0200 Subject: [PATCH 3/4] teleport works & tested --- __tests__/teleport.test.ts | 29 +++++++++++++++---- src/utils/index.ts | 2 +- src/utils/transactionRouter/index.ts | 16 ++++++---- .../transactionRouter/reserveTransfer.test.ts | 4 +-- .../transactionRouter/reserveTransfer.ts | 4 +-- .../transactionRouter/teleportTransfer.ts | 6 ++-- .../transactionRouter/teleportableRoutes.ts | 21 ++++++++++---- 7 files changed, 57 insertions(+), 25 deletions(-) rename teleportableRoutes.ts => src/utils/transactionRouter/teleportableRoutes.ts (52%) diff --git a/__tests__/teleport.test.ts b/__tests__/teleport.test.ts index 4edfac7..62ae7a0 100644 --- a/__tests__/teleport.test.ts +++ b/__tests__/teleport.test.ts @@ -72,14 +72,15 @@ describe("TransactionRouter Cross-chain teleport", () => { provider: assetHubProvider, }); - const receiverBalanceBefore = await getBalance(rococoApi, bob.address); - const senderBalanceBefore = await getBalance(assetHubApi, alice.address); + const senderBalanceBefore = await getBalance(rococoApi, alice.address); + const receiverBalanceBefore = await getBalance(assetHubApi, bob.address); - const amount = 4000000000000; + const amount = 4 * Math.pow(10, 12); // 4 KSM const assetReserveChainId = 0; const asset: Fungible = { multiAsset: { + parents: 0, interior: "Here" }, amount @@ -92,7 +93,21 @@ describe("TransactionRouter Cross-chain teleport", () => { 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 ( @@ -106,9 +121,11 @@ const addNetwork = async ( }; const getBalance = async (api: ApiPromise, who: string): Promise => { - const maybeBalance: any = (await api.query.system.account(who)).toJSON(); + const maybeBalance: any = (await api.query.system.account(who)).toPrimitive(); if (maybeBalance && maybeBalance.data) { return maybeBalance.data.free; } return 0; -} \ No newline at end of file +} + +const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); diff --git a/src/utils/index.ts b/src/utils/index.ts index fe746e7..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; } } diff --git a/src/utils/transactionRouter/index.ts b/src/utils/transactionRouter/index.ts index 9ada579..f4be613 100644 --- a/src/utils/transactionRouter/index.ts +++ b/src/utils/transactionRouter/index.ts @@ -6,7 +6,7 @@ import TransferAsset from "./transferAsset"; import { Fungible, Receiver, Sender } from "./types"; import IdentityContract from "../../../types/contracts/identity"; import { AccountType } from "../../../types/types-arguments/identity"; -import { TeleportableRoute, teleportableRoutes } from "../../../teleportableRoutes"; +import { TeleportableRoute, teleportableRoutes } from "./teleportableRoutes"; import { getParaId } from ".."; // Responsible for handling all the transfer logic. @@ -64,14 +64,18 @@ class TransactionRouter { const originParaId = await getParaId(originApi); const destParaId = await getParaId(destApi); - const teleportableRoute: TeleportableRoute = { - relayChain: process.env.RELAY_CHAIN ? process.env.RELAY_CHAIN : "", - destParaId: destParaId, + const maybeTeleportableRoute: TeleportableRoute = { + relayChain: process.env.RELAY_CHAIN ? process.env.RELAY_CHAIN : "rococo", originParaId: originParaId, - xcAsset: asset.multiAsset + destParaId: destParaId, + multiAsset: asset.multiAsset }; - // if (TeleportableRoutes.indexOf()) + 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 diff --git a/src/utils/transactionRouter/reserveTransfer.test.ts b/src/utils/transactionRouter/reserveTransfer.test.ts index 8b0b345..4db39b4 100644 --- a/src/utils/transactionRouter/reserveTransfer.test.ts +++ b/src/utils/transactionRouter/reserveTransfer.test.ts @@ -601,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, @@ -961,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 9073a3c..1d875ec 100644 --- a/src/utils/transactionRouter/reserveTransfer.ts +++ b/src/utils/transactionRouter/reserveTransfer.ts @@ -19,7 +19,7 @@ class ReserveTransfer { // eslint-disable-next-line no-prototype-builtins const isOriginPara = originApi.query.hasOwnProperty("parachainInfo"); - const destination = getDestination(isOriginPara, destParaId, destParaId >= 0); + const destination = getDestination(isOriginPara, destParaId, destParaId > 0); const beneficiary = getTransferBeneficiary(receiver); const multiAsset = getMultiAsset(asset); @@ -195,7 +195,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 { diff --git a/src/utils/transactionRouter/teleportTransfer.ts b/src/utils/transactionRouter/teleportTransfer.ts index 4890b79..fbc0d9c 100644 --- a/src/utils/transactionRouter/teleportTransfer.ts +++ b/src/utils/transactionRouter/teleportTransfer.ts @@ -7,7 +7,7 @@ import { getDestination, getMultiAsset, getTransferBeneficiary } from "."; class TeleportTransfer { public static async send( originApi: ApiPromise, - destinationApi: ApiPromise, + destApi: ApiPromise, sender: KeyringPair, receiver: Receiver, asset: Fungible @@ -21,9 +21,9 @@ class TeleportTransfer { // eslint-disable-next-line no-prototype-builtins const isOriginPara = originApi.query.hasOwnProperty("parachainInfo"); - const destParaId = await getParaId(destinationApi); + const destParaId = await getParaId(destApi); - const destination = getDestination(isOriginPara, destParaId, destParaId >= 0); + const destination = getDestination(isOriginPara, destParaId, destParaId > 0); const beneficiary = getTransferBeneficiary(receiver); const multiAsset = getMultiAsset(asset); diff --git a/teleportableRoutes.ts b/src/utils/transactionRouter/teleportableRoutes.ts similarity index 52% rename from teleportableRoutes.ts rename to src/utils/transactionRouter/teleportableRoutes.ts index 12f9c42..55e2625 100644 --- a/teleportableRoutes.ts +++ b/src/utils/transactionRouter/teleportableRoutes.ts @@ -1,11 +1,13 @@ // 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, - xcAsset: string + multiAsset: any }; export const teleportableRoutes: TeleportableRoute[] = [ @@ -13,18 +15,27 @@ export const teleportableRoutes: TeleportableRoute[] = [ relayChain: "polkadot", originParaId: 0, destParaId: 1000, - xcAsset: "[{\"network\":\"polkadot\"},\"here\"]" + multiAsset: AssetRegistry.xcmInteriorToMultiAsset( + JSON.parse('[{"network":"polkadot"},"here"]'), + false, + ), }, { relayChain: "kusama", originParaId: 0, destParaId: 1000, - xcAsset: "[{\"network\":\"kusama\"},\"here\"]" + multiAsset: AssetRegistry.xcmInteriorToMultiAsset( + JSON.parse('[{"network":"kusama"},"here"]'), + false, + ), }, { relayChain: "rococo", originParaId: 0, destParaId: 1000, - xcAsset: "[{\"network\":\"kusama\"},\"here\"]" - } + multiAsset: AssetRegistry.xcmInteriorToMultiAsset( + JSON.parse('[{"network":"rocooc"},"here"]'), + false, + ), + }, ]; From 0003e171b4c1d203ad01653ddf0f79056e00bf78 Mon Sep 17 00:00:00 2001 From: Sergej Sakac Date: Fri, 25 Aug 2023 20:05:08 +0200 Subject: [PATCH 4/4] make eslint happy --- src/utils/transactionRouter/index.ts | 4 ++-- src/utils/transactionRouter/reserveTransfer.test.ts | 2 +- src/utils/transactionRouter/reserveTransfer.ts | 3 +-- src/utils/transactionRouter/teleportTransfer.ts | 3 ++- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/transactionRouter/index.ts b/src/utils/transactionRouter/index.ts index f4be613..5378796 100644 --- a/src/utils/transactionRouter/index.ts +++ b/src/utils/transactionRouter/index.ts @@ -1,13 +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"; -import { TeleportableRoute, teleportableRoutes } from "./teleportableRoutes"; -import { getParaId } from ".."; // Responsible for handling all the transfer logic. // diff --git a/src/utils/transactionRouter/reserveTransfer.test.ts b/src/utils/transactionRouter/reserveTransfer.test.ts index 4db39b4..0734a67 100644 --- a/src/utils/transactionRouter/reserveTransfer.test.ts +++ b/src/utils/transactionRouter/reserveTransfer.test.ts @@ -5,10 +5,10 @@ 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"; -import { getDestination, getMultiAsset, getTransferBeneficiary } from "."; const sr25519Keyring = new Keyring({ type: "sr25519" }); const ecdsaKeyring = new Keyring({ type: "ecdsa" }); diff --git a/src/utils/transactionRouter/reserveTransfer.ts b/src/utils/transactionRouter/reserveTransfer.ts index 1d875ec..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 { Fungible, Receiver } from "./types"; -import { getParaId } from ".."; import { getDestination, getMultiAsset, getReceiverAccount, getTransferBeneficiary } from "."; +import { Fungible, Receiver } from "./types"; class ReserveTransfer { // Transfers assets from the sender to the receiver. diff --git a/src/utils/transactionRouter/teleportTransfer.ts b/src/utils/transactionRouter/teleportTransfer.ts index fbc0d9c..d15a41d 100644 --- a/src/utils/transactionRouter/teleportTransfer.ts +++ b/src/utils/transactionRouter/teleportTransfer.ts @@ -1,8 +1,9 @@ import { ApiPromise } from "@polkadot/api"; import { KeyringPair } from "@polkadot/keyring/types"; + +import { getDestination, getMultiAsset, getTransferBeneficiary } from "."; import { Fungible, Receiver } from "./types"; import { getParaId } from ".."; -import { getDestination, getMultiAsset, getTransferBeneficiary } from "."; class TeleportTransfer { public static async send(