diff --git a/Cargo.lock b/Cargo.lock index 51ccdcf0cf8..a5aecb1d985 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6473,6 +6473,7 @@ dependencies = [ "strum 0.24.1", "strum_macros 0.24.3", "substrate-wasm-builder", + "xcm-fee-payment-runtime-api", "xcm-primitives 0.1.0", "xcm-primitives 0.1.1", "xcm-simulator", @@ -6962,6 +6963,7 @@ dependencies = [ "strum 0.24.1", "strum_macros 0.24.3", "substrate-wasm-builder", + "xcm-fee-payment-runtime-api", "xcm-primitives 0.1.0", "xcm-primitives 0.1.1", "xcm-simulator", @@ -7030,6 +7032,7 @@ dependencies = [ "staging-xcm", "staging-xcm-builder", "staging-xcm-executor", + "xcm-fee-payment-runtime-api", "xcm-primitives 0.1.1", ] @@ -7166,6 +7169,7 @@ dependencies = [ "tiny-bip39", "tokio", "trie-root 0.15.2", + "xcm-fee-payment-runtime-api", ] [[package]] @@ -7364,6 +7368,7 @@ dependencies = [ "strum 0.24.1", "strum_macros 0.24.3", "substrate-wasm-builder", + "xcm-fee-payment-runtime-api", "xcm-primitives 0.1.0", "xcm-primitives 0.1.1", "xcm-simulator", diff --git a/Cargo.toml b/Cargo.toml index d6f53b331f8..22e43152b79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -311,6 +311,7 @@ polkadot-runtime-parachains = { git = "https://github.com/moonbeam-foundation/po xcm = { package = "staging-xcm", git = "https://github.com/moonbeam-foundation/polkadot-sdk", branch = "moonbeam-polkadot-v1.11.0", default-features = false } xcm-builder = { package = "staging-xcm-builder", git = "https://github.com/moonbeam-foundation/polkadot-sdk", branch = "moonbeam-polkadot-v1.11.0", default-features = false } xcm-executor = { package = "staging-xcm-executor", git = "https://github.com/moonbeam-foundation/polkadot-sdk", branch = "moonbeam-polkadot-v1.11.0", default-features = false } +xcm-fee-payment-runtime-api = { git = "https://github.com/moonbeam-foundation/polkadot-sdk", branch = "moonbeam-polkadot-v1.11.0", default-features = false } # Polkadot / XCM (client) #kusama-runtime = { package = "staging-kusama-runtime", git = "https://github.com/moonbeam-foundation/polkadot-sdk", branch = "moonbeam-polkadot-v1.11.0" } diff --git a/node/service/Cargo.toml b/node/service/Cargo.toml index 8281583a2eb..fccdc95db0a 100644 --- a/node/service/Cargo.toml +++ b/node/service/Cargo.toml @@ -144,6 +144,7 @@ polkadot-parachain = { workspace = true } polkadot-primitives = { workspace = true } polkadot-service = { workspace = true } xcm = { workspace = true } +xcm-fee-payment-runtime-api = { workspace = true } # Benchmarking frame-benchmarking = { workspace = true, features = ["std"] } diff --git a/node/service/src/client.rs b/node/service/src/client.rs index bb6d634c975..97dd59e8955 100644 --- a/node/service/src/client.rs +++ b/node/service/src/client.rs @@ -47,6 +47,7 @@ pub trait RuntimeApiCollection: + cumulus_primitives_core::CollectCollationInfo + session_keys_primitives::VrfApi + async_backing_primitives::UnincludedSegmentApi + + xcm_fee_payment_runtime_api::XcmPaymentApi { } @@ -67,6 +68,7 @@ impl RuntimeApiCollection for Api where + cumulus_primitives_core::CollectCollationInfo + session_keys_primitives::VrfApi + async_backing_primitives::UnincludedSegmentApi + + xcm_fee_payment_runtime_api::XcmPaymentApi { } diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index a6e2387eabd..aa101362273 100644 --- a/runtime/common/Cargo.toml +++ b/runtime/common/Cargo.toml @@ -76,6 +76,7 @@ pallet-author-slot-filter = { workspace = true } xcm = { workspace = true } xcm-builder = { workspace = true } xcm-executor = { workspace = true } +xcm-fee-payment-runtime-api = { workspace = true } # Parity parity-scale-codec = { workspace = true } @@ -111,6 +112,7 @@ std = [ "sp-std/std", "sp-genesis-builder/std", "xcm-executor/std", + "xcm-fee-payment-runtime-api/std", "xcm/std", "account/std", ] diff --git a/runtime/common/src/apis.rs b/runtime/common/src/apis.rs index 2d974706c3e..38912cb7d4d 100644 --- a/runtime/common/src/apis.rs +++ b/runtime/common/src/apis.rs @@ -741,6 +741,86 @@ macro_rules! impl_runtime_apis_plus_common { } } + impl xcm_fee_payment_runtime_api::XcmPaymentApi for Runtime { + fn query_acceptable_payment_assets( + xcm_version: xcm::Version + ) -> Result, XcmPaymentApiError> { + if !matches!(xcm_version, 3) { + return Err(XcmPaymentApiError::UnhandledXcmVersion); + } + + let self_reserve_location: Location = Location::try_from(xcm_config::SelfReserve::get()) + .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; + + Ok([VersionedAssetId::V3(XcmAssetId::from(self_reserve_location))] + .into_iter() + .chain( + pallet_asset_manager::AssetTypeId::::iter_keys().filter_map(|asset_location| { + if !AssetManager::payment_is_supported(asset_location.clone()) { + return None; + } + + let location: Option = asset_location.into(); + if let Some(loc) = location { + return Some(VersionedAssetId::V3(loc.into())) + } + None + }) + ) + .filter_map(|asset| asset.into_version(xcm_version).ok()) + .collect()) + } + + fn query_weight_to_asset_fee( + weight: Weight, asset: VersionedAssetId + ) -> Result { + let self_reserve_location: Location = Location::try_from(xcm_config::SelfReserve::get()) + .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; + + let local_asset = VersionedAssetId::V3(XcmAssetId::from(self_reserve_location)); + let asset = asset + .into_version(3) + .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; + + if asset == local_asset { + Ok(TransactionPayment::weight_to_fee(weight)) + }else { + let asset_v3: XcmAssetId = asset.try_into() + .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; + + if let XcmAssetId::Concrete(asset_location) = asset_v3 { + let asset_type: AssetType = AssetType::from(asset_location); + if !AssetManager::payment_is_supported(asset_type.clone()) { + return Err(XcmPaymentApiError::AssetNotFound); + } + + let units_per_sec = AssetManager::get_units_per_second(asset_type); + if let None = units_per_sec { + return Err(XcmPaymentApiError::WeightNotComputable); + } + + let final_asset_fee = units_per_sec + .unwrap_or_default() + .saturating_mul(weight.ref_time() as u128) + / (WEIGHT_REF_TIME_PER_SECOND as u128); + + return Ok(final_asset_fee) + } + Err(XcmPaymentApiError::AssetNotFound) + } + } + + fn query_xcm_weight(message: VersionedXcm<()>) -> Result { + PolkadotXcm::query_xcm_weight(message) + } + + fn query_delivery_fees( + destination: VersionedLocation, message: VersionedXcm<()> + ) -> Result { + PolkadotXcm::query_delivery_fees(destination, message) + } + } + #[cfg(feature = "runtime-benchmarks")] impl frame_benchmarking::Benchmark for Runtime { diff --git a/runtime/moonbase/Cargo.toml b/runtime/moonbase/Cargo.toml index fb1d35eca72..308bfdd1341 100644 --- a/runtime/moonbase/Cargo.toml +++ b/runtime/moonbase/Cargo.toml @@ -149,6 +149,7 @@ polkadot-runtime-common = { workspace = true } xcm = { workspace = true } xcm-builder = { workspace = true } xcm-executor = { workspace = true } +xcm-fee-payment-runtime-api = { workspace = true } # Cumulus cumulus-pallet-dmp-queue = { workspace = true } @@ -307,6 +308,7 @@ std = [ "strum/std", "xcm-builder/std", "xcm-executor/std", + "xcm-fee-payment-runtime-api/std", "xcm-primitives/std", "xcm/std", ] diff --git a/runtime/moonbase/src/lib.rs b/runtime/moonbase/src/lib.rs index c5132c3575d..25a6fd1049b 100644 --- a/runtime/moonbase/src/lib.rs +++ b/runtime/moonbase/src/lib.rs @@ -118,6 +118,13 @@ use sp_std::{ #[cfg(feature = "std")] use sp_version::NativeVersion; use sp_version::RuntimeVersion; +use xcm::{ + v3::{AssetId as XcmAssetId, Location}, + IntoVersion, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm, +}; +use xcm_config::AssetType; +use xcm_fee_payment_runtime_api::Error as XcmPaymentApiError; +use xcm_primitives::UnitsToWeightRatio; use smallvec::smallvec; use sp_runtime::serde::{Deserialize, Serialize}; diff --git a/runtime/moonbeam/Cargo.toml b/runtime/moonbeam/Cargo.toml index da139a4e7af..3d3e0a5195d 100644 --- a/runtime/moonbeam/Cargo.toml +++ b/runtime/moonbeam/Cargo.toml @@ -145,6 +145,7 @@ polkadot-parachain = { workspace = true } xcm = { workspace = true } xcm-builder = { workspace = true } xcm-executor = { workspace = true } +xcm-fee-payment-runtime-api = { workspace = true } # Cumulus cumulus-pallet-dmp-queue = { workspace = true } @@ -298,6 +299,7 @@ std = [ "strum/std", "xcm-builder/std", "xcm-executor/std", + "xcm-fee-payment-runtime-api/std", "xcm-primitives/std", "xcm/std", ] diff --git a/runtime/moonbeam/src/lib.rs b/runtime/moonbeam/src/lib.rs index 12f5ad9db47..9d9c9c44f1f 100644 --- a/runtime/moonbeam/src/lib.rs +++ b/runtime/moonbeam/src/lib.rs @@ -99,6 +99,13 @@ use sp_runtime::{ Perquintill, SaturatedConversion, }; use sp_std::{convert::TryFrom, prelude::*}; +use xcm::{ + v3::{AssetId as XcmAssetId, Location}, + IntoVersion, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm, +}; +use xcm_config::AssetType; +use xcm_fee_payment_runtime_api::Error as XcmPaymentApiError; +use xcm_primitives::UnitsToWeightRatio; #[cfg(feature = "std")] use sp_version::NativeVersion; diff --git a/runtime/moonriver/Cargo.toml b/runtime/moonriver/Cargo.toml index 2ab9b858635..361bc16a3ff 100644 --- a/runtime/moonriver/Cargo.toml +++ b/runtime/moonriver/Cargo.toml @@ -145,6 +145,7 @@ polkadot-runtime-common = { workspace = true } xcm = { workspace = true } xcm-builder = { workspace = true } xcm-executor = { workspace = true } +xcm-fee-payment-runtime-api = { workspace = true } pallet-message-queue = { workspace = true } # Cumulus @@ -298,6 +299,7 @@ std = [ "strum/std", "xcm-builder/std", "xcm-executor/std", + "xcm-fee-payment-runtime-api/std", "xcm-primitives/std", "xcm/std", ] diff --git a/runtime/moonriver/src/lib.rs b/runtime/moonriver/src/lib.rs index ff5c8f03bc4..73fb75ea42f 100644 --- a/runtime/moonriver/src/lib.rs +++ b/runtime/moonriver/src/lib.rs @@ -99,6 +99,13 @@ use sp_runtime::{ Perquintill, SaturatedConversion, }; use sp_std::{convert::TryFrom, prelude::*}; +use xcm::{ + v3::{AssetId as XcmAssetId, Location}, + IntoVersion, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm, +}; +use xcm_config::AssetType; +use xcm_fee_payment_runtime_api::Error as XcmPaymentApiError; +use xcm_primitives::UnitsToWeightRatio; use smallvec::smallvec; #[cfg(feature = "std")] diff --git a/test/suites/dev/moonbase/test-xcm-v4/test-xcm-payment-api-transact-foreign.ts b/test/suites/dev/moonbase/test-xcm-v4/test-xcm-payment-api-transact-foreign.ts new file mode 100644 index 00000000000..ad57f12f91c --- /dev/null +++ b/test/suites/dev/moonbase/test-xcm-v4/test-xcm-payment-api-transact-foreign.ts @@ -0,0 +1,252 @@ +import "@moonbeam-network/api-augment"; +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import { ApiPromise, WsProvider } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { u128 } from "@polkadot/types"; +import { hexToBigInt } from "@polkadot/util"; +import { PalletAssetsAssetAccount, PalletAssetsAssetDetails } from "@polkadot/types/lookup"; +import { generateKeyringPair, alith } from "@moonwall/util"; +import { + XcmFragment, + RawXcmMessage, + injectHrmpMessageAndSeal, + descendOriginFromAddress20, + relayAssetMetadata, + RELAY_SOURCE_LOCATION, + registerForeignAsset, + mockAssetBalance, +} from "../../../../helpers"; + +// TODO: remove once we upgrade @polkadot/api to v12.1.1 +const runtimeApi = { + runtime: { + XcmPaymentApi: [ + { + methods: { + query_acceptable_payment_assets: { + description: "The API to query acceptable payment assets", + params: [ + { + name: "version", + type: "u32", + }, + ], + type: "Result, XcmPaymentApiError>", + }, + query_weight_to_asset_fee: { + description: "", + params: [ + { + name: "weight", + type: "WeightV2", + }, + { + name: "asset", + type: "XcmVersionedAssetId", + }, + ], + type: "Result", + }, + query_xcm_weight: { + description: "", + params: [ + { + name: "message", + type: "XcmVersionedXcm", + }, + ], + type: "Result", + }, + query_delivery_fees: { + description: "", + params: [ + { + name: "destination", + type: "XcmVersionedLocation", + }, + { + name: "message", + type: "XcmVersionedXcm", + }, + ], + type: "Result", + }, + }, + version: 1, + }, + ], + }, + types: { + XcmPaymentApiError: { + _enum: { + Unimplemented: "Null", + VersionedConversionFailed: "Null", + WeightNotComputable: "Null", + UnhandledXcmVersion: "Null", + AssetNotFound: "Null", + }, + }, + }, +}; + +describeSuite({ + id: "D014133", + title: "XCM - XcmPaymentApi - Transact", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + let polkadotJs: ApiPromise; + let amountForFees: bigint; + let amountForTransfer: bigint; + let assetId: u128; + let sendingAddress: `0x${string}`; + let descendAddress: `0x${string}`; + let random: KeyringPair; + let foreignAssetId: string; + const weightLimit = { + refTime: 40_000_000_000n, + proofSize: 110_000n, + }; + let weightToForeignFee: any; + + beforeAll(async () => { + // TODO: remove once we upgrade @polkadot/api to v12.1.1 + polkadotJs = await ApiPromise.create({ + provider: new WsProvider(`ws://localhost:${process.env.MOONWALL_RPC_PORT}/`), + ...runtimeApi, + }); + + const { registeredAssetId } = await registerForeignAsset( + context, + RELAY_SOURCE_LOCATION, + relayAssetMetadata as any, + 20000000000 + ); + + foreignAssetId = registeredAssetId; + + // Fetch the exact amount of foreign fees that we will use given + // the indicated weightLimit + weightToForeignFee = await polkadotJs.call.xcmPaymentApi.queryWeightToAssetFee(weightLimit, { + V3: { + Concrete: { parents: 1, interior: "Here" }, + }, + }); + + expect(weightToForeignFee.isOk).to.be.true; + + const { originAddress, descendOriginAddress } = descendOriginFromAddress20(context); + descendAddress = descendOriginAddress; + sendingAddress = originAddress; + + random = generateKeyringPair(); + // Amount to use inside BuyExecution + amountForFees = BigInt(weightToForeignFee.asOk.toJSON()); + // Amount to transfer to random address + amountForTransfer = 1_000_000_000_000_000_000n; + + const balance = polkadotJs.createType("Balance", amountForFees); + assetId = polkadotJs.createType("u128", hexToBigInt(foreignAssetId as `0x${string}`)); + + const assetBalance: PalletAssetsAssetAccount = polkadotJs.createType( + "PalletAssetsAssetAccount", + { + balance: balance, + } + ); + const assetDetails: PalletAssetsAssetDetails = polkadotJs.createType( + "PalletAssetsAssetDetails", + { + supply: balance, + } + ); + + // Fund descendAddress with enough xcDOTs to pay XCM execution fees + await mockAssetBalance(context, assetBalance, assetDetails, alith, assetId, descendAddress); + + // We need to fund the descendAddress with both amounts. + // This account takes care of paying the foreign fees and also transfering the + // native tokens to the random address. + await context.createBlock( + polkadotJs.tx.balances.transferAllowDeath(descendAddress, amountForTransfer), + { allowFailures: false } + ); + + const descendForeignBalance = ( + await polkadotJs.query.assets.account(foreignAssetId, descendAddress) + ) + .unwrap() + .balance.toBigInt(); + + const descendNativeBalance = ( + await polkadotJs.query.system.account(descendAddress) + ).data.free.toBigInt(); + expect(descendForeignBalance).to.eq(amountForFees); + expect(descendNativeBalance).to.eq(amountForTransfer); + }); + + it({ + id: "T01", + title: "Should de able to transact using the estimated foreign fees", + test: async function () { + // Build Transact encoded call + const transferCall = polkadotJs.tx.balances.transferAllowDeath( + random.address, + amountForTransfer + ); + const transferCallEncoded = transferCall?.method.toHex(); + + // Build the XCM message with the corresponding weightLimit + const xcmMessage = new XcmFragment({ + assets: [ + { + multilocation: { + parents: 1, + interior: { Here: null }, + }, + fungible: amountForFees, + }, + ], + weight_limit: weightLimit, + descend_origin: sendingAddress, + }) + .descend_origin() + .withdraw_asset() + .buy_execution() + .push_any({ + Transact: { + originKind: "SovereignAccount", + requireWeightAtMost: { + refTime: 1_000_000_000n, + proofSize: 80_000n, + }, + call: { + encoded: transferCallEncoded, + }, + }, + }) + .as_v4(); + + // Send an XCM and create block to execute it + await injectHrmpMessageAndSeal(context, 1, { + type: "XcmVersionedXcm", + payload: xcmMessage, + } as RawXcmMessage); + + // Make sure the random address received the transfer + const testAccountBalance = ( + await polkadotJs.query.system.account(random.address) + ).data.free.toBigInt(); + + // Make sure the descendOrigin address has zero foreign balance now + const testDescendBalance = ( + await polkadotJs.query.assets.account(foreignAssetId, descendAddress) + ) + .unwrap() + .balance.toBigInt(); + + expect(testAccountBalance).to.eq(amountForTransfer); + expect(testDescendBalance).to.eq(0n); + }, + }); + }, +}); diff --git a/test/suites/dev/moonbase/test-xcm-v4/test-xcm-payment-api-transact-native.ts b/test/suites/dev/moonbase/test-xcm-v4/test-xcm-payment-api-transact-native.ts new file mode 100644 index 00000000000..3460238abdc --- /dev/null +++ b/test/suites/dev/moonbase/test-xcm-v4/test-xcm-payment-api-transact-native.ts @@ -0,0 +1,221 @@ +import "@moonbeam-network/api-augment"; +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import { ApiPromise, WsProvider } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { generateKeyringPair } from "@moonwall/util"; +import { + XcmFragment, + RawXcmMessage, + injectHrmpMessageAndSeal, + descendOriginFromAddress20, +} from "../../../../helpers/xcm.js"; + +// TODO: remove once we upgrade @polkadot/api to v12.1.1 +const runtimeApi = { + runtime: { + XcmPaymentApi: [ + { + methods: { + query_acceptable_payment_assets: { + description: "The API to query acceptable payment assets", + params: [ + { + name: "version", + type: "u32", + }, + ], + type: "Result, XcmPaymentApiError>", + }, + query_weight_to_asset_fee: { + description: "", + params: [ + { + name: "weight", + type: "WeightV2", + }, + { + name: "asset", + type: "XcmVersionedAssetId", + }, + ], + type: "Result", + }, + query_xcm_weight: { + description: "", + params: [ + { + name: "message", + type: "XcmVersionedXcm", + }, + ], + type: "Result", + }, + query_delivery_fees: { + description: "", + params: [ + { + name: "destination", + type: "XcmVersionedLocation", + }, + { + name: "message", + type: "XcmVersionedXcm", + }, + ], + type: "Result", + }, + }, + version: 1, + }, + ], + }, + types: { + XcmPaymentApiError: { + _enum: { + Unimplemented: "Null", + VersionedConversionFailed: "Null", + WeightNotComputable: "Null", + UnhandledXcmVersion: "Null", + AssetNotFound: "Null", + }, + }, + }, +}; + +describeSuite({ + id: "D014132", + title: "XCM - XcmPaymentApi - Transact", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + let polkadotJs: ApiPromise; + let amountForFees: bigint; + let amountForTransfer: bigint; + let sendingAddress: `0x${string}`; + let descendAddress: `0x${string}`; + let random: KeyringPair; + let balancesPalletIndex: number; + const weightLimit = { + refTime: 40_000_000_000n, + proofSize: 110_000n, + }; + let weightToNativeFee: any; + + beforeAll(async () => { + // TODO: remove once we upgrade @polkadot/api to v12.1.1 + polkadotJs = await ApiPromise.create({ + provider: new WsProvider(`ws://localhost:${process.env.MOONWALL_RPC_PORT}/`), + ...runtimeApi, + }); + + // Get Pallet balances index + const metadata = await polkadotJs.rpc.state.getMetadata(); + balancesPalletIndex = metadata.asLatest.pallets + .find(({ name }) => name.toString() == "Balances")! + .index.toNumber(); + + // Fetch the exact amount of native fees that we will use given + // the indicated weightLimit + weightToNativeFee = await polkadotJs.call.xcmPaymentApi.queryWeightToAssetFee(weightLimit, { + V3: { + Concrete: { + parents: 0, + interior: { + X1: { PalletInstance: balancesPalletIndex }, + }, + }, + }, + }); + + expect(weightToNativeFee.isOk).to.be.true; + + const { originAddress, descendOriginAddress } = descendOriginFromAddress20(context); + descendAddress = descendOriginAddress; + sendingAddress = originAddress; + + random = generateKeyringPair(); + // Amount to use inside BuyExecution + amountForFees = BigInt(weightToNativeFee.asOk.toJSON()); + // Amount to transfer to random address + amountForTransfer = 1_000_000_000_000_000_000n; + + // We need to fund the descendAddress with both amounts. + // This account takes care of paying the fees and also transfering the + // tokens to the random address. + await context.createBlock( + polkadotJs.tx.balances.transferAllowDeath( + descendAddress, + amountForFees + amountForTransfer + ), + { allowFailures: false } + ); + + const balance = (await polkadotJs.query.system.account(descendAddress)).data.free.toBigInt(); + expect(balance).to.eq(amountForFees + amountForTransfer); + }); + + it({ + id: "T01", + title: "Should de able to transact using the estimated native fees", + test: async function () { + // Build Transact encoded call + const transferCall = polkadotJs.tx.balances.transferAllowDeath( + random.address, + amountForTransfer + ); + const transferCallEncoded = transferCall?.method.toHex(); + + // Build the XCM message with the corresponding weightLimit + const xcmMessage = new XcmFragment({ + assets: [ + { + multilocation: { + parents: 0, + interior: { + X1: { PalletInstance: balancesPalletIndex }, + }, + }, + fungible: amountForFees, + }, + ], + weight_limit: weightLimit, + descend_origin: sendingAddress, + }) + .descend_origin() + .withdraw_asset() + .buy_execution() + .push_any({ + Transact: { + originKind: "SovereignAccount", + requireWeightAtMost: { + refTime: 1000000000n, + proofSize: 80000n, + }, + call: { + encoded: transferCallEncoded, + }, + }, + }) + .as_v4(); + + // Send an XCM and create block to execute it + await injectHrmpMessageAndSeal(context, 1, { + type: "XcmVersionedXcm", + payload: xcmMessage, + } as RawXcmMessage); + + // Make sure the random address received the transfer + const testAccountBalance = ( + await polkadotJs.query.system.account(random.address) + ).data.free.toBigInt(); + + // Make sure the descendOrigin address has zero balance now + const testDescendBalance = ( + await polkadotJs.query.system.account(descendAddress) + ).data.free.toBigInt(); + + expect(testAccountBalance).to.eq(amountForTransfer); + expect(testDescendBalance).to.eq(0n); + }, + }); + }, +}); diff --git a/test/suites/dev/moonbase/test-xcm-v4/test-xcm-payment-api.ts b/test/suites/dev/moonbase/test-xcm-v4/test-xcm-payment-api.ts new file mode 100644 index 00000000000..0ad1f176bb7 --- /dev/null +++ b/test/suites/dev/moonbase/test-xcm-v4/test-xcm-payment-api.ts @@ -0,0 +1,215 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import { ApiPromise, WsProvider } from "@polkadot/api"; +import { + XcmFragment, + registerForeignAsset, + relayAssetMetadata, + RELAY_SOURCE_LOCATION, +} from "../../../../helpers"; + +// TODO: remove once we upgrade @polkadot/api to v12.1.1 +const runtimeApi = { + runtime: { + XcmPaymentApi: [ + { + methods: { + query_acceptable_payment_assets: { + description: "The API to query acceptable payment assets", + params: [ + { + name: "version", + type: "u32", + }, + ], + type: "Result, XcmPaymentApiError>", + }, + query_weight_to_asset_fee: { + description: "", + params: [ + { + name: "weight", + type: "WeightV2", + }, + { + name: "asset", + type: "XcmVersionedAssetId", + }, + ], + type: "Result", + }, + query_xcm_weight: { + description: "", + params: [ + { + name: "message", + type: "XcmVersionedXcm", + }, + ], + type: "Result", + }, + query_delivery_fees: { + description: "", + params: [ + { + name: "destination", + type: "XcmVersionedLocation", + }, + { + name: "message", + type: "XcmVersionedXcm", + }, + ], + type: "Result", + }, + }, + version: 1, + }, + ], + }, + types: { + XcmPaymentApiError: { + _enum: { + Unimplemented: "Null", + VersionedConversionFailed: "Null", + WeightNotComputable: "Null", + UnhandledXcmVersion: "Null", + AssetNotFound: "Null", + }, + }, + }, +}; + +describeSuite({ + id: "D014131", + title: "XCM - XcmPaymentApi", + foundationMethods: "dev", + testCases: ({ context, it }) => { + let polkadotJs: ApiPromise; + + beforeAll(async function () { + // TODO: this won't be needed after we upgrade @polkadot/api to v12.1.1 + polkadotJs = await ApiPromise.create({ + provider: new WsProvider(`ws://localhost:${process.env.MOONWALL_RPC_PORT}/`), + ...runtimeApi, + }); + + await registerForeignAsset( + context, + RELAY_SOURCE_LOCATION, + relayAssetMetadata as any, + 20000000000 + ); + }); + + it({ + id: "T01", + title: "Should succeed calling XcmPaymentApi methods", + test: async function () { + const metadata = await context.polkadotJs().rpc.state.getMetadata(); + const balancesPalletIndex = metadata.asLatest.pallets + .find(({ name }) => name.toString() == "Balances")! + .index.toNumber(); + + const allowedAssets = await polkadotJs.call.xcmPaymentApi.queryAcceptablePaymentAssets(3); + + expect(allowedAssets.isOk).to.be.true; + // Should include the native asset + the foreign one + expect(allowedAssets.asOk.toJSON().length).to.be.equal(2); + + const weightToNativeFee = await polkadotJs.call.xcmPaymentApi.queryWeightToAssetFee( + { + refTime: 10_000_000_000n, + proofSize: 80_000n, + }, + { + V3: { + Concrete: { + parents: 0, + interior: { + X1: { PalletInstance: balancesPalletIndex }, + }, + }, + }, + } + ); + + expect(weightToNativeFee.isOk).to.be.true; + // 0.0005 GLMR + expect(BigInt(weightToNativeFee.asOk.toJSON())).to.eq(500_000_000_000_000n); + + const weightToForeignFee = await polkadotJs.call.xcmPaymentApi.queryWeightToAssetFee( + { + refTime: 10_000_000_000n, + proofSize: 0n, + }, + { + V3: { + Concrete: { parents: 1, interior: "Here" }, + }, + } + ); + + expect(weightToForeignFee.isOk).to.be.true; + + // (unitsPerSec * weight.ref_time()) / WEIGHT_REF_TIME_PER_SECOND + // (20_000_000_000 * 10_000_000_000) / 1_000_000_000_000 + expect(BigInt(weightToForeignFee.asOk.toJSON())).to.eq(200_000_000n); + + const transactWeightAtMost = { + refTime: 500_000_000n, + proofSize: 20000n, + }; + + const xcmMessage = new XcmFragment({ + assets: [ + { + multilocation: { + parents: 0, + interior: { + X1: { PalletInstance: balancesPalletIndex }, + }, + }, + fungible: 1000000000n, + }, + ], + weight_limit: { + refTime: 40000000000n, + proofSize: 110000n, + }, + }) + .withdraw_asset() + .buy_execution() + .push_any({ + Transact: { + originKind: "SovereignAccount", + requireWeightAtMost: transactWeightAtMost, + call: { + encoded: "0x", + }, + }, + }) + .as_v3(); + + const weightMessage = await polkadotJs.call.xcmPaymentApi.queryXcmWeight(xcmMessage); + expect(weightMessage.isOk).to.be.true; + expect(weightMessage.asOk.refTime.toBigInt() > transactWeightAtMost.refTime).to.be.true; + expect(weightMessage.asOk.proofSize.toBigInt() > transactWeightAtMost.proofSize).to.be.true; + + const dest = { + V2: { + parents: 1, + interior: "Here", + }, + }; + + const deliveryFees = await polkadotJs.call.xcmPaymentApi.queryDeliveryFees( + dest, + xcmMessage + ); + expect(deliveryFees.isOk).to.be.true; + // No delivery fees set for now + expect(deliveryFees.asOk.toHuman()["V3"]).to.be.empty; + }, + }); + }, +});