Skip to content

Commit

Permalink
Cross-chain: Teleport (#62)
Browse files Browse the repository at this point in the history
* Cross-chain: Teleport

* remove

* teleport works & tested

* make eslint happy
  • Loading branch information
Szegoo committed Aug 25, 2023
1 parent b2a5e44 commit c62c987
Show file tree
Hide file tree
Showing 8 changed files with 369 additions and 147 deletions.
6 changes: 3 additions & 3 deletions __tests__/crossChainRouter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down
131 changes: 131 additions & 0 deletions __tests__/teleport.test.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> => {
await contract
.withSigner(signer)
.tx.addNetwork(network);
};

const getBalance = async (api: ApiPromise, who: string): Promise<any> => {
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));
4 changes: 2 additions & 2 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ export const getParaId = async (api: ApiPromise): Promise<number> => {
const response = (await api.query.parachainInfo.parachainId()).toJSON();
return Number(response);
} else {
return -1;
return 0;
}
}
}
119 changes: 115 additions & 4 deletions src/utils/transactionRouter/index.ts
Original file line number Diff line number Diff line change
@@ -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.
//
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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");
}
}
Loading

0 comments on commit c62c987

Please sign in to comment.