Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cross-chain: Teleport #62

Merged
merged 4 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";
cuteolaf marked this conversation as resolved.
Show resolved Hide resolved
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