diff --git a/.gitattributes b/.gitattributes index 60113eb62..f2c586680 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ # Mark typescript-api folder as auto generated typescript-api/** linguist-generated=true +typescript-api/package.json linguist-generated=false +typescript-api/scripts/** linguist-generated=false +typescript-api/src/*/interfaces/tanssi/definitions.ts linguist-generated=false diff --git a/Cargo.lock b/Cargo.lock index bbab6990e..8a237ba78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1947,6 +1947,7 @@ dependencies = [ "staging-xcm-executor", "substrate-wasm-builder", "tanssi-runtime-common", + "xcm-fee-payment-runtime-api", "xcm-primitives", ] @@ -2028,6 +2029,7 @@ dependencies = [ "staging-xcm-executor", "substrate-wasm-builder", "tanssi-runtime-common", + "xcm-fee-payment-runtime-api", "xcm-primitives", ] @@ -3131,6 +3133,7 @@ dependencies = [ "westend-runtime", "westend-runtime-constants", "xcm-emulator", + "xcm-fee-payment-runtime-api", "xcm-primitives", ] diff --git a/Cargo.toml b/Cargo.toml index 4f73d649c..9ac6d8ddf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -232,6 +232,7 @@ staging-xcm-builder = { git = "https://github.com/moondance-labs/polkadot-sdk", staging-xcm-executor = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-v1.11.0", default-features = false } westend-runtime = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-v1.11.0", default-features = false } westend-runtime-constants = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-v1.11.0", default-features = false } +xcm-fee-payment-runtime-api = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-v1.11.0", default-features = false } # Polkadot (client) polkadot-cli = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-v1.11.0" } diff --git a/container-chains/runtime-templates/frontier/Cargo.toml b/container-chains/runtime-templates/frontier/Cargo.toml index 9796a8078..ce116ff1a 100644 --- a/container-chains/runtime-templates/frontier/Cargo.toml +++ b/container-chains/runtime-templates/frontier/Cargo.toml @@ -94,6 +94,7 @@ polkadot-runtime-common = { workspace = true } staging-xcm = { workspace = true } staging-xcm-builder = { workspace = true } staging-xcm-executor = { workspace = true } +xcm-fee-payment-runtime-api = { workspace = true } # Cumulus cumulus-pallet-dmp-queue = { workspace = true } @@ -226,6 +227,7 @@ std = [ "staging-xcm-executor/std", "staging-xcm/std", "tanssi-runtime-common/std", + "xcm-fee-payment-runtime-api/std", "xcm-primitives/std", ] diff --git a/container-chains/runtime-templates/frontier/src/lib.rs b/container-chains/runtime-templates/frontier/src/lib.rs index 4ddf47ae5..7e81e257c 100644 --- a/container-chains/runtime-templates/frontier/src/lib.rs +++ b/container-chains/runtime-templates/frontier/src/lib.rs @@ -49,16 +49,17 @@ use { pallet_prelude::DispatchResult, parameter_types, traits::{ - ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, Contains, Currency as CurrencyT, - FindAuthor, Imbalance, InsideBoth, InstanceFilter, OnFinalize, OnUnbalanced, + tokens::ConversionToAssetBalance, ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, + Contains, Currency as CurrencyT, FindAuthor, Imbalance, InsideBoth, InstanceFilter, + OnFinalize, OnUnbalanced, }, weights::{ constants::{ BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND, }, - ConstantMultiplier, Weight, WeightToFeeCoefficient, WeightToFeeCoefficients, - WeightToFeePolynomial, + ConstantMultiplier, Weight, WeightToFee as _, WeightToFeeCoefficient, + WeightToFeeCoefficients, WeightToFeePolynomial, }, }, frame_system::{ @@ -93,6 +94,10 @@ use { }, sp_std::prelude::*, sp_version::RuntimeVersion, + staging_xcm::{ + IntoVersion, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm, + }, + xcm_fee_payment_runtime_api::Error as XcmPaymentApiError, }; pub use { sp_consensus_aura::sr25519::AuthorityId as AuraId, @@ -1648,6 +1653,62 @@ impl_runtime_apis! { SLOT_DURATION } } + + impl xcm_fee_payment_runtime_api::XcmPaymentApi for Runtime { + fn query_acceptable_payment_assets(xcm_version: staging_xcm::Version) -> Result, XcmPaymentApiError> { + if !matches!(xcm_version, 3 | 4) { + return Err(XcmPaymentApiError::UnhandledXcmVersion); + } + + Ok([VersionedAssetId::V4(xcm_config::SelfReserve::get().into())] + .into_iter() + .chain( + pallet_asset_rate::ConversionRateToNative::::iter_keys().filter_map(|asset_id_u16| { + pallet_foreign_asset_creator::AssetIdToForeignAsset::::get(asset_id_u16).map(|location| { + VersionedAssetId::V4(location.into()) + }).or_else(|| { + log::warn!("Asset `{}` is present in pallet_asset_rate but not in pallet_foreign_asset_creator", asset_id_u16); + None + }) + }) + ) + .filter_map(|asset| asset.into_version(xcm_version).map_err(|e| { + log::warn!("Failed to convert asset to version {}: {:?}", xcm_version, e); + }).ok()) + .collect()) + } + + fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result { + let local_asset = VersionedAssetId::V4(xcm_config::SelfReserve::get().into()); + let asset = asset + .into_version(4) + .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; + + if asset == local_asset { + Ok(WeightToFee::weight_to_fee(&weight)) + } else { + let native_fee = WeightToFee::weight_to_fee(&weight); + let asset_v4: staging_xcm::opaque::lts::AssetId = asset.try_into().map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; + let location: staging_xcm::opaque::lts::Location = asset_v4.0; + let asset_id = pallet_foreign_asset_creator::ForeignAssetToAssetId::::get(location).ok_or(XcmPaymentApiError::AssetNotFound)?; + let asset_rate = AssetRate::to_asset_balance(native_fee, asset_id); + match asset_rate { + Ok(x) => Ok(x), + Err(pallet_asset_rate::Error::UnknownAssetKind) => Err(XcmPaymentApiError::AssetNotFound), + // Error when converting native balance to asset balance, probably overflow + Err(_e) => Err(XcmPaymentApiError::WeightNotComputable), + } + } + } + + 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) + } + } } #[allow(dead_code)] diff --git a/container-chains/runtime-templates/simple/Cargo.toml b/container-chains/runtime-templates/simple/Cargo.toml index 24b33d1dd..91575f69c 100644 --- a/container-chains/runtime-templates/simple/Cargo.toml +++ b/container-chains/runtime-templates/simple/Cargo.toml @@ -84,6 +84,7 @@ polkadot-runtime-common = { workspace = true } staging-xcm = { workspace = true } staging-xcm-builder = { workspace = true } staging-xcm-executor = { workspace = true } +xcm-fee-payment-runtime-api = { workspace = true } # Cumulus cumulus-pallet-dmp-queue = { workspace = true } @@ -180,6 +181,7 @@ std = [ "staging-xcm-executor/std", "staging-xcm/std", "tanssi-runtime-common/std", + "xcm-fee-payment-runtime-api/std", "xcm-primitives/std", ] diff --git a/container-chains/runtime-templates/simple/src/lib.rs b/container-chains/runtime-templates/simple/src/lib.rs index 5d2e137c8..b328cce9b 100644 --- a/container-chains/runtime-templates/simple/src/lib.rs +++ b/container-chains/runtime-templates/simple/src/lib.rs @@ -43,15 +43,16 @@ use { pallet_prelude::DispatchResult, parameter_types, traits::{ - ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, Contains, InsideBoth, InstanceFilter, + tokens::ConversionToAssetBalance, ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, + Contains, InsideBoth, InstanceFilter, }, weights::{ constants::{ BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND, }, - ConstantMultiplier, Weight, WeightToFeeCoefficient, WeightToFeeCoefficients, - WeightToFeePolynomial, + ConstantMultiplier, Weight, WeightToFee as _, WeightToFeeCoefficient, + WeightToFeeCoefficients, WeightToFeePolynomial, }, }, frame_system::{ @@ -75,6 +76,10 @@ use { }, sp_std::prelude::*, sp_version::RuntimeVersion, + staging_xcm::{ + IntoVersion, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm, + }, + xcm_fee_payment_runtime_api::Error as XcmPaymentApiError, }; pub mod xcm_config; @@ -1126,6 +1131,62 @@ impl_runtime_apis! { SLOT_DURATION } } + + impl xcm_fee_payment_runtime_api::XcmPaymentApi for Runtime { + fn query_acceptable_payment_assets(xcm_version: staging_xcm::Version) -> Result, XcmPaymentApiError> { + if !matches!(xcm_version, 3 | 4) { + return Err(XcmPaymentApiError::UnhandledXcmVersion); + } + + Ok([VersionedAssetId::V4(xcm_config::SelfReserve::get().into())] + .into_iter() + .chain( + pallet_asset_rate::ConversionRateToNative::::iter_keys().filter_map(|asset_id_u16| { + pallet_foreign_asset_creator::AssetIdToForeignAsset::::get(asset_id_u16).map(|location| { + VersionedAssetId::V4(location.into()) + }).or_else(|| { + log::warn!("Asset `{}` is present in pallet_asset_rate but not in pallet_foreign_asset_creator", asset_id_u16); + None + }) + }) + ) + .filter_map(|asset| asset.into_version(xcm_version).map_err(|e| { + log::warn!("Failed to convert asset to version {}: {:?}", xcm_version, e); + }).ok()) + .collect()) + } + + fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result { + let local_asset = VersionedAssetId::V4(xcm_config::SelfReserve::get().into()); + let asset = asset + .into_version(4) + .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; + + if asset == local_asset { + Ok(WeightToFee::weight_to_fee(&weight)) + } else { + let native_fee = WeightToFee::weight_to_fee(&weight); + let asset_v4: staging_xcm::opaque::lts::AssetId = asset.try_into().map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; + let location: staging_xcm::opaque::lts::Location = asset_v4.0; + let asset_id = pallet_foreign_asset_creator::ForeignAssetToAssetId::::get(location).ok_or(XcmPaymentApiError::AssetNotFound)?; + let asset_rate = AssetRate::to_asset_balance(native_fee, asset_id); + match asset_rate { + Ok(x) => Ok(x), + Err(pallet_asset_rate::Error::UnknownAssetKind) => Err(XcmPaymentApiError::AssetNotFound), + // Error when converting native balance to asset balance, probably overflow + Err(_e) => Err(XcmPaymentApiError::WeightNotComputable), + } + } + } + + 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) + } + } } #[allow(dead_code)] diff --git a/runtime/dancebox/Cargo.toml b/runtime/dancebox/Cargo.toml index cf1325f45..be4cd7859 100644 --- a/runtime/dancebox/Cargo.toml +++ b/runtime/dancebox/Cargo.toml @@ -107,6 +107,7 @@ polkadot-runtime-common = { workspace = true } staging-xcm = { workspace = true } staging-xcm-builder = { workspace = true } staging-xcm-executor = { workspace = true } +xcm-fee-payment-runtime-api = { workspace = true } # Cumulus cumulus-pallet-dmp-queue = { workspace = true } @@ -270,6 +271,7 @@ std = [ "tp-traits/std", "westend-runtime-constants/std", "westend-runtime/std", + "xcm-fee-payment-runtime-api/std", "xcm-primitives/std", ] diff --git a/runtime/dancebox/src/lib.rs b/runtime/dancebox/src/lib.rs index 04002d3f0..bbf4a125e 100644 --- a/runtime/dancebox/src/lib.rs +++ b/runtime/dancebox/src/lib.rs @@ -51,8 +51,8 @@ use { traits::{ fungible::{Balanced, Credit, Inspect, InspectHold, Mutate, MutateHold}, tokens::{ - imbalance::ResolveTo, PayFromAccount, Precision, Preservation, - UnityAssetBalanceConversion, + imbalance::ResolveTo, ConversionToAssetBalance, PayFromAccount, Precision, + Preservation, UnityAssetBalanceConversion, }, ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, Contains, EitherOfDiverse, Imbalance, InsideBoth, InstanceFilter, OnUnbalanced, ValidatorRegistration, @@ -62,8 +62,8 @@ use { BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND, }, - ConstantMultiplier, Weight, WeightToFeeCoefficient, WeightToFeeCoefficients, - WeightToFeePolynomial, + ConstantMultiplier, Weight, WeightToFee as _, WeightToFeeCoefficient, + WeightToFeeCoefficients, WeightToFeePolynomial, }, PalletId, }, @@ -102,10 +102,14 @@ use { }, sp_std::{collections::btree_set::BTreeSet, marker::PhantomData, prelude::*}, sp_version::RuntimeVersion, + staging_xcm::{ + IntoVersion, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm, + }, tp_traits::{ GetContainerChainAuthor, GetHostConfiguration, GetSessionContainerChains, RelayStorageRootProvider, RemoveInvulnerables, RemoveParaIdsWithNoCredits, SlotFrequency, }, + xcm_fee_payment_runtime_api::Error as XcmPaymentApiError, }; pub use { dp_core::{AccountId, Address, Balance, BlockNumber, Hash, Header, Index, Signature}, @@ -2607,6 +2611,62 @@ impl_runtime_apis! { XcmCoreBuyer::is_core_buying_allowed(para_id) } } + + impl xcm_fee_payment_runtime_api::XcmPaymentApi for Runtime { + fn query_acceptable_payment_assets(xcm_version: staging_xcm::Version) -> Result, XcmPaymentApiError> { + if !matches!(xcm_version, 3 | 4) { + return Err(XcmPaymentApiError::UnhandledXcmVersion); + } + + Ok([VersionedAssetId::V4(xcm_config::SelfReserve::get().into())] + .into_iter() + .chain( + pallet_asset_rate::ConversionRateToNative::::iter_keys().filter_map(|asset_id_u16| { + pallet_foreign_asset_creator::AssetIdToForeignAsset::::get(asset_id_u16).map(|location| { + VersionedAssetId::V4(location.into()) + }).or_else(|| { + log::warn!("Asset `{}` is present in pallet_asset_rate but not in pallet_foreign_asset_creator", asset_id_u16); + None + }) + }) + ) + .filter_map(|asset| asset.into_version(xcm_version).map_err(|e| { + log::warn!("Failed to convert asset to version {}: {:?}", xcm_version, e); + }).ok()) + .collect()) + } + + fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result { + let local_asset = VersionedAssetId::V4(xcm_config::SelfReserve::get().into()); + let asset = asset + .into_version(4) + .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; + + if asset == local_asset { + Ok(WeightToFee::weight_to_fee(&weight)) + } else { + let native_fee = WeightToFee::weight_to_fee(&weight); + let asset_v4: staging_xcm::opaque::lts::AssetId = asset.try_into().map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; + let location: staging_xcm::opaque::lts::Location = asset_v4.0; + let asset_id = pallet_foreign_asset_creator::ForeignAssetToAssetId::::get(location).ok_or(XcmPaymentApiError::AssetNotFound)?; + let asset_rate = AssetRate::to_asset_balance(native_fee, asset_id); + match asset_rate { + Ok(x) => Ok(x), + Err(pallet_asset_rate::Error::UnknownAssetKind) => Err(XcmPaymentApiError::AssetNotFound), + // Error when converting native balance to asset balance, probably overflow + Err(_e) => Err(XcmPaymentApiError::WeightNotComputable), + } + } + } + + 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) + } + } } #[allow(dead_code)] diff --git a/test/suites/common-xcm/xcm/test-xcm-payment-api.ts b/test/suites/common-xcm/xcm/test-xcm-payment-api.ts new file mode 100644 index 000000000..051caa1e3 --- /dev/null +++ b/test/suites/common-xcm/xcm/test-xcm-payment-api.ts @@ -0,0 +1,202 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import { KeyringPair, alith } from "@moonwall/util"; +import { ApiPromise, Keyring, WsProvider } from "@polkadot/api"; +import { STATEMINT_LOCATION_EXAMPLE } from "../../../util/constants.ts"; + +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", + }, + }, + version: 1, + }, + ], + }, + types: { + XcmPaymentApiError: { + _enum: { + Unimplemented: "Null", + VersionedConversionFailed: "Null", + WeightNotComputable: "Null", + UnhandledXcmVersion: "Null", + AssetNotFound: "Null", + }, + }, + }, +}; + +describeSuite({ + id: "CX0207", + title: "XCM - XcmPaymentApi", + foundationMethods: "dev", + testCases: ({ context, it }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let chain; + + beforeAll(async function () { + // Not using context.polkadotJs() because we need to add the runtime api + // This won't be needed after @polkadot/api adds the XcmPaymentApi + polkadotJs = await ApiPromise.create({ + provider: new WsProvider(`ws://localhost:${process.env.MOONWALL_RPC_PORT}/`), + ...runtimeApi, + }); + chain = polkadotJs.consts.system.version.specName.toString(); + alice = + chain == "frontier-template" + ? alith + : new Keyring({ type: "sr25519" }).addFromUri("//Alice", { + name: "Alice default", + }); + + // We register the token + const txSigned = polkadotJs.tx.sudo.sudo( + polkadotJs.tx.utility.batch([ + polkadotJs.tx.foreignAssetsCreator.createForeignAsset( + STATEMINT_LOCATION_EXAMPLE, + 1, + alice.address, + true, + 1 + ), + polkadotJs.tx.assetRate.create( + 1, + // this defines how much the asset costs with respect to the + // new asset + // in this case, asset*2=native + // that means that we will charge 0.5 of the native balance + 2000000000000000000n + ), + ]) + ); + + await context.createBlock(await txSigned.signAsync(alice), { + allowFailures: false, + }); + }); + + it({ + id: "T01", + title: "Should succeed calling runtime api", + test: async function () { + const chainInfo = polkadotJs.registry.getChainProperties(); + const metadata = await polkadotJs.rpc.state.getMetadata(); + const balancesPalletIndex = metadata.asLatest.pallets + .find(({ name }) => name.toString() == "Balances")! + .index.toNumber(); + + console.log(chainInfo.toHuman()); + + const assets = await polkadotJs.call.xcmPaymentApi.queryAcceptablePaymentAssets(3); + const weightToNativeAssets = await polkadotJs.call.xcmPaymentApi.queryWeightToAssetFee( + { + refTime: 10_000_000_000n, + profSize: 0n, + }, + { + V3: { + Concrete: { + parents: 0, + interior: { + X1: { PalletInstance: Number(balancesPalletIndex) }, + }, + }, + }, + } + ); + + const weightToForeingAssets = await polkadotJs.call.xcmPaymentApi.queryWeightToAssetFee( + { + refTime: 10_000_000_000n, + profSize: 0n, + }, + { + V3: { + Concrete: STATEMINT_LOCATION_EXAMPLE, + }, + } + ); + + const transactWeightAtMost = { + refTime: 200000000n, + proofSize: 3000n, + }; + const xcmToWeight = await polkadotJs.call.xcmPaymentApi.queryXcmWeight({ + V3: [ + { + Transact: { + originKind: "Superuser", + requireWeightAtMost: transactWeightAtMost, + call: { + encoded: + "0x0408001cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c0284d717", + }, + }, + }, + ], + }); + // Uncomment to debug if test fails + /* + console.log( + "assets:", + JSON.stringify(assets.toJSON()), + "\nweightToNativeAsset: ", + weightToNativeAssets.toHuman(), + "\nweightToForeingAsset: ", + weightToForeingAssets.toHuman(), + "\nxcmToWeight: ", + xcmToWeight.toHuman() + ); + */ + + expect(assets.isOk).to.be.true; + // Includes the native asset and the asset registered in foreignAssetsCreator + expect(assets.asOk.toJSON().length).to.be.equal(2); + expect(xcmToWeight.isOk).to.be.true; + // Weight estimated by queryXcmWeight will always be greater than the weight passed to the transact call as requireWeightAtMost + expect(xcmToWeight.asOk.refTime.toBigInt() > transactWeightAtMost.refTime).to.be.true; + expect(xcmToWeight.asOk.proofSize.toBigInt() > transactWeightAtMost.proofSize).to.be.true; + + // foreign*2=native + const diff = weightToNativeAssets.asOk.toBigInt() - 2n * weightToForeingAssets.asOk.toBigInt(); + // Allow rounding error of +/- 1 + expect(diff >= -1n && diff <= 1n).to.be.true; + }, + }); + }, +});