diff --git a/integration-tests/chopsticks/.papi/polkadot-api.json b/integration-tests/chopsticks/.papi/polkadot-api.json index 5eeeb6cdd..11a8fad67 100644 --- a/integration-tests/chopsticks/.papi/polkadot-api.json +++ b/integration-tests/chopsticks/.papi/polkadot-api.json @@ -7,12 +7,12 @@ "metadata": ".papi/metadata/polimec.scale" }, "polkadot": { - "wsUrl": "ws://localhost:8002", + "wsUrl": "wss://rpc.ibp.network/polkadot", "metadata": ".papi/metadata/polkadot.scale" }, "pah": { - "wsUrl": "ws://localhost:8001", + "wsUrl": "wss://sys.ibp.network/statemint", "metadata": ".papi/metadata/pah.scale" } } -} +} \ No newline at end of file diff --git a/integration-tests/chopsticks/README.md b/integration-tests/chopsticks/README.md index 0ec45cfb1..8f0d3d2ab 100644 --- a/integration-tests/chopsticks/README.md +++ b/integration-tests/chopsticks/README.md @@ -6,6 +6,10 @@ To install dependencies: bun install ``` +```bash +bun papi +``` + To start the chains: ```bash diff --git a/integration-tests/chopsticks/bun.lockb b/integration-tests/chopsticks/bun.lockb old mode 100644 new mode 100755 index 56471c648..a421c147c Binary files a/integration-tests/chopsticks/bun.lockb and b/integration-tests/chopsticks/bun.lockb differ diff --git a/integration-tests/chopsticks/overrides/polimec.ts b/integration-tests/chopsticks/overrides/polimec.ts index 686ea3260..507470cc4 100644 --- a/integration-tests/chopsticks/overrides/polimec.ts +++ b/integration-tests/chopsticks/overrides/polimec.ts @@ -1,9 +1,48 @@ -import { INITIAL_BALANCES } from '@/constants'; -import { Accounts, Assets } from '@/types'; +import { INITIAL_BALANCES, WETH_ADDRESS } from '@/constants'; +import { Accounts } from '@/types'; export const POLIMEC_WASM = '../../target/release/wbuild/polimec-runtime/polimec_runtime.compact.compressed.wasm'; +const usdc_location = { + parents: 1, + interior: { + x3: [{ parachain: 1000 }, { palletInstance: 50 }, { generalIndex: 1337 }], + }, +}; +const usdt_location = { + parents: 1, + interior: { + x3: [{ parachain: 1000 }, { palletInstance: 50 }, { generalIndex: 1984 }], + }, +}; +const dot_location = { + parents: 1, + interior: { + here: undefined, + }, +}; + +export const weth_location = { + parents: 2, + interior: { + x2: [ + { + globalConsensus: { + ethereum: { + chainId: 1n, + }, + }, + }, + { + accountKey20: { + key: WETH_ADDRESS, + }, + }, + ], + }, +}; + export const polimec_storage = { System: { Account: [ @@ -21,23 +60,51 @@ export const polimec_storage = { ForeignAssets: { Account: [ [ - [Assets.USDC, Accounts.BOB], + [usdc_location, Accounts.BOB], { balance: INITIAL_BALANCES.USDC, }, ], [ - [Assets.USDT, Accounts.BOB], + [usdt_location, Accounts.BOB], { balance: INITIAL_BALANCES.USDT, }, ], [ - [Assets.DOT, Accounts.BOB], + [dot_location, Accounts.BOB], { balance: INITIAL_BALANCES.DOT, }, ], + [ + [weth_location, Accounts.BOB], + { + balance: INITIAL_BALANCES.WETH, + }, + ], + ], + Asset: [ + [ + [weth_location], + { + owner: Accounts.ALICE, + issuer: Accounts.ALICE, + admin: Accounts.ALICE, + freezer: Accounts.ALICE, + supply: 100n * INITIAL_BALANCES.WETH, + deposit: 0n, + min_balance: 15000000000000n, + is_sufficient: true, + accounts: 1, + sufficients: 1, + approvals: 0, + status: 'Live', + }, + ], + ], + Metadata: [ + [[weth_location], { symbol: 'Wrapped Ether', name: 'WETH', decimals: 18, isFrozen: false }], ], }, } as const; diff --git a/integration-tests/chopsticks/overrides/polkadot-hub.ts b/integration-tests/chopsticks/overrides/polkadot-hub.ts index 9fb9a8a41..023242a63 100644 --- a/integration-tests/chopsticks/overrides/polkadot-hub.ts +++ b/integration-tests/chopsticks/overrides/polkadot-hub.ts @@ -1,5 +1,6 @@ import { INITIAL_BALANCES } from '@/constants'; -import { Accounts, Assets } from '@/types'; +import { Accounts, Asset } from '@/types';import { weth_location } from './polimec'; +; export const polkadot_hub_storage = { System: { @@ -18,24 +19,20 @@ export const polkadot_hub_storage = { Assets: { Account: [ [ - [Assets.USDT, Accounts.ALICE], + [Asset.USDT, Accounts.ALICE], { balance: INITIAL_BALANCES.USDT, }, ], [ - [Assets.USDC, Accounts.ALICE], + [Asset.USDC, Accounts.ALICE], { balance: INITIAL_BALANCES.USDC, }, ], - [ - [Assets.UNKNOWN, Accounts.ALICE], - { - balance: INITIAL_BALANCES.USDT, - }, - ], ], }, - // TODO: Add the foreignAssets storage to give to ALICE WETH = INITIAL_BALANCES.WETH + ForeignAssets: { + Account: [[[weth_location, Accounts.ALICE], { balance: INITIAL_BALANCES.WETH }]], + }, } as const; diff --git a/integration-tests/chopsticks/package.json b/integration-tests/chopsticks/package.json index ccff3ea5c..9a0c09ebd 100644 --- a/integration-tests/chopsticks/package.json +++ b/integration-tests/chopsticks/package.json @@ -13,11 +13,10 @@ "@types/bun": "latest" }, "peerDependencies": { - "typescript": "^5.0.0" + "typescript": "^5.7.3" }, "dependencies": { "@polkadot-api/descriptors": "file:.papi/descriptors", - "@polkadot/keyring": "13.2.3", - "polkadot-api": "^1.7.7" + "polkadot-api": "^1.8.2" } } diff --git a/integration-tests/chopsticks/src/constants.ts b/integration-tests/chopsticks/src/constants.ts index 4c8b2f611..331a98125 100644 --- a/integration-tests/chopsticks/src/constants.ts +++ b/integration-tests/chopsticks/src/constants.ts @@ -18,3 +18,5 @@ export const DERIVE_PATHS = { [Accounts.ALICE]: '//Alice', [Accounts.BOB]: '//Bob', }; + +export const WETH_ADDRESS = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" \ No newline at end of file diff --git a/integration-tests/chopsticks/src/managers/BaseManager.ts b/integration-tests/chopsticks/src/managers/BaseManager.ts index 81a4a7669..7a1ebca39 100644 --- a/integration-tests/chopsticks/src/managers/BaseManager.ts +++ b/integration-tests/chopsticks/src/managers/BaseManager.ts @@ -1,5 +1,12 @@ import { DERIVE_PATHS } from '@/constants'; -import type { Accounts, ChainClient, ChainToDefinition, Chains } from '@/types'; +import type { + Accounts, + Asset, + AssetSourceRelation, + ChainClient, + ChainToDefinition, + Chains, +} from '@/types'; import { sr25519CreateDerive } from '@polkadot-labs/hdkd'; import { DEV_PHRASE, entropyToMiniSecret, mnemonicToEntropy } from '@polkadot-labs/hdkd-helpers'; import type { PolkadotSigner, TypedApi } from 'polkadot-api'; @@ -67,19 +74,15 @@ export abstract class BaseChainManager { return events[0]?.payload.actual_fee || 0n; } - async getNativeBalanceOf(account: Accounts) { - const api = this.getApi(this.getChainType()); - const balance = await api.query.System.Account.getValue(account); - return balance.data.free; - } + abstract getAssetSourceRelation(asset: Asset): AssetSourceRelation; + + abstract getAssetBalanceOf(account: Accounts, asset: Asset): Promise; // @ts-expect-error - TODO: Not sure which is the correct type for this abstract getXcmPallet(); abstract getChainType(): Chains; - abstract getAssetBalanceOf(account: Accounts, asset: number): Promise; - abstract connect(): void; abstract disconnect(): void; diff --git a/integration-tests/chopsticks/src/managers/PolimecManager.ts b/integration-tests/chopsticks/src/managers/PolimecManager.ts index 303c86939..ebf41407f 100644 --- a/integration-tests/chopsticks/src/managers/PolimecManager.ts +++ b/integration-tests/chopsticks/src/managers/PolimecManager.ts @@ -1,12 +1,14 @@ -import { type Accounts, type Assets, Chains } from '@/types'; +import { type Accounts, Asset, AssetLocation, AssetSourceRelation, Chains } from '@/types'; +import { flatObject } from '@/utils.ts'; import { polimec } from '@polkadot-api/descriptors'; import { createClient } from 'polkadot-api'; +import { withPolkadotSdkCompat } from 'polkadot-api/polkadot-sdk-compat'; import { getWsProvider } from 'polkadot-api/ws-provider/web'; import { BaseChainManager } from './BaseManager'; export class PolimecManager extends BaseChainManager { connect() { - const client = createClient(getWsProvider(this.getChainType())); + const client = createClient(withPolkadotSdkCompat(getWsProvider(this.getChainType()))); const api = client.getTypedApi(polimec); // Verify connection @@ -34,13 +36,37 @@ export class PolimecManager extends BaseChainManager { return '58kXueYKLr5b8yCeY3Gd1nLQX2zSJLXjfMzTAuksNq25CFEL' as Accounts; } - async getAssetBalanceOf(account: Accounts, asset: Assets) { + getAssetSourceRelation(asset: Asset): AssetSourceRelation { + switch (asset) { + case Asset.DOT: + return AssetSourceRelation.Parent; + case Asset.USDT: + return AssetSourceRelation.Sibling; + case Asset.USDC: + return AssetSourceRelation.Sibling; + case Asset.WETH: + // Placeholder + return AssetSourceRelation.Self; + } + } + + async getAssetBalanceOf(account: Accounts, asset: Asset): Promise { const api = this.getApi(Chains.Polimec); - const balance = await api.query.ForeignAssets.Account.getValue(asset, account); - return balance?.balance || 0n; + const asset_source_relation = this.getAssetSourceRelation(asset); + const asset_location = AssetLocation(asset, asset_source_relation).value; + const account_balances_result = await api.apis.FungiblesApi.query_account_balances(account); + if (account_balances_result.success === true && account_balances_result.value.type === 'V4') { + const assets = account_balances_result.value.value; + for (const asset of assets) { + if (Bun.deepEquals(flatObject(asset.id), flatObject(asset_location))) { + return asset.fun.value as bigint; + } + } + } + return 0n; } - async getXcmFee() { + async getLocalXcmFee() { const api = this.getApi(Chains.Polimec); const events = await api.event.PolkadotXcm.FeesPaid.pull(); if (!events.length) return 0n; diff --git a/integration-tests/chopsticks/src/managers/PolkadotHubManager.ts b/integration-tests/chopsticks/src/managers/PolkadotHubManager.ts index 1e889e2d8..ceb1ccab4 100644 --- a/integration-tests/chopsticks/src/managers/PolkadotHubManager.ts +++ b/integration-tests/chopsticks/src/managers/PolkadotHubManager.ts @@ -1,12 +1,14 @@ -import { type Accounts, type Assets, Chains } from '@/types'; +import { type Accounts, Asset, AssetLocation, AssetSourceRelation, Chains } from '@/types'; +import { flatObject } from '@/utils.ts'; import { pah } from '@polkadot-api/descriptors'; import { createClient } from 'polkadot-api'; +import { withPolkadotSdkCompat } from 'polkadot-api/polkadot-sdk-compat'; import { getWsProvider } from 'polkadot-api/ws-provider/web'; import { BaseChainManager } from './BaseManager'; export class PolkadotHubManager extends BaseChainManager { connect() { - const client = createClient(getWsProvider(this.getChainType())); + const client = createClient(withPolkadotSdkCompat(getWsProvider(this.getChainType()))); const api = client.getTypedApi(pah); // Verify connection @@ -30,10 +32,35 @@ export class PolkadotHubManager extends BaseChainManager { return api.tx.PolkadotXcm; } - async getAssetBalanceOf(account: Accounts, asset: Assets) { + getAssetSourceRelation(asset: Asset): AssetSourceRelation { + switch (asset) { + case Asset.DOT: + return AssetSourceRelation.Parent; + case Asset.USDT: + return AssetSourceRelation.Self; + case Asset.USDC: + return AssetSourceRelation.Self; + case Asset.WETH: + // This is not actually used, so we use Self as a placeholder + return AssetSourceRelation.Self; + } + } + + async getAssetBalanceOf(account: Accounts, asset: Asset): Promise { const api = this.getApi(Chains.PolkadotHub); - const balance = await api.query.Assets.Account.getValue(asset, account); - return balance?.balance || 0n; + const asset_source_relation = this.getAssetSourceRelation(asset); + const asset_location = AssetLocation(asset, asset_source_relation).value; + const account_balances_result = await api.apis.FungiblesApi.query_account_balances(account); + + if (account_balances_result.success === true && account_balances_result.value.type === 'V4') { + const assets = account_balances_result.value.value; + for (const asset of assets) { + if (Bun.deepEquals(flatObject(asset.id), flatObject(asset_location))) { + return asset.fun.value as bigint; + } + } + } + return 0n; } async getSwapCredit() { @@ -47,4 +74,10 @@ export class PolkadotHubManager extends BaseChainManager { const events = await api.event.PolkadotXcm.FeesPaid.pull(); return (events[0]?.payload.fees[0].fun.value as bigint) || 0n; } + + async getTransactionFee() { + const api = this.getApi(Chains.PolkadotHub); + const events = await api.event.TransactionPayment.TransactionFeePaid.pull(); + return (events[0]?.payload.actual_fee as bigint) || 0n; + } } diff --git a/integration-tests/chopsticks/src/managers/PolkadotManager.ts b/integration-tests/chopsticks/src/managers/PolkadotManager.ts index 92beee323..c611025cb 100644 --- a/integration-tests/chopsticks/src/managers/PolkadotManager.ts +++ b/integration-tests/chopsticks/src/managers/PolkadotManager.ts @@ -1,12 +1,13 @@ -import { type Accounts, Chains } from '@/types'; +import { type Accounts, Asset, AssetSourceRelation, Chains } from '@/types'; import { polkadot } from '@polkadot-api/descriptors'; import { createClient } from 'polkadot-api'; +import { withPolkadotSdkCompat } from 'polkadot-api/polkadot-sdk-compat'; import { getWsProvider } from 'polkadot-api/ws-provider/web'; import { BaseChainManager } from './BaseManager'; export class PolkadotManager extends BaseChainManager { connect() { - const client = createClient(getWsProvider(this.getChainType())); + const client = createClient(withPolkadotSdkCompat(getWsProvider(this.getChainType()))); const api = client.getTypedApi(polkadot); // Verify connection @@ -30,8 +31,29 @@ export class PolkadotManager extends BaseChainManager { return api.tx.XcmPallet; } - async getAssetBalanceOf(_account: Accounts, _asset: number): Promise { - throw new Error('Polkadot does not support assets'); + getAssetSourceRelation(asset: Asset): AssetSourceRelation { + switch (asset) { + case Asset.DOT: + return AssetSourceRelation.Self; + case Asset.USDT: + // Placeholder + return AssetSourceRelation.Self; + case Asset.USDC: + // Placeholder + return AssetSourceRelation.Self; + case Asset.WETH: + // Placeholder + return AssetSourceRelation.Self; + } + } + + async getAssetBalanceOf(account: Accounts, asset: Asset): Promise { + const api = this.getApi(this.getChainType()); + if (asset === Asset.DOT) { + const balance = await api.query.System.Account.getValue(account); + return balance.data.free; + } + return 0n; } async getXcmFee() { @@ -39,4 +61,10 @@ export class PolkadotManager extends BaseChainManager { const events = await api.event.XcmPallet.FeesPaid.pull(); return (events[0]?.payload.fees[0].fun.value as bigint) || 0n; } + + async getTransactionFee() { + const api = this.getApi(Chains.Polkadot); + const events = await api.event.TransactionPayment.TransactionFeePaid.pull(); + return (events[0]?.payload.actual_fee as bigint) || 0n; + } } diff --git a/integration-tests/chopsticks/src/setup.ts b/integration-tests/chopsticks/src/setup.ts index 5f2119656..ba7413117 100644 --- a/integration-tests/chopsticks/src/setup.ts +++ b/integration-tests/chopsticks/src/setup.ts @@ -1,3 +1,4 @@ +import { expect } from 'bun:test'; import { setupWithServer } from '@acala-network/chopsticks'; import { type Blockchain, @@ -45,6 +46,14 @@ export class ChainSetup { ]); console.log('✅ HRMP channels created'); + + // Needed to execute storage migrations within the new WASM before running tests. + const head = this.polimec.head; + console.log(`✅ Polimec chain is at block ${head.number}`); + console.log('✅ Producing a new block...'); + const new_block = await this.polimec?.newBlock(); + console.log(`✅ Polimec chain is at block ${new_block.number}`); + expect(new_block.number === head.number + 1, 'Block number should be incremented by 1'); } async cleanup() { @@ -60,7 +69,7 @@ export class ChainSetup { private async setupPolimec(polimec_storage: unknown) { const file = Bun.file(POLIMEC_WASM); - // Note: the tests are inteded to use a pre-production, locally compiled runtime, that's why we throw an error. + // Note: the tests are intended to use a pre-production, locally compiled runtime, that's why we throw an error. if (!(await file.exists())) { throw new Error( 'Polimec runtime not found! Please build it by running `cargo b -r -p polimec-runtime` before executing the tests.', diff --git a/integration-tests/chopsticks/src/tests/hub.test.ts b/integration-tests/chopsticks/src/tests/hub.test.ts index 20b9de7b2..df0496d0c 100644 --- a/integration-tests/chopsticks/src/tests/hub.test.ts +++ b/integration-tests/chopsticks/src/tests/hub.test.ts @@ -1,9 +1,9 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test'; +import { afterAll, beforeAll, beforeEach, describe, test } from 'bun:test'; import { TRANSFER_AMOUNTS } from '@/constants'; import { createChainManager } from '@/managers/Factory'; import { ChainSetup } from '@/setup'; import { HubToPolimecTransfer } from '@/transfers/HubToPolimec'; -import { Accounts, Assets, Chains } from '@/types'; +import { Accounts, Asset, AssetSourceRelation, Chains } from '@/types'; describe('Polkadot Hub -> Polimec Transfer Tests', () => { const sourceManager = createChainManager(Chains.PolkadotHub); @@ -18,33 +18,46 @@ describe('Polkadot Hub -> Polimec Transfer Tests', () => { }); afterAll(async () => await chainSetup.cleanup()); - test('Send DOT to Polimec', () => - transferTest.testTransfer({ - amount: TRANSFER_AMOUNTS.NATIVE, - account: Accounts.ALICE, - asset: Assets.DOT, - })); - - test('Send USDt to Polimec', () => - transferTest.testTransfer({ - amount: TRANSFER_AMOUNTS.TOKENS, - account: Accounts.ALICE, - asset: Assets.USDT, - })); + test( + 'Send DOT to Polimec', + () => + transferTest.testTransfer({ + account: Accounts.ALICE, + assets: [[Asset.DOT, TRANSFER_AMOUNTS.NATIVE, AssetSourceRelation.Parent]], + }), + { timeout: 25000 }, + ); - test('Send USDC to Polimec', () => - transferTest.testTransfer({ - amount: TRANSFER_AMOUNTS.TOKENS, - account: Accounts.ALICE, - asset: Assets.USDC, - })); + test( + 'Send USDT to Polimec', + () => + transferTest.testTransfer({ + account: Accounts.ALICE, + assets: [[Asset.USDT, TRANSFER_AMOUNTS.TOKENS, AssetSourceRelation.Self]], + }), + { timeout: 25000 }, + ); - test('Send Unknown Asset to Polimec', () => - expect(() => + test( + 'Send USDC to Polimec', + () => transferTest.testTransfer({ - amount: TRANSFER_AMOUNTS.TOKENS, account: Accounts.ALICE, - asset: Assets.UNKNOWN, + assets: [[Asset.USDC, TRANSFER_AMOUNTS.TOKENS, AssetSourceRelation.Self]], }), - ).toThrow()); + { timeout: 25000 }, + ); + + // test( + // 'Send WETH to Polimec', + // () => + // transferTest.testTransfer({ + // account: Accounts.ALICE, + // assets: [ + // // [Asset.USDT, TRANSFER_AMOUNTS.TOKENS, AssetSourceRelation.Self], + // [Asset.WETH, TRANSFER_AMOUNTS.BRIDGED, AssetSourceRelation.Self], + // ], + // }), + // { timeout: 25000 }, + // ); }); diff --git a/integration-tests/chopsticks/src/tests/polimec.test.ts b/integration-tests/chopsticks/src/tests/polimec.test.ts index 39e989d96..23ad70a4e 100644 --- a/integration-tests/chopsticks/src/tests/polimec.test.ts +++ b/integration-tests/chopsticks/src/tests/polimec.test.ts @@ -4,7 +4,7 @@ import { createChainManager } from '@/managers/Factory'; import { polimec_storage } from '@/polimec'; import { ChainSetup } from '@/setup'; import { PolimecToHubTransfer } from '@/transfers/PolimecToHub'; -import { Accounts, Assets, Chains } from '@/types'; +import { Accounts, Asset, AssetSourceRelation, Chains } from '@/types'; describe('Polimec -> Hub Transfer Tests', () => { const sourceManager = createChainManager(Chains.Polimec); @@ -19,24 +19,33 @@ describe('Polimec -> Hub Transfer Tests', () => { }); afterAll(async () => await chainSetup.cleanup()); - test('Send USDC to Hub', () => - transferTest.testTransfer({ - amount: TRANSFER_AMOUNTS.TOKENS, - account: Accounts.BOB, - asset: Assets.USDC, - })); + test( + 'Send USDC to Hub', + () => + transferTest.testTransfer({ + account: Accounts.BOB, + assets: [[Asset.USDC, TRANSFER_AMOUNTS.TOKENS, AssetSourceRelation.Sibling]], + }), + { timeout: 25000 }, + ); - test('Send USDt to Hub', () => - transferTest.testTransfer({ - amount: TRANSFER_AMOUNTS.TOKENS, - account: Accounts.BOB, - asset: Assets.USDT, - })); + test( + 'Send USDT to Hub', + () => + transferTest.testTransfer({ + account: Accounts.BOB, + assets: [[Asset.USDT, TRANSFER_AMOUNTS.TOKENS, AssetSourceRelation.Sibling]], + }), + { timeout: 25000 }, + ); - test('Send DOT to Hub', () => - transferTest.testTransfer({ - amount: TRANSFER_AMOUNTS.NATIVE, - account: Accounts.BOB, - asset: Assets.DOT, - })); + test( + 'Send DOT to Hub', + () => + transferTest.testTransfer({ + account: Accounts.BOB, + assets: [[Asset.DOT, TRANSFER_AMOUNTS.NATIVE, AssetSourceRelation.Parent]], + }), + { timeout: 25000 }, + ); }); diff --git a/integration-tests/chopsticks/src/tests/polkadot.test.ts b/integration-tests/chopsticks/src/tests/polkadot.test.ts index 9a13531e3..6f9fd2522 100644 --- a/integration-tests/chopsticks/src/tests/polkadot.test.ts +++ b/integration-tests/chopsticks/src/tests/polkadot.test.ts @@ -3,26 +3,31 @@ import { TRANSFER_AMOUNTS } from '@/constants'; import { createChainManager } from '@/managers/Factory'; import { ChainSetup } from '@/setup'; import { PolkadotToPolimecTransfer } from '@/transfers/PolkadotToPolimec'; -import { Accounts, Assets, Chains } from '@/types'; +import { Accounts, Asset, AssetSourceRelation, Chains } from '@/types'; describe('Polkadot -> Polimec Transfer Tests', () => { const chainSetup = new ChainSetup(); const sourceManager = createChainManager(Chains.Polkadot); const destManager = createChainManager(Chains.Polimec); - const transferTest = new PolkadotToPolimecTransfer(sourceManager, destManager); + const hopManager = createChainManager(Chains.PolkadotHub); + const transferTest = new PolkadotToPolimecTransfer(sourceManager, destManager, hopManager); beforeAll(async () => await chainSetup.initialize()); beforeEach(() => { sourceManager.connect(); + hopManager.connect(); destManager.connect(); }); afterAll(async () => await chainSetup.cleanup()); - test('Send DOT to Polimec', () => - transferTest.testTransfer({ - amount: TRANSFER_AMOUNTS.NATIVE, - account: Accounts.ALICE, - asset: Assets.DOT, - })); + test( + 'Send DOT to Polimec, via AH', + () => + transferTest.testTransfer({ + account: Accounts.ALICE, + assets: [[Asset.DOT, TRANSFER_AMOUNTS.NATIVE, AssetSourceRelation.Self]], + }), + { timeout: 25000 }, + ); }); diff --git a/integration-tests/chopsticks/src/transfers/BaseTransfer.ts b/integration-tests/chopsticks/src/transfers/BaseTransfer.ts index 90bfa4aab..7d98074fc 100644 --- a/integration-tests/chopsticks/src/transfers/BaseTransfer.ts +++ b/integration-tests/chopsticks/src/transfers/BaseTransfer.ts @@ -1,44 +1,46 @@ import { expect } from 'bun:test'; import type { BaseChainManager } from '@/managers/BaseManager'; -import type { Accounts, BalanceCheck, TransferResult } from '@/types'; +import type { Accounts, Asset, AssetSourceRelation, BalanceCheck, TransferResult } from '@/types'; +import { sleep } from 'bun'; -export interface BaseTransferOptions { - amount: bigint; +export interface TransferOptions { account: Accounts; + assets: [Asset, bigint, AssetSourceRelation][]; } -export abstract class BaseTransferTest { +export abstract class BaseTransferTest { constructor( protected sourceManager: BaseChainManager, protected destManager: BaseChainManager, ) {} - abstract executeTransfer(options: T): Promise; - abstract getBalances(options: Omit): Promise<{ balances: BalanceCheck }>; + abstract executeTransfer(options: TransferOptions): Promise; + abstract getBalances(options: TransferOptions): Promise<{ asset_balances: BalanceCheck[] }>; abstract verifyFinalBalances( - initialBalances: BalanceCheck, - finalBalances: BalanceCheck, - options: T, - ): Promise; + initialBalances: BalanceCheck[], + finalBalances: BalanceCheck[], + options: TransferOptions, + ): void; - async testTransfer(options: T) { - const { balances: initialBalances } = await this.getBalances(options); + async testTransfer(options: TransferOptions) { + const { asset_balances: initialBalances } = await this.getBalances(options); + if (options.assets[0][1] > initialBalances[0].source) { + throw new Error(`Insufficient balance on Source chain for asset: ${options.assets[0][0]}`); + } const blockNumbers = await this.executeTransfer(options); - await this.waitForBlocks(blockNumbers); + await this.waitForBlocks(blockNumbers.destBlock); await this.verifyExecution(); - const { balances: finalBalances } = await this.getBalances(options); - await this.verifyFinalBalances(initialBalances, finalBalances, options); + const { asset_balances: finalBalances } = await this.getBalances(options); + this.verifyFinalBalances(initialBalances, finalBalances, options); } - protected async waitForBlocks({ sourceBlock, destBlock }: TransferResult) { - await Promise.all([ - this.sourceManager.waitForNextBlock(sourceBlock), - this.destManager.waitForNextBlock(destBlock), - ]); + protected async waitForBlocks(destBlockNumber: number) { + await sleep(2000); } protected async verifyExecution() { const events = await this.destManager.getMessageQueueEvents(); + expect(events).not.toBeEmpty(); expect(events).toBeArray(); expect(events).toHaveLength(1); diff --git a/integration-tests/chopsticks/src/transfers/HubToPolimec.ts b/integration-tests/chopsticks/src/transfers/HubToPolimec.ts index f3793759f..7e8d9c105 100644 --- a/integration-tests/chopsticks/src/transfers/HubToPolimec.ts +++ b/integration-tests/chopsticks/src/transfers/HubToPolimec.ts @@ -1,18 +1,25 @@ import { expect } from 'bun:test'; -import { INITIAL_BALANCES } from '@/constants'; import type { PolimecManager } from '@/managers/PolimecManager'; import type { PolkadotHubManager } from '@/managers/PolkadotHubManager'; -import { type Accounts, Assets, Chains, type PolimecBalanceCheck } from '@/types'; -import { createTransferData } from '@/utils'; -import { BaseTransferTest } from './BaseTransfer'; - -interface HubTransferOptions { - amount: bigint; - account: Accounts; - asset: Assets; -} +import { + Asset, + AssetSourceRelation, + Chains, + ParaId, + type PolimecBalanceCheck, + getVersionedAssets, +} from '@/types'; +import { createTransferData, unwrap } from '@/utils'; +import { + DispatchRawOrigin, + XcmVersionedAssetId, + type XcmVersionedLocation, + type XcmVersionedXcm, +} from '@polkadot-api/descriptors'; + +import { BaseTransferTest, type TransferOptions } from './BaseTransfer'; -export class HubToPolimecTransfer extends BaseTransferTest { +export class HubToPolimecTransfer extends BaseTransferTest { constructor( protected override sourceManager: PolkadotHubManager, protected override destManager: PolimecManager, @@ -20,23 +27,25 @@ export class HubToPolimecTransfer extends BaseTransferTest { super(sourceManager, destManager); } - async executeTransfer({ amount, account, asset }: HubTransferOptions) { + async executeTransfer({ account, assets }: TransferOptions) { const [sourceBlock, destBlock] = await Promise.all([ this.sourceManager.getBlockNumber(), this.destManager.getBlockNumber(), ]); + const versioned_assets = getVersionedAssets(assets); + const data = createTransferData({ - amount, toChain: Chains.Polimec, + assets: versioned_assets, recv: account, - assetIndex: asset === Assets.DOT ? undefined : BigInt(asset), }); const api = this.sourceManager.getApi(Chains.PolkadotHub); - const res = await api.tx.PolkadotXcm.transfer_assets(data).signAndSubmit( - this.sourceManager.getSigner(account), - ); + const transfer = api.tx.PolkadotXcm.transfer_assets(data); + const res = await transfer.signAndSubmit(this.sourceManager.getSigner(account)); + + console.log('Extrinsic result: ', res.ok); expect(res.ok).toBeTrue(); return { sourceBlock, destBlock }; @@ -44,38 +53,123 @@ export class HubToPolimecTransfer extends BaseTransferTest { async getBalances({ account, - asset, - }: Omit): Promise<{ balances: PolimecBalanceCheck }> { - const isNativeTransfer = asset === Assets.DOT; - const treasuryAccount = this.destManager.getTreasuryAccount(); - return { - balances: { - source: isNativeTransfer - ? await this.sourceManager.getNativeBalanceOf(account) - : await this.sourceManager.getAssetBalanceOf(account, asset), + assets, + }: TransferOptions): Promise<{ asset_balances: PolimecBalanceCheck[] }> { + const asset_balances: PolimecBalanceCheck[] = []; + const treasury_account = this.destManager.getTreasuryAccount(); + for (const [asset] of assets) { + const balances: PolimecBalanceCheck = { + source: await this.sourceManager.getAssetBalanceOf(account, asset), destination: await this.destManager.getAssetBalanceOf(account, asset), - treasury: await this.destManager.getAssetBalanceOf(treasuryAccount, asset), - }, - }; + treasury: await this.destManager.getAssetBalanceOf(treasury_account, asset), + }; + asset_balances.push(balances); + } + return { asset_balances }; } async verifyFinalBalances( - initialBalances: PolimecBalanceCheck, - finalBalances: PolimecBalanceCheck, - { asset }: HubTransferOptions, + assetInitialBalances: PolimecBalanceCheck[], + assetFinalBalances: PolimecBalanceCheck[], + transferOptions: TransferOptions, ) { - // TODO: At the moment we exclude fees from the balance check since the PAPI team is wotking on some utilies to calculate fees. - const initialBalance = - asset === Assets.DOT - ? INITIAL_BALANCES.DOT - : asset === Assets.USDT - ? INITIAL_BALANCES.USDT - : INITIAL_BALANCES.USDC; - // Note: Initially every account on destination is empty. - expect(initialBalances.destination).toBe(0n); - expect(initialBalances.source).toBe(initialBalance); - expect(finalBalances.source).toBeLessThan(initialBalances.source); - expect(finalBalances.destination).toBeGreaterThan(initialBalances.destination); - expect(finalBalances.treasury).toBeGreaterThan(initialBalances.treasury); + const native_extrinsic_fee_amount = await this.sourceManager.getTransactionFee(); + const source_xcm_asset_fee_amount = await this.sourceManager.getXcmFee(); + const dest_xcm_asset_fee_amount = await this.calculatePolimecXcmFee(transferOptions); + + const fee_asset = transferOptions.assets[0][0]; + + for (let i = 0; i < transferOptions.assets.length; i++) { + const initialBalances = assetInitialBalances[i]; + const finalBalances = assetFinalBalances[i]; + const send_amount = transferOptions.assets[i][1]; + const asset = transferOptions.assets[i][0]; + + let expectedSourceBalanceSpent = send_amount; + let expectedDestBalanceSpent = 0n; + let expectedTreasuryBalanceGained = 0n; + + if (asset === Asset.DOT) { + expectedSourceBalanceSpent += native_extrinsic_fee_amount + source_xcm_asset_fee_amount; + } + if (asset === fee_asset) { + expectedDestBalanceSpent += dest_xcm_asset_fee_amount; + expectedTreasuryBalanceGained += dest_xcm_asset_fee_amount; + } + + expect(finalBalances.source).toBe(initialBalances.source - expectedSourceBalanceSpent); + expect(finalBalances.destination).toBe( + initialBalances.destination + send_amount - expectedDestBalanceSpent, + ); + expect(finalBalances.treasury).toBe(initialBalances.treasury + expectedTreasuryBalanceGained); + } + } + + async calculatePolimecXcmFee(transferOptions: TransferOptions): Promise { + let destinationExecutionFee: bigint; + + const sourceApi = this.sourceManager.getApi(Chains.PolkadotHub); + const destApi = this.destManager.getApi(Chains.Polimec); + + const versioned_assets = getVersionedAssets(transferOptions.assets); + const transferData = createTransferData({ + toChain: Chains.Polimec, + assets: versioned_assets, + recv: transferOptions.account, + }); + + let remoteFeeAssetId: XcmVersionedAssetId; + const lastAsset = unwrap(transferOptions.assets.at(0)); + if (lastAsset[2] === AssetSourceRelation.Self) { + lastAsset[2] = AssetSourceRelation.Sibling; + } + const versioned_asset = getVersionedAssets([lastAsset]); + if (versioned_asset.type === 'V4') { + remoteFeeAssetId = XcmVersionedAssetId.V4(unwrap(versioned_asset.value.at(0)).id); + } else { + throw new Error('Invalid versioned assets'); + } + + const localDryRunResult = await sourceApi.apis.DryRunApi.dry_run_call( + { type: 'system', value: DispatchRawOrigin.Signed(transferOptions.account) }, + { type: 'PolkadotXcm', value: { type: 'transfer_assets', value: transferData } }, + ); + + let forwardedXcms: [XcmVersionedLocation, XcmVersionedXcm[]][] = []; + if (localDryRunResult.success && localDryRunResult.value.forwarded_xcms) { + forwardedXcms = localDryRunResult.value.forwarded_xcms; + } else { + throw new Error('Dry run failed'); + } + + const xcmsToPolimec = forwardedXcms.find( + ([location, _]) => + location.type === 'V4' && + location.value.parents === 1 && + location.value.interior.type === 'X1' && + location.value.interior.value.type === 'Parachain' && + location.value.interior.value.value === ParaId[Chains.Polimec], + ); + if (!xcmsToPolimec) { + throw new Error('Could not find xcm to polimec'); + } + const messages = xcmsToPolimec[1]; + const remoteXcm = messages[0]; + const remoteXcmWeightResult = await destApi.apis.XcmPaymentApi.query_xcm_weight(remoteXcm); + if (remoteXcmWeightResult.success) { + const remoteExecutionFeesResult = await destApi.apis.XcmPaymentApi.query_weight_to_asset_fee( + remoteXcmWeightResult.value, + remoteFeeAssetId, + ); + if (remoteExecutionFeesResult.success) { + destinationExecutionFee = remoteExecutionFeesResult.value; + } else { + throw new Error('Could not calculate destination xcm fee'); + } + } else { + throw new Error('Could not calculate xcm weight'); + } + + return destinationExecutionFee; } } diff --git a/integration-tests/chopsticks/src/transfers/PolimecToHub.ts b/integration-tests/chopsticks/src/transfers/PolimecToHub.ts index 7d65132f3..c6e347414 100644 --- a/integration-tests/chopsticks/src/transfers/PolimecToHub.ts +++ b/integration-tests/chopsticks/src/transfers/PolimecToHub.ts @@ -2,15 +2,11 @@ import { expect } from 'bun:test'; import { INITIAL_BALANCES } from '@/constants'; import type { PolimecManager } from '@/managers/PolimecManager'; import type { PolkadotHubManager } from '@/managers/PolkadotHubManager'; -import { Assets, type BalanceCheck, Chains } from '@/types'; +import { Asset, type BalanceCheck, Chains, getVersionedAssets } from '@/types'; import { createTransferData } from '@/utils'; -import { type BaseTransferOptions, BaseTransferTest } from './BaseTransfer'; +import { BaseTransferTest, type TransferOptions } from './BaseTransfer'; -interface PolimecTransferOptions extends BaseTransferOptions { - asset: Assets; -} - -export class PolimecToHubTransfer extends BaseTransferTest { +export class PolimecToHubTransfer extends BaseTransferTest { constructor( protected override sourceManager: PolimecManager, protected override destManager: PolkadotHubManager, @@ -18,16 +14,16 @@ export class PolimecToHubTransfer extends BaseTransferTest): Promise<{ balances: BalanceCheck }> { - const isNativeTransfer = asset === Assets.DOT; - return { - balances: { - source: await this.sourceManager.getAssetBalanceOf(account, asset), - destination: isNativeTransfer - ? await this.destManager.getNativeBalanceOf(account) - : await this.destManager.getAssetBalanceOf(account, asset), - }, - }; + async getBalances(options: TransferOptions): Promise<{ asset_balances: BalanceCheck[] }> { + const source = await this.sourceManager.getAssetBalanceOf( + options.account, + options.assets[0][0], + ); + const destination = await this.destManager.getAssetBalanceOf( + options.account, + options.assets[0][0], + ); + return { asset_balances: [{ source, destination }] }; } - async verifyFinalBalances( - initialBalances: BalanceCheck, - finalBalances: BalanceCheck, - { asset }: PolimecTransferOptions, + verifyFinalBalances( + initialBalances: BalanceCheck[], + finalBalances: BalanceCheck[], + options: TransferOptions, ) { // TODO: At the moment we exclude fees from the balance check since the PAPI team is wotking on some utilies to calculate fees. const initialBalance = - asset === Assets.DOT + options.assets[0][0] === Asset.DOT ? INITIAL_BALANCES.DOT - : asset === Assets.USDT + : options.assets[0][0] === Asset.USDT ? INITIAL_BALANCES.USDT : INITIAL_BALANCES.USDC; - - expect(initialBalances.destination).toBe(0n); - expect(initialBalances.source).toBe(initialBalance); - expect(finalBalances.source).toBeLessThan(initialBalances.source); - expect(finalBalances.destination).toBeGreaterThan(initialBalances.destination); + for (let i = 0; i < options.assets.length; i++) { + expect(initialBalances[i].destination).toBe(0n); + expect(initialBalances[i].source).toBe(initialBalance); + expect(finalBalances[i].source).toBeLessThan(initialBalances[i].source); + expect(finalBalances[i].destination).toBeGreaterThan(initialBalances[i].destination); + } } } diff --git a/integration-tests/chopsticks/src/transfers/PolkadotToPolimec.ts b/integration-tests/chopsticks/src/transfers/PolkadotToPolimec.ts index 7e777019f..fbda1dcec 100644 --- a/integration-tests/chopsticks/src/transfers/PolkadotToPolimec.ts +++ b/integration-tests/chopsticks/src/transfers/PolkadotToPolimec.ts @@ -1,33 +1,42 @@ import { expect } from 'bun:test'; import type { PolimecManager } from '@/managers/PolimecManager'; +import type { PolkadotHubManager } from '@/managers/PolkadotHubManager'; import type { PolkadotManager } from '@/managers/PolkadotManager'; -import { Assets, Chains, type PolimecBalanceCheck } from '@/types'; -import { createMultiHopTransferData } from '@/utils'; -import { type BaseTransferOptions, BaseTransferTest } from './BaseTransfer'; +import { + Asset, + AssetSourceRelation, + Chains, + ParaId, + type PolimecBalanceCheck, + getVersionedAssets, +} from '@/types'; +import { abs, createDotMultiHopTransferData, createTransferData, unwrap } from '@/utils'; +import { + DispatchRawOrigin, + XcmVersionedAssetId, + type XcmVersionedLocation, + type XcmVersionedXcm, +} from '@polkadot-api/descriptors'; +import { BaseTransferTest, type TransferOptions } from './BaseTransfer'; -interface PolkadotTransferOptions extends BaseTransferOptions { - asset: Assets.DOT; -} - -export class PolkadotToPolimecTransfer extends BaseTransferTest { +export class PolkadotToPolimecTransfer extends BaseTransferTest { constructor( protected override sourceManager: PolkadotManager, protected override destManager: PolimecManager, + protected hopManager: PolkadotHubManager, ) { super(sourceManager, destManager); } - async executeTransfer({ amount, account }: PolkadotTransferOptions) { + async executeTransfer({ account, assets }: TransferOptions) { const [sourceBlock, destBlock] = await Promise.all([ this.sourceManager.getBlockNumber(), this.destManager.getBlockNumber(), + this.hopManager.getBlockNumber(), ]); - const data = createMultiHopTransferData({ - amount, - toChain: Chains.Polimec, - recv: account, - }); + const amount = assets[0][1]; + const data = createDotMultiHopTransferData(amount); const api = this.sourceManager.getApi(Chains.Polkadot); const res = await api.tx.XcmPallet.transfer_assets_using_type_and_then(data).signAndSubmit( @@ -40,25 +49,136 @@ export class PolkadotToPolimecTransfer extends BaseTransferTest): Promise<{ balances: PolimecBalanceCheck }> { - const treasuryAccount = this.destManager.getTreasuryAccount(); - return { - balances: { - source: await this.sourceManager.getNativeBalanceOf(account), - destination: await this.destManager.getAssetBalanceOf(account, Assets.DOT), - treasury: await this.destManager.getAssetBalanceOf(treasuryAccount, Assets.DOT), - }, - }; + assets, + }: TransferOptions): Promise<{ asset_balances: PolimecBalanceCheck[] }> { + const asset_balances: PolimecBalanceCheck[] = []; + const treasury_account = this.destManager.getTreasuryAccount(); + for (const [asset] of assets) { + const balances: PolimecBalanceCheck = { + source: await this.sourceManager.getAssetBalanceOf(account, asset), + destination: await this.destManager.getAssetBalanceOf(account, asset), + treasury: await this.destManager.getAssetBalanceOf(treasury_account, asset), + }; + asset_balances.push(balances); + } + return { asset_balances }; } + // TODO: This is not accurate at the moment. + // TODO: We should improve the logic to handle the Polkadot -> Hub -> Polimec transfer fee. async verifyFinalBalances( - initialBalances: PolimecBalanceCheck, - finalBalances: PolimecBalanceCheck, + assetInitialBalances: PolimecBalanceCheck[], + assetFinalBalances: PolimecBalanceCheck[], + transferOptions: TransferOptions, ) { - // TODO: At the moment we exclude fees from the balance check since the PAPI team is wotking on some utilies to calculate fees. - expect(initialBalances.destination).toBe(0n); - expect(finalBalances.source).toBeLessThan(initialBalances.source); - expect(finalBalances.destination).toBeGreaterThan(initialBalances.destination); - expect(finalBalances.treasury).toBeGreaterThan(initialBalances.treasury); + const native_extrinsic_fee_amount = await this.sourceManager.getTransactionFee(); + const source_xcm_asset_fee_amount = await this.sourceManager.getXcmFee(); + const hop_xcm_asset_fee_amount = await this.calculateHubXcmFee(transferOptions); + + const fee_asset = transferOptions.assets[0][0]; + + for (let i = 0; i < transferOptions.assets.length; i++) { + const initialBalances = assetInitialBalances[i]; + const finalBalances = assetFinalBalances[i]; + const send_amount = transferOptions.assets[i][1]; + const asset = transferOptions.assets[i][0]; + + let expectedSourceBalanceSpent = send_amount; + let expectedDestBalanceSpent = 0n; + let expectedTreasuryBalanceGained = 0n; + + if (asset === Asset.DOT) { + expectedSourceBalanceSpent += native_extrinsic_fee_amount + source_xcm_asset_fee_amount; + } + if (asset === fee_asset) { + expectedDestBalanceSpent += hop_xcm_asset_fee_amount; + expectedTreasuryBalanceGained += hop_xcm_asset_fee_amount; + } + + expect(finalBalances.source).toBe(initialBalances.source - expectedSourceBalanceSpent); + const difference = + finalBalances.destination - + (initialBalances.destination + send_amount - expectedDestBalanceSpent); + const tolerance = (finalBalances.destination * 1_000_000n) / 1_000_000_000n; // 0.0001% + expect(abs(difference)).toBeLessThanOrEqual(tolerance); + } + } + + async calculateHubXcmFee(transferOptions: TransferOptions): Promise { + console.log('Calculating Polkadot -> Hub -> Polimec fees'); + return 422157353n; // TODO: Replace this with the actual fee calculation below + } + + async computeFee(transferOptions: TransferOptions) { + let destinationExecutionFee: bigint; + + const sourceApi = this.sourceManager.getApi(Chains.Polkadot); + // const polimecApi = this.destManager.getApi(Chains.Polimec); + const destApi = this.hopManager.getApi(Chains.PolkadotHub); + + const versioned_assets = getVersionedAssets(transferOptions.assets); + const transferData = createTransferData({ + toChain: Chains.PolkadotHub, + assets: versioned_assets, + recv: transferOptions.account, + }); + console.dir(transferData, { depth: null, colors: true }); + + let remoteFeeAssetId: XcmVersionedAssetId; + const lastAsset = unwrap(transferOptions.assets[0]); + if (lastAsset[0] === Asset.DOT) { + lastAsset[2] = AssetSourceRelation.Parent; + } else { + throw new Error('Invalid asset'); + } + const versioned_asset = getVersionedAssets([lastAsset]); + if (versioned_asset.type === 'V4') { + remoteFeeAssetId = XcmVersionedAssetId.V4(unwrap(versioned_asset.value[0]).id); + } else { + throw new Error('Invalid versioned assets'); + } + console.log('remoteFeeAssetId', remoteFeeAssetId); + const localDryRunResult = await sourceApi.apis.DryRunApi.dry_run_call( + { type: 'system', value: DispatchRawOrigin.Signed(transferOptions.account) }, + { type: 'XcmPallet', value: { type: 'transfer_assets', value: transferData } }, + ); + console.log('localDryRunResult', localDryRunResult); + + let forwardedXcms: [XcmVersionedLocation, XcmVersionedXcm[]][] = []; + if (localDryRunResult.success && localDryRunResult.value.forwarded_xcms) { + forwardedXcms = localDryRunResult.value.forwarded_xcms; + } else { + throw new Error('Dry run failed'); + } + + const xcmsToHub = forwardedXcms.find( + ([location, _]) => + location.type === 'V4' && + location.value.parents === 0 && + location.value.interior.type === 'X1' && + location.value.interior.value.type === 'Parachain' && + location.value.interior.value.value === ParaId[Chains.PolkadotHub], + ); + if (!xcmsToHub) { + throw new Error('Could not find xcm to Polkadot Hub'); + } + const messages = xcmsToHub[1]; + const remoteXcm = messages[0]; + const remoteXcmWeightResult = await destApi.apis.XcmPaymentApi.query_xcm_weight(remoteXcm); + if (remoteXcmWeightResult.success) { + const remoteExecutionFeesResult = await destApi.apis.XcmPaymentApi.query_weight_to_asset_fee( + remoteXcmWeightResult.value, + remoteFeeAssetId, + ); + if (remoteExecutionFeesResult.success) { + destinationExecutionFee = remoteExecutionFeesResult.value; + } else { + throw new Error('Could not calculate destination xcm fee'); + } + } else { + throw new Error('Could not calculate xcm weight'); + } + + return destinationExecutionFee; } } diff --git a/integration-tests/chopsticks/src/types.ts b/integration-tests/chopsticks/src/types.ts index a7beac378..66b5500eb 100644 --- a/integration-tests/chopsticks/src/types.ts +++ b/integration-tests/chopsticks/src/types.ts @@ -1,5 +1,16 @@ -import type { pah, polimec, polkadot } from '@polkadot-api/descriptors'; -import type { PolkadotClient, TypedApi } from 'polkadot-api'; +import { + XcmV3Junction, + XcmV3JunctionNetworkId, + XcmV3Junctions, + XcmV3MultiassetFungibility, + XcmVersionedAssets, + XcmVersionedLocation, + type pah, + type polimec, + type polkadot, +} from '@polkadot-api/descriptors'; +import { FixedSizeBinary, type PolkadotClient, type TypedApi } from 'polkadot-api'; +import { WETH_ADDRESS } from './constants'; type Polimec = typeof polimec; type PolkadotHub = typeof pah; @@ -11,34 +22,33 @@ export enum Chains { Polkadot = 'ws://localhost:8002', } +export type ChainClient = { + api: TypedApi; + client: PolkadotClient; +}; + export const ParaId = { [Chains.Polimec]: 3344, [Chains.PolkadotHub]: 1000, }; +export enum AssetSourceRelation { + Parent = 0, + Sibling = 1, + Self = 2, +} + export enum Accounts { BOB = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', ALICE = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', } -export enum Assets { - USDT = 1984, - DOT = 10, - USDC = 1337, - UNKNOWN = 42, -} - export type ChainToDefinition = { [Chains.Polimec]: Polimec; [Chains.PolkadotHub]: PolkadotHub; [Chains.Polkadot]: Polkadot; }; -export type ChainClient = { - api: TypedApi; - client: PolkadotClient; -}; - export interface TransferResult { sourceBlock: number; destBlock: number; @@ -54,17 +64,127 @@ export interface PolimecBalanceCheck extends BalanceCheck { } export interface TransferDataParams { - amount: bigint; toChain: Chains; - assetIndex?: bigint; + assets: XcmVersionedAssets; recv?: Accounts; isMultiHop?: boolean; - // TODO: Check if this flag is actually needed. - isFromBridge?: boolean; } -export interface CreateAssetsParams { - amount: bigint; - assetIndex?: bigint; - isFromBridge?: boolean; +export enum Asset { + DOT = 10, + USDC = 1337, + USDT = 1984, + WETH = 10000, +} + +export function AssetHubAssetLocation( + assetId: bigint, + source_relation: AssetSourceRelation, +): XcmVersionedLocation { + switch (source_relation) { + case AssetSourceRelation.Sibling: + return XcmVersionedLocation.V4({ + parents: 1, + interior: XcmV3Junctions.X3([ + XcmV3Junction.Parachain(ParaId[Chains.PolkadotHub]), + XcmV3Junction.PalletInstance(50), + XcmV3Junction.GeneralIndex(assetId), + ]), + }); + case AssetSourceRelation.Self: + return XcmVersionedLocation.V4({ + parents: 0, + interior: XcmV3Junctions.X2([ + XcmV3Junction.PalletInstance(50), + XcmV3Junction.GeneralIndex(assetId), + ]), + }); + case AssetSourceRelation.Parent: + return XcmVersionedLocation.V4({ + parents: 0, + interior: XcmV3Junctions.X3([ + XcmV3Junction.Parachain(ParaId[Chains.PolkadotHub]), + XcmV3Junction.PalletInstance(50), + XcmV3Junction.GeneralIndex(assetId), + ]), + }); + } +} + +export function NativeAssetLocation( + source_relation: AssetSourceRelation, + paraId?: number, +): XcmVersionedLocation { + switch (source_relation) { + case AssetSourceRelation.Sibling: + if (!paraId) { + throw new Error('You need to specify a paraId with SourceRelation.Sibling'); + } + return XcmVersionedLocation.V4({ + parents: 1, + interior: XcmV3Junctions.X1(XcmV3Junction.Parachain(paraId)), + }); + case AssetSourceRelation.Self: + return XcmVersionedLocation.V4({ + parents: 0, + interior: XcmV3Junctions.Here(), + }); + case AssetSourceRelation.Parent: + return XcmVersionedLocation.V4({ + parents: 1, + interior: XcmV3Junctions.Here(), + }); + } +} + +export function EthereumAssetLocation(contract_address: FixedSizeBinary<20>): XcmVersionedLocation { + return XcmVersionedLocation.V4({ + parents: 2, + interior: XcmV3Junctions.X2([ + XcmV3Junction.GlobalConsensus(XcmV3JunctionNetworkId.Ethereum({ chain_id: 1n })), + XcmV3Junction.AccountKey20({ network: undefined, key: contract_address }), + ]), + }); +} + +export function AssetLocation( + asset: Asset, + asset_source_relation: AssetSourceRelation, +): XcmVersionedLocation { + switch (asset) { + case Asset.USDT: + return AssetHubAssetLocation(1984n, asset_source_relation); + + case Asset.USDC: + return AssetHubAssetLocation(1337n, asset_source_relation); + + case Asset.DOT: + return NativeAssetLocation(asset_source_relation); + + case Asset.WETH: { + return EthereumAssetLocation(FixedSizeBinary.fromHex(WETH_ADDRESS)); + } + } +} + +export function getVersionedAssets( + assets: [Asset, bigint, AssetSourceRelation][], +): XcmVersionedAssets { + const final_assets: { + id: { parents: number; interior: XcmV3Junctions }; + fun: XcmV3MultiassetFungibility; + }[] = []; + for (const [asset, amount, asset_source_relation] of assets) { + const location = AssetLocation(asset, asset_source_relation); + const id = { + parents: location.value.parents, + interior: location.value.interior as XcmV3Junctions, // We assume that this is not an XCM v2 MultiLocation. + }; + final_assets.push({ + id, + fun: XcmV3MultiassetFungibility.Fungible(amount), + }); + } + + return XcmVersionedAssets.V4(final_assets); } diff --git a/integration-tests/chopsticks/src/utils.ts b/integration-tests/chopsticks/src/utils.ts index 670d9fb58..c11577659 100644 --- a/integration-tests/chopsticks/src/utils.ts +++ b/integration-tests/chopsticks/src/utils.ts @@ -1,9 +1,11 @@ import { Accounts, + Asset, + AssetSourceRelation, Chains, - type CreateAssetsParams, ParaId, type TransferDataParams, + getVersionedAssets, } from '@/types'; import { XcmV3Instruction, @@ -15,7 +17,6 @@ import { XcmV3MultiassetWildMultiAsset, XcmV3WeightLimit, XcmVersionedAssetId, - XcmVersionedAssets, XcmVersionedLocation, XcmVersionedXcm, } from '@polkadot-api/descriptors'; @@ -57,61 +58,7 @@ const custom_xcm_on_dest = (): XcmVersionedXcm => { ]); }; -// TODO: Modify this function to allow the creation of an XcmVersionedAssets that supports also WETH/bridged assets. -const createHubAssets = ({ - amount, - assetIndex, - isFromBridge, -}: CreateAssetsParams): XcmVersionedAssets => - XcmVersionedAssets.V3([ - { - fun: XcmV3MultiassetFungibility.Fungible(amount), - id: XcmV3MultiassetAssetId.Concrete({ - parents: assetIndex ? 0 : 1, - interior: assetIndex - ? XcmV3Junctions.X2([ - XcmV3Junction.PalletInstance(50), - XcmV3Junction.GeneralIndex(assetIndex), - ]) - : XcmV3Junctions.Here(), - }), - }, - ]); - -const createDotAssets = ({ amount }: CreateAssetsParams): XcmVersionedAssets => - XcmVersionedAssets.V3([ - { - fun: XcmV3MultiassetFungibility.Fungible(amount), - id: XcmV3MultiassetAssetId.Concrete({ - parents: 0, - interior: XcmV3Junctions.Here(), - }), - }, - ]); - -const createPolimecAssets = ({ amount, assetIndex }: CreateAssetsParams): XcmVersionedAssets => { - if (!assetIndex) { - throw new Error('You need to specify an Asset ID while creating an asset for Polimec'); - } - return XcmVersionedAssets.V3([ - { - id: XcmV3MultiassetAssetId.Concrete({ - parents: 1, - interior: - assetIndex === 10n - ? XcmV3Junctions.Here() - : XcmV3Junctions.X3([ - XcmV3Junction.Parachain(ParaId[Chains.PolkadotHub]), - XcmV3Junction.PalletInstance(50), - XcmV3Junction.GeneralIndex(assetIndex), - ]), - }), - fun: XcmV3MultiassetFungibility.Fungible(amount), - }, - ]); -}; - -export const createTransferData = ({ amount, toChain, assetIndex, recv }: TransferDataParams) => { +export const createTransferData = ({ toChain, assets, recv }: TransferDataParams) => { if (toChain === Chains.Polkadot) { throw new Error('Invalid chain'); } @@ -133,16 +80,13 @@ export const createTransferData = ({ amount, toChain, assetIndex, recv }: Transf return { dest, beneficiary, - assets: - toChain === Chains.PolkadotHub - ? createPolimecAssets({ amount, assetIndex }) - : createHubAssets({ amount, assetIndex }), + assets, fee_asset_item: 0, weight_limit: XcmV3WeightLimit.Unlimited(), }; }; -export const createMultiHopTransferData = ({ amount }: TransferDataParams) => { +export const createDotMultiHopTransferData = (amount: bigint) => { const dest = XcmVersionedLocation.V3({ parents: 0, interior: XcmV3Junctions.X1(XcmV3Junction.Parachain(ParaId[Chains.PolkadotHub])), @@ -150,7 +94,7 @@ export const createMultiHopTransferData = ({ amount }: TransferDataParams) => { return { dest, - assets: createDotAssets({ amount }), + assets: getVersionedAssets([[Asset.DOT, amount, AssetSourceRelation.Self]]), assets_transfer_type: Enum('Teleport'), remote_fees_id: XcmVersionedAssetId.V3( XcmV3MultiassetAssetId.Concrete({ @@ -163,3 +107,31 @@ export const createMultiHopTransferData = ({ amount }: TransferDataParams) => { weight_limit: XcmV3WeightLimit.Unlimited(), }; }; + +export function unwrap(value: T | undefined, errorMessage = 'Value is undefined'): T { + if (value === undefined) { + throw new Error(errorMessage); + } + return value; +} + +export function flatObject(obj: unknown): unknown { + if (obj === null || obj === undefined) { + return obj; + } + if (obj instanceof Object && typeof (obj as { asHex?: unknown }).asHex === 'function') { + return (obj as { asHex: () => unknown }).asHex(); + } + if (typeof obj === 'object') { + if (Array.isArray(obj)) { + return obj.map(flatObject); + } + const normalized: Record = {}; + for (const [key, value] of Object.entries(obj)) { + normalized[key] = flatObject(value); + } + return normalized; + } + return obj; +} +export const abs = (n: bigint) => (n < 0n ? -n : n); diff --git a/integration-tests/src/tests/runtime_apis.rs b/integration-tests/src/tests/runtime_apis.rs index 8a0f733c6..fe06ba00d 100644 --- a/integration-tests/src/tests/runtime_apis.rs +++ b/integration-tests/src/tests/runtime_apis.rs @@ -3,8 +3,10 @@ use assets_common::runtime_api::runtime_decl_for_fungibles_api::FungiblesApiV2; use frame_support::traits::{ fungible::{Inspect, Mutate as FMutate}, fungibles::Mutate, + tokens::ConversionToAssetBalance, }; use polimec_common::assets::AcceptedFundingAsset; +use polimec_runtime::PLMCToAssetBalance; use sp_arithmetic::FixedU128; use xcm::v4::Junctions::X3; use xcm_fee_payment_runtime_api::fees::runtime_decl_for_xcm_payment_api::XcmPaymentApiV1; @@ -106,3 +108,13 @@ mod fungibles_api { }); } } + +#[test] +fn sandbox() { + use super::*; + + PolimecNet::execute_with(|| { + let b = PLMCToAssetBalance::to_asset_balance(135_0_000_000_000, Location::here()); + dbg!(b); + }); +} diff --git a/pallets/funding/src/storage_migrations.rs b/pallets/funding/src/storage_migrations.rs index 7690415cc..736a1081f 100644 --- a/pallets/funding/src/storage_migrations.rs +++ b/pallets/funding/src/storage_migrations.rs @@ -95,10 +95,6 @@ pub mod v6 { log::info!("Starting migration to V5"); let translate_project_details = |_key, item: OldProjectMetadataOf| -> Option> { items += 1; - log::info!("project_details item {:?}", items); - - // let old_participation_currencies = item.participation_currencies.to_vec(); - // let new_participation_currencies: BoundedVec> = old_participation_currencies.try_into().ok()?; Some(ProjectMetadataOf:: { token_information: item.token_information, @@ -125,7 +121,6 @@ pub mod v6 { for mut old_migration in old_migrations { items += 1; - log::info!("migration items {:?}", items); let origin_junction = old_migration.origin.user.interior.take_first().unwrap(); let new_origin = MigrationOrigin { user: origin_junction, @@ -139,6 +134,7 @@ pub mod v6 { }; crate::UserMigrations::::translate(translate_migration); + log::info!("Migration to V5 completed. Migrated {} items", items); T::DbWeight::get().reads_writes(items, items) } } diff --git a/runtimes/polimec/src/custom_migrations/asset_id_migration.rs b/runtimes/polimec/src/custom_migrations/asset_id_migration.rs index 650c5f7d8..e567b41a0 100644 --- a/runtimes/polimec/src/custom_migrations/asset_id_migration.rs +++ b/runtimes/polimec/src/custom_migrations/asset_id_migration.rs @@ -9,12 +9,20 @@ use frame_support::{ use itertools::Itertools; use pallet_assets::{Approval, AssetAccount, AssetDetails, AssetMetadata}; use polimec_common::assets::AcceptedFundingAsset; -use sp_api::runtime_decl_for_core::CoreV5; use sp_runtime::BoundedVec; use xcm::v4::Location; -// Storage items of pallet-assets are set to private for some reason. So we have to redefine them to get the same storage -// encoding and call the `translate` methods. -_-' +#[cfg(feature = "try-runtime")] +use frame_support::migrations::VersionedPostUpgradeData; + +#[cfg(feature = "try-runtime")] +use parity_scale_codec::Encode; + +#[cfg(feature = "try-runtime")] +use sp_std::vec::Vec; + +// Storage items of pallet-assets are private. +// So we have to redefine them to get the same storage encoding and call the `translate` methods. pub mod pallet_assets_storage_items { use super::*; @@ -115,81 +123,92 @@ impl OnRuntimeUpgrade for FromOldAssetIdMigration { fn on_runtime_upgrade() -> frame_support::weights::Weight { let version = Funding::on_chain_storage_version(); - log::info!("funding version: {:?}", version); + let runtime_version = ::Version::get(); if version != 5 { - log::info!("funding version is not 5"); - return frame_support::weights::Weight::zero(); + log::info!("AssetId Migration can be removed"); + return ::DbWeight::get().reads(1) } - let runtime_version = Runtime::version(); let mut items = 0; if runtime_version.spec_version == 1_000_000 { + log::info!("Running AssetId Migration..."); let id_map = BTreeMap::from([ - (1984, AcceptedFundingAsset::USDT.id()), - (1337, AcceptedFundingAsset::USDC.id()), (10, AcceptedFundingAsset::DOT.id()), + (1337, AcceptedFundingAsset::USDC.id()), + (1984, AcceptedFundingAsset::USDT.id()), (3344, Location::here()), ]); let old_account_iterator = pallet_assets_storage_items::old_types::Account::iter().collect_vec(); for (old_asset_id, account, account_info) in old_account_iterator { - items += 1; - log::info!("old_account item {:?}", items); - pallet_assets_storage_items::new_types::Account::insert( - id_map.get(&old_asset_id).unwrap(), - account.clone(), - account_info, - ); - pallet_assets_storage_items::old_types::Account::remove(old_asset_id, account); + if let Some(new_asset_id) = id_map.get(&old_asset_id) { + items += 1; + log::info!("old_account item {:?}", items); + + pallet_assets_storage_items::new_types::Account::insert( + new_asset_id, + account.clone(), + account_info, + ); + pallet_assets_storage_items::old_types::Account::remove(old_asset_id, account); + } } let old_asset_iterator = pallet_assets_storage_items::old_types::Asset::iter().collect_vec(); for (old_asset_id, asset_info) in old_asset_iterator { - items += 1; - log::info!("old_asset item {:?}", items); - pallet_assets_storage_items::new_types::Asset::insert(id_map.get(&old_asset_id).unwrap(), asset_info); - pallet_assets_storage_items::old_types::Asset::remove(old_asset_id); + if let Some(new_asset_id) = id_map.get(&old_asset_id) { + items += 1; + log::info!("old_asset item {:?}", items); + pallet_assets_storage_items::new_types::Asset::insert(new_asset_id, asset_info); + pallet_assets_storage_items::old_types::Asset::remove(old_asset_id); + } } let old_approvals_iterator = pallet_assets_storage_items::old_types::Approvals::iter().collect_vec(); for ((old_asset_id, owner, delegate), approval) in old_approvals_iterator { - items += 1; - log::info!("old_approvals item {:?}", items); - pallet_assets_storage_items::new_types::Approvals::insert( - (id_map.get(&old_asset_id).unwrap(), owner.clone(), delegate.clone()), - approval, - ); - pallet_assets_storage_items::old_types::Approvals::remove((old_asset_id, owner, delegate)); + if let Some(new_asset_id) = id_map.get(&old_asset_id) { + items += 1; + log::info!("old_approvals item {:?}", items); + pallet_assets_storage_items::new_types::Approvals::insert( + (new_asset_id, owner.clone(), delegate.clone()), + approval, + ); + pallet_assets_storage_items::old_types::Approvals::remove((old_asset_id, owner, delegate)); + } } let old_metadata_iterator = pallet_assets_storage_items::old_types::Metadata::iter().collect_vec(); for (old_asset_id, metadata) in old_metadata_iterator { - items += 1; - log::info!("old_metadata item {:?}", items); - pallet_assets_storage_items::new_types::Metadata::insert(id_map.get(&old_asset_id).unwrap(), metadata); - pallet_assets_storage_items::old_types::Metadata::remove(old_asset_id); + if let Some(new_asset_id) = id_map.get(&old_asset_id) { + items += 1; + log::info!("old_metadata item {:?}", items); + pallet_assets_storage_items::new_types::Metadata::insert(new_asset_id, metadata); + pallet_assets_storage_items::old_types::Metadata::remove(old_asset_id); + } } let old_oracle_raw_values_iterator = orml_oracle_storage_items::old_types::RawValues::iter().collect_vec(); for (account, old_asset_id, raw_values) in old_oracle_raw_values_iterator { - items += 1; - log::info!("old_oracle_raw_values item {:?}", items); - orml_oracle::RawValues::::insert( - account.clone(), - id_map.get(&old_asset_id).unwrap(), - raw_values, - ); - orml_oracle_storage_items::old_types::RawValues::remove(account, old_asset_id); + if let Some(new_asset_id) = id_map.get(&old_asset_id) { + items += 1; + log::info!("old_oracle_raw_values item {:?}", items); + orml_oracle::RawValues::::insert(account.clone(), new_asset_id, raw_values); + orml_oracle_storage_items::old_types::RawValues::remove(account, old_asset_id); + } } let old_oracle_values_iterator = orml_oracle_storage_items::old_types::Values::iter().collect_vec(); for (old_asset_id, value) in old_oracle_values_iterator { - items += 1; - log::info!("old_oracle_values item {:?}", items); - orml_oracle::Values::::insert(id_map.get(&old_asset_id).unwrap(), value); - orml_oracle_storage_items::old_types::Values::remove(old_asset_id); + if let Some(new_asset_id) = id_map.get(&old_asset_id) { + items += 1; + log::info!("old_oracle_values item {:?}", items); + orml_oracle::Values::::insert(new_asset_id, value); + orml_oracle_storage_items::old_types::Values::remove(old_asset_id); + } } } + log::info!("Total items migrated: {:?}", items); + ::DbWeight::get().reads_writes(items, items) } diff --git a/runtimes/polimec/src/lib.rs b/runtimes/polimec/src/lib.rs index f0cbc0edb..178b4055c 100644 --- a/runtimes/polimec/src/lib.rs +++ b/runtimes/polimec/src/lib.rs @@ -1176,29 +1176,38 @@ impl pallet_dispenser::Config for Runtime { type WeightInfo = weights::pallet_dispenser::WeightInfo; type WhitelistedPolicy = DispenserWhitelistedPolicy; } -pub struct PLMCToFundingAssetBalance; -impl ConversionToAssetBalance for PLMCToFundingAssetBalance { +pub struct PLMCToAssetBalance; +impl ConversionToAssetBalance for PLMCToAssetBalance { type Error = InvalidTransaction; fn to_asset_balance(plmc_balance: Balance, asset_id: Location) -> Result { + if asset_id == Location::here() { + return Ok(plmc_balance); + } + let plmc_price = >::get_decimals_aware_price(Location::here(), USD_DECIMALS, PLMC_DECIMALS) .ok_or(InvalidTransaction::Payment)?; + let funding_asset_decimals = >::decimals(asset_id.clone()); + let funding_asset_price = >::get_decimals_aware_price(asset_id, USD_DECIMALS, funding_asset_decimals) .ok_or(InvalidTransaction::Payment)?; + let usd_balance = plmc_price.saturating_mul_int(plmc_balance); + let funding_asset_balance = funding_asset_price.reciprocal().ok_or(InvalidTransaction::Payment)?.saturating_mul_int(usd_balance); + Ok(funding_asset_balance) } } impl pallet_asset_tx_payment::Config for Runtime { type Fungibles = ForeignAssets; type OnChargeAssetTransaction = TxFeeFungiblesAdapter< - PLMCToFundingAssetBalance, + PLMCToAssetBalance, CreditFungiblesToAccount, AssetsToBlockAuthor, >; @@ -1716,9 +1725,10 @@ impl_runtime_apis! { let location: Location = xcm::v4::AssetId::try_from(asset).map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?.0; let native_fee = TransactionPayment::weight_to_fee(weight); if location == Location::here() { + log::info!("Native fee in XcmPaymentApi: {:?}", native_fee); return Ok(native_fee) } - PLMCToFundingAssetBalance::to_asset_balance(native_fee, location).map_err(|_| XcmPaymentApiError::AssetNotFound) + PLMCToAssetBalance::to_asset_balance(native_fee, location).map_err(|_| XcmPaymentApiError::AssetNotFound) } diff --git a/runtimes/polimec/src/xcm_config.rs b/runtimes/polimec/src/xcm_config.rs index 811301b07..6302cfc92 100644 --- a/runtimes/polimec/src/xcm_config.rs +++ b/runtimes/polimec/src/xcm_config.rs @@ -17,8 +17,8 @@ extern crate alloc; use super::{ AccountId, AllPalletsWithSystem, Balance, Balances, ContributionTokens, EnsureRoot, ForeignAssets, Funding, - ParachainInfo, ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, ToTreasury, - TreasuryAccount, Vec, WeightToFee, + PLMCToAssetBalance, ParachainInfo, ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, + ToTreasury, TreasuryAccount, Vec, WeightToFee, }; use core::marker::PhantomData; use cumulus_primitives_core::ParaId; @@ -26,8 +26,10 @@ use frame_support::{ ensure, pallet_prelude::*, parameter_types, - traits::{ConstU32, Contains, ContainsPair, Everything, Nothing, ProcessMessageError}, - weights::Weight, + traits::{ + ConstU32, Contains, ContainsPair, Everything, Nothing, OnUnbalanced as OnUnbalancedT, ProcessMessageError, + }, + weights::{Weight, WeightToFee as WeightToFeeT}, }; use pallet_xcm::XcmPassthrough; use polimec_common::assets::AcceptedFundingAsset; @@ -35,6 +37,7 @@ use polimec_common::assets::AcceptedFundingAsset; use polimec_common_test_utils::DummyXcmSender; use polkadot_parachain_primitives::primitives::Sibling; use polkadot_runtime_common::xcm_sender::NoPriceForMessageDelivery; +use sp_runtime::traits::Zero; use xcm::v4::prelude::*; use xcm_builder::{ AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, AllowKnownQueryResponses, AllowSubscriptionsFrom, @@ -42,13 +45,15 @@ use xcm_builder::{ FixedRateOfFungible, FixedWeightBounds, FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, IsConcrete, MatchXcm, MatchedConvertedConcreteId, MintLocation, NoChecking, ParentIsPreset, RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, - SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit, TrailingSetTopicAsId, - UsingComponents, WithComputedOrigin, + SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeRevenue, TakeWeightCredit, + TrailingSetTopicAsId, UsingComponents, WithComputedOrigin, }; use xcm_executor::{ - traits::{JustTry, Properties, ShouldExecute}, - XcmExecutor, + traits::{JustTry, Properties, ShouldExecute, WeightTrader}, + AssetsInHolding, XcmExecutor, }; +use frame_support::traits::tokens::ConversionToAssetBalance; + // DOT from Polkadot Asset Hub const DOT_PER_SECOND_EXECUTION: u128 = 0_2_000_000_000; // 0.2 DOT per second of execution time @@ -70,10 +75,9 @@ parameter_types! { GlobalConsensus(Polkadot), Parachain(ParachainInfo::parachain_id().into()), ).into(); - pub UniversalLocationNetworkId: NetworkId = UniversalLocation::get().global_consensus().unwrap(); pub const HereLocation: Location = Location::here(); pub AssetHubLocation: Location = (Parent, Parachain(1000)).into(); - + pub UniversalLocationNetworkId: NetworkId = UniversalLocation::get().global_consensus().unwrap(); pub CheckAccount: AccountId = PolkadotXcm::check_account(); /// The check account that is allowed to mint assets locally. Used for PLMC teleport /// checking once enabled. @@ -316,11 +320,7 @@ impl xcm_executor::Config for XcmConfig { type SafeCallFilter = Nothing; type SubscriptionService = PolkadotXcm; type Trader = ( - // TODO: `WeightToFee` has to be carefully considered. For now use default - UsingComponents, - FixedRateOfFungible, - FixedRateOfFungible, - FixedRateOfFungible, + AssetTrader, ); type TransactionalProcessor = FrameTransactionalProcessor; type UniversalAliases = Nothing; @@ -495,3 +495,91 @@ impl cumulus_pallet_xcmp_queue::migration::v5::V5Config for Runtime { // This must be the same as the `ChannelInfo` from the `Config`: type ChannelList = ParachainSystem; } + +/// Can be used to buy weight in exchange for an accepted asset. +/// Only one asset can be used to buy weight at a time. +pub struct AssetTrader { + weight_bought: Weight, + asset_spent: Option, + phantom: PhantomData, +} +impl WeightTrader for AssetTrader { + fn new() -> Self { + Self { weight_bought: Weight::zero(), asset_spent: None, phantom: PhantomData } + } + + fn buy_weight( + &mut self, + weight: Weight, + payment: AssetsInHolding, + context: &XcmContext, + ) -> Result { + log::trace!(target: "xcm::weight", "AssetsTrader::buy_weight weight: {:?}, payment: {:?}, context: {:?}", weight, payment, context); + let native_amount = WeightToFee::weight_to_fee(&weight); + let mut acceptable_assets = AcceptedFundingAsset::all_ids(); + acceptable_assets.push(Location::here()); + + // We know the executor always sends just one asset to pay for weight, even if the struct supports multiple. + let payment_fun = payment.fungible.clone(); + let (asset_id, asset_amount) = payment_fun.first_key_value().ok_or(XcmError::FeesNotMet)?; + ensure!(acceptable_assets.contains(&asset_id.0), XcmError::FeesNotMet); + + // If the trader was used already in this xcm execution, make sure we continue trading with the same asset + let old_amount = if let Some(asset) =&self.asset_spent { + ensure!(asset.id == *asset_id, XcmError::FeesNotMet); + if let Fungibility::Fungible(amount) = asset.fun { + amount + } else { + return Err(XcmError::FeesNotMet) + } + } else { + Zero::zero() + }; + + let required_asset_amount = + PLMCToAssetBalance::to_asset_balance(native_amount, asset_id.0.clone()).map_err(|_| XcmError::FeesNotMet)?; + ensure!(*asset_amount >= required_asset_amount, XcmError::FeesNotMet); + + let required = (AssetId(asset_id.0.clone()), required_asset_amount).into(); + let unused = payment.checked_sub(required).map_err(|_| XcmError::FeesNotMet)?; + + self.weight_bought = self.weight_bought.saturating_add(weight); + self.asset_spent = + Some(Asset { id: asset_id.clone(), fun: Fungibility::Fungible(old_amount + required_asset_amount) }); + + Ok(unused) + } + + fn refund_weight(&mut self, weight: Weight, context: &XcmContext) -> Option { + log::trace!(target: "xcm::weight", "AssetsTrader::refund_weight weight: {:?}, context: {:?}, available weight: {:?}, available amount: {:?}", weight, context, self.weight_bought, self.asset_spent); + let weight_refunded = weight.min(self.weight_bought); + self.weight_bought -= weight_refunded; + + let native_amount = WeightToFee::weight_to_fee(&weight_refunded); + let asset_id = self.asset_spent.clone()?.id; + let asset_amount = PLMCToAssetBalance::to_asset_balance(native_amount, asset_id.0.clone()).ok()?; + log::trace!(target: "xcm::weight", "AssetTrader::refund_weight amount to refund: {:?}", asset_amount); + + if let Fungibility::Fungible(amount) = self.asset_spent.clone()?.fun { + self.asset_spent = + Some(Asset { id: asset_id.clone(), fun: Fungibility::Fungible(amount.saturating_sub(asset_amount)) }); + } else { + log::trace!(target: "xcm::weight", "AssetTrader::refund_weight unexpected non-fungible asset found. Bug somewhere"); + return None; + } + + if asset_amount > 0 { + Some((asset_id.clone(), asset_amount).into()) + } else { + None + } + } +} +impl Drop for AssetTrader { + fn drop(&mut self) { + if let Some(asset) = &self.asset_spent { + log::trace!(target: "xcm::weight", "AssetTrader::drop asset: {:?}", asset); + Payee::take_revenue(asset.clone()); + } + } +}