From e4f87ba361dc6b41dca3eaad57ff449070d21d55 Mon Sep 17 00:00:00 2001 From: Francisco Aguirre Date: Wed, 8 May 2024 09:29:40 +0200 Subject: [PATCH] XcmDryRunApi - Dry-running extrinsics to get their XCM effects (#3872) Estimating fees for XCM execution and sending has been an area with bad UX. The addition of the [XcmPaymentApi](https://github.com/paritytech/polkadot-sdk/pull/3607) exposed the necessary components to be able to estimate XCM fees correctly, however, that was not the full story. The `XcmPaymentApi` works for estimating fees only if you know the specific XCM you want to execute or send. This is necessary but most UIs want to estimate the fees for extrinsics, they don't necessarily know the XCM program that's executed by them. A new runtime API is introduced, the `XcmDryRunApi`, that given an extrinsic, or an XCM program, returns its effects: - Execution result - Local XCM (in the case of an extrinsic) - Forwarded XCMs - List of events This API can be used on its own for dry-running purposes, for double-checking or testing, but it mainly shines when used in conjunction with the `XcmPaymentApi`. UIs can use these two APIs to estimate transfers. New tests are added to exemplify how to incorporate both APIs. There's a mock test just to make sure everything works under `xcm-fee-payment-runtime-api`. There's a real-world test using Westend and AssetHubWestend under `cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/xcm_fee_estimation.rs`. Added both a test for a simple teleport between chains and a reserve transfer asset between two parachains going through a reserve. The steps to follow: - Use `XcmDryRunApi::dry_run_extrinsic` to get local XCM program and forwarded messages - For each forwarded message - Use `XcmPaymentApi::query_delivery_fee` LOCALLY to get the delivery fees - Use `XcmPaymentApi::query_xcm_weight` ON THE DESTINATION to get the remote execution weight - (optional) Use `XcmPaymentApi::query_acceptable_payment_assets` ON THE DESTINATION to know on which assets the execution fees can be paid - Use `XcmPaymentApi::query_weight_to_asset_fee` ON THE DESTINATION to convert weight to the actual remote execution fees - Use `XcmDryRunApi::dry_run_xcm` ON THE DESTINATION to know if a new message will be forwarded, if so, continue The changes in this PR are grouped as follows, and in order of importance: - Addition of new runtime API - Definition, mock and simple tests: polkadot/xcm/xcm-fee-payment-runtime-api/* - Implemented on Westend, Asset Hub Westend and Penpal, will implement on every runtime in a following PR - Addition of a new config item to the XCM executor for recording xcms about to be executed - Definition: polkadot/xcm/xcm-executor/* - Implementation: polkadot/xcm/pallet-xcm/* - had to update all runtime xcm_config.rs files with `type XcmRecorder = XcmPallet;` - Addition of a new trait for inspecting the messages in queues - Definition: polkadot/xcm/xcm-builder/src/routing.rs - Implemented it on all routers: - ChildParachainRouter: polkadot/runtime/common/src/xcm_sender.rs - ParentAsUmp: cumulus/primitives/utility/src/lib.rs (piggybacked on implementation in cumulus/pallets/parachain-system/src/lib.rs) - XcmpQueue: cumulus/pallets/xcmp-queue/src/lib.rs - Bridge: bridges/modules/xcm-bridge-hub-router/src/lib.rs - More complicated and useful tests: - cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/xcm_fee_estimation.rs With this PR, Westend, AssetHubWestend, Rococo and AssetHubRococo have the new API. UIs can test on these runtimes to create better experiences around cross-chain operations. Next: - Add XcmDryRunApi to all system parachains - Integrate xcm fee estimation in all emulated tests - Get this on the fellowship runtimes --------- Co-authored-by: Adrian Catangiu --- Cargo.lock | 20 + .../modules/xcm-bridge-hub-router/src/lib.rs | 43 +- .../modules/xcm-bridge-hub-router/src/mock.rs | 47 +- cumulus/pallets/parachain-system/Cargo.toml | 3 + cumulus/pallets/parachain-system/src/lib.rs | 16 +- cumulus/pallets/xcmp-queue/Cargo.toml | 5 +- cumulus/pallets/xcmp-queue/src/lib.rs | 35 +- cumulus/pallets/xcmp-queue/src/mock.rs | 1 + cumulus/pallets/xcmp-queue/src/tests.rs | 40 ++ .../emulated/chains/relays/westend/Cargo.toml | 2 + .../tests/assets/asset-hub-westend/Cargo.toml | 6 + .../assets/asset-hub-westend/src/tests/mod.rs | 1 + .../src/tests/xcm_fee_estimation.rs | 370 ++++++++++++ .../assets/asset-hub-rococo/Cargo.toml | 1 + .../assets/asset-hub-rococo/src/lib.rs | 71 ++- .../assets/asset-hub-rococo/src/xcm_config.rs | 1 + .../assets/asset-hub-westend/Cargo.toml | 1 + .../assets/asset-hub-westend/src/lib.rs | 157 ++++-- .../asset-hub-westend/src/xcm_config.rs | 1 + .../bridge-hub-rococo/src/xcm_config.rs | 1 + .../bridge-hub-westend/src/xcm_config.rs | 1 + .../collectives-westend/src/xcm_config.rs | 1 + .../contracts-rococo/src/xcm_config.rs | 1 + .../coretime-rococo/src/xcm_config.rs | 1 + .../coretime-westend/src/xcm_config.rs | 1 + .../glutton/glutton-westend/src/xcm_config.rs | 1 + .../people/people-rococo/src/xcm_config.rs | 1 + .../people/people-westend/src/xcm_config.rs | 1 + .../runtimes/starters/shell/src/xcm_config.rs | 1 + .../runtimes/testing/penpal/Cargo.toml | 3 + .../runtimes/testing/penpal/src/lib.rs | 107 +++- .../runtimes/testing/penpal/src/xcm_config.rs | 1 + .../testing/rococo-parachain/src/lib.rs | 1 + cumulus/primitives/utility/src/lib.rs | 12 +- polkadot/node/service/Cargo.toml | 1 + polkadot/node/service/src/fake_runtime_api.rs | 20 +- polkadot/runtime/common/src/xcm_sender.rs | 21 +- polkadot/runtime/parachains/src/dmp.rs | 2 +- polkadot/runtime/rococo/Cargo.toml | 1 + polkadot/runtime/rococo/src/lib.rs | 67 ++- polkadot/runtime/rococo/src/xcm_config.rs | 1 + .../runtime/test-runtime/src/xcm_config.rs | 1 + polkadot/runtime/westend/Cargo.toml | 1 + polkadot/runtime/westend/src/lib.rs | 67 ++- polkadot/runtime/westend/src/xcm_config.rs | 1 + .../src/fungible/mock.rs | 1 + .../pallet-xcm-benchmarks/src/generic/mock.rs | 1 + polkadot/xcm/pallet-xcm/Cargo.toml | 1 + polkadot/xcm/pallet-xcm/src/lib.rs | 61 +- polkadot/xcm/pallet-xcm/src/mock.rs | 1 + polkadot/xcm/pallet-xcm/src/tests/mod.rs | 36 +- polkadot/xcm/xcm-builder/src/lib.rs | 4 +- polkadot/xcm/xcm-builder/src/routing.rs | 27 +- polkadot/xcm/xcm-builder/src/tests/mock.rs | 1 + .../xcm/xcm-builder/src/tests/pay/mock.rs | 1 + .../xcm/xcm-builder/src/universal_exports.rs | 9 + polkadot/xcm/xcm-builder/tests/mock/mod.rs | 1 + polkadot/xcm/xcm-executor/src/config.rs | 6 +- polkadot/xcm/xcm-executor/src/lib.rs | 9 + polkadot/xcm/xcm-executor/src/traits/mod.rs | 2 + .../xcm/xcm-executor/src/traits/record_xcm.rs | 46 ++ .../xcm-fee-payment-runtime-api/Cargo.toml | 31 ++ .../src/dry_run.rs | 83 +++ .../xcm-fee-payment-runtime-api/src/fees.rs | 97 ++++ .../xcm-fee-payment-runtime-api/src/lib.rs | 93 +--- .../tests/fee_estimation.rs | 370 ++++++++++++ .../xcm-fee-payment-runtime-api/tests/mock.rs | 525 ++++++++++++++++++ .../xcm/xcm-simulator/fuzzer/src/parachain.rs | 1 + .../xcm-simulator/fuzzer/src/relay_chain.rs | 1 + prdoc/pr_3872.prdoc | 86 +++ .../contracts/mock-network/src/parachain.rs | 1 + .../contracts/mock-network/src/relay_chain.rs | 1 + .../runtime/src/configs/xcm_config.rs | 1 + 73 files changed, 2462 insertions(+), 173 deletions(-) create mode 100644 cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/xcm_fee_estimation.rs create mode 100644 polkadot/xcm/xcm-executor/src/traits/record_xcm.rs create mode 100644 polkadot/xcm/xcm-fee-payment-runtime-api/src/dry_run.rs create mode 100644 polkadot/xcm/xcm-fee-payment-runtime-api/src/fees.rs create mode 100644 polkadot/xcm/xcm-fee-payment-runtime-api/tests/fee_estimation.rs create mode 100644 polkadot/xcm/xcm-fee-payment-runtime-api/tests/mock.rs create mode 100644 prdoc/pr_3872.prdoc diff --git a/Cargo.lock b/Cargo.lock index 931b6443c848..b94a452f9bc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -901,21 +901,27 @@ dependencies = [ "cumulus-pallet-xcmp-queue", "emulated-integration-tests-common", "frame-support", + "frame-system", "pallet-asset-conversion", + "pallet-asset-tx-payment", "pallet-assets", "pallet-balances", "pallet-message-queue", + "pallet-transaction-payment", "pallet-treasury", "pallet-xcm", "parachains-common", "parity-scale-codec", "penpal-runtime", "polkadot-runtime-common", + "sp-core", + "sp-keyring", "sp-runtime", "staging-xcm", "staging-xcm-executor", "westend-runtime", "westend-system-emulated-network", + "xcm-fee-payment-runtime-api", ] [[package]] @@ -4005,6 +4011,7 @@ dependencies = [ "sp-trie", "sp-version", "staging-xcm", + "staging-xcm-builder", "trie-db", "trie-standardmap", ] @@ -12303,6 +12310,7 @@ dependencies = [ "staging-xcm-builder", "staging-xcm-executor", "substrate-wasm-builder", + "xcm-fee-payment-runtime-api", ] [[package]] @@ -22800,8 +22808,10 @@ dependencies = [ "sp-consensus-beefy", "sp-core", "sp-runtime", + "staging-xcm", "westend-runtime", "westend-runtime-constants", + "xcm-fee-payment-runtime-api", ] [[package]] @@ -23413,14 +23423,24 @@ dependencies = [ name = "xcm-fee-payment-runtime-api" version = "0.1.0" dependencies = [ + "env_logger 0.11.3", + "frame-executive", "frame-support", + "frame-system", + "log", + "pallet-assets", + "pallet-balances", + "pallet-xcm", "parity-scale-codec", "scale-info", "sp-api", + "sp-io", "sp-runtime", "sp-std 14.0.0", "sp-weights", "staging-xcm", + "staging-xcm-builder", + "staging-xcm-executor", ] [[package]] diff --git a/bridges/modules/xcm-bridge-hub-router/src/lib.rs b/bridges/modules/xcm-bridge-hub-router/src/lib.rs index ece72ac8494b..607394603466 100644 --- a/bridges/modules/xcm-bridge-hub-router/src/lib.rs +++ b/bridges/modules/xcm-bridge-hub-router/src/lib.rs @@ -37,8 +37,9 @@ use codec::Encode; use frame_support::traits::Get; use sp_core::H256; use sp_runtime::{FixedPointNumber, FixedU128, Saturating}; +use sp_std::vec::Vec; use xcm::prelude::*; -use xcm_builder::{ExporterFor, SovereignPaidRemoteExporter}; +use xcm_builder::{ExporterFor, InspectMessageQueues, SovereignPaidRemoteExporter}; pub use pallet::*; pub use weights::WeightInfo; @@ -95,7 +96,7 @@ pub mod pallet { /// Origin of the sibling bridge hub that is allowed to report bridge status. type BridgeHubOrigin: EnsureOrigin; /// Actual message sender (`HRMP` or `DMP`) to the sibling bridge hub location. - type ToBridgeHubSender: SendXcm; + type ToBridgeHubSender: SendXcm + InspectMessageQueues; /// Underlying channel with the sibling bridge hub. It must match the channel, used /// by the `Self::ToBridgeHubSender`. type WithBridgeHubChannel: XcmChannelStatusProvider; @@ -396,6 +397,12 @@ impl, I: 'static> SendXcm for Pallet { } } +impl, I: 'static> InspectMessageQueues for Pallet { + fn get_messages() -> Vec<(VersionedLocation, Vec>)> { + ViaBridgeHubExporter::::get_messages() + } +} + #[cfg(test)] mod tests { use super::*; @@ -635,4 +642,36 @@ mod tests { ); }); } + + #[test] + fn get_messages_works() { + run_test(|| { + assert_ok!(send_xcm::( + (Parent, Parent, GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)).into(), + vec![ClearOrigin].into() + )); + assert_eq!( + XcmBridgeHubRouter::get_messages(), + vec![( + VersionedLocation::V4((Parent, Parachain(1002)).into()), + vec![VersionedXcm::V4( + Xcm::builder() + .withdraw_asset((Parent, 1_002_000)) + .buy_execution((Parent, 1_002_000), Unlimited) + .set_appendix( + Xcm::builder_unsafe() + .deposit_asset(AllCounted(1), (Parent, Parachain(1000))) + .build() + ) + .export_message( + Kusama, + Parachain(1000), + Xcm::builder_unsafe().clear_origin().build() + ) + .build() + )], + ),], + ); + }); + } } diff --git a/bridges/modules/xcm-bridge-hub-router/src/mock.rs b/bridges/modules/xcm-bridge-hub-router/src/mock.rs index 20c86d1da9a2..3e2c1bb369cb 100644 --- a/bridges/modules/xcm-bridge-hub-router/src/mock.rs +++ b/bridges/modules/xcm-bridge-hub-router/src/mock.rs @@ -19,14 +19,16 @@ use crate as pallet_xcm_bridge_hub_router; use bp_xcm_bridge_hub_router::XcmChannelStatusProvider; +use codec::Encode; use frame_support::{ construct_runtime, derive_impl, parameter_types, traits::{Contains, Equals}, }; use frame_system::EnsureRoot; use sp_runtime::{traits::ConstU128, BuildStorage}; +use sp_std::cell::RefCell; use xcm::prelude::*; -use xcm_builder::{NetworkExportTable, NetworkExportTableItem}; +use xcm_builder::{InspectMessageQueues, NetworkExportTable, NetworkExportTableItem}; pub type AccountId = u64; type Block = frame_system::mocking::MockBlock; @@ -102,23 +104,46 @@ pub struct TestToBridgeHubSender; impl TestToBridgeHubSender { pub fn is_message_sent() -> bool { - frame_support::storage::unhashed::get_or_default(b"TestToBridgeHubSender.Sent") + !Self::get_messages().is_empty() } } +thread_local! { + pub static SENT_XCM: RefCell)>> = RefCell::new(Vec::new()); +} + impl SendXcm for TestToBridgeHubSender { - type Ticket = (); + type Ticket = (Location, Xcm<()>); fn validate( - _destination: &mut Option, - _message: &mut Option>, + destination: &mut Option, + message: &mut Option>, ) -> SendResult { - Ok(((), (BridgeFeeAsset::get(), HRMP_FEE).into())) + let pair = (destination.take().unwrap(), message.take().unwrap()); + Ok((pair, (BridgeFeeAsset::get(), HRMP_FEE).into())) } - fn deliver(_ticket: Self::Ticket) -> Result { - frame_support::storage::unhashed::put(b"TestToBridgeHubSender.Sent", &true); - Ok([0u8; 32]) + fn deliver(pair: Self::Ticket) -> Result { + let hash = fake_message_hash(&pair.1); + SENT_XCM.with(|q| q.borrow_mut().push(pair)); + Ok(hash) + } +} + +impl InspectMessageQueues for TestToBridgeHubSender { + fn get_messages() -> Vec<(VersionedLocation, Vec>)> { + SENT_XCM.with(|q| { + (*q.borrow()) + .clone() + .iter() + .map(|(location, message)| { + ( + VersionedLocation::V4(location.clone()), + vec![VersionedXcm::V4(message.clone())], + ) + }) + .collect() + }) } } @@ -146,3 +171,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities { pub fn run_test(test: impl FnOnce() -> T) -> T { new_test_ext().execute_with(test) } + +pub(crate) fn fake_message_hash(message: &Xcm) -> XcmHash { + message.using_encoded(sp_io::hashing::blake2_256) +} diff --git a/cumulus/pallets/parachain-system/Cargo.toml b/cumulus/pallets/parachain-system/Cargo.toml index cc2e8943caad..57e274db361d 100644 --- a/cumulus/pallets/parachain-system/Cargo.toml +++ b/cumulus/pallets/parachain-system/Cargo.toml @@ -38,6 +38,7 @@ polkadot-parachain-primitives = { path = "../../../polkadot/parachain", default- polkadot-runtime-parachains = { path = "../../../polkadot/runtime/parachains", default-features = false } polkadot-runtime-common = { path = "../../../polkadot/runtime/common", default-features = false, optional = true } xcm = { package = "staging-xcm", path = "../../../polkadot/xcm", default-features = false } +xcm-builder = { package = "staging-xcm-builder", path = "../../../polkadot/xcm/xcm-builder", default-features = false } # Cumulus cumulus-pallet-parachain-system-proc-macro = { path = "proc-macro", default-features = false } @@ -95,6 +96,7 @@ std = [ "sp-tracing/std", "sp-trie/std", "trie-db/std", + "xcm-builder/std", "xcm/std", ] @@ -109,6 +111,7 @@ runtime-benchmarks = [ "polkadot-runtime-common/runtime-benchmarks", "polkadot-runtime-parachains/runtime-benchmarks", "sp-runtime/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", ] try-runtime = [ diff --git a/cumulus/pallets/parachain-system/src/lib.rs b/cumulus/pallets/parachain-system/src/lib.rs index 2e92f9197693..a1f95eb29cf3 100644 --- a/cumulus/pallets/parachain-system/src/lib.rs +++ b/cumulus/pallets/parachain-system/src/lib.rs @@ -55,7 +55,8 @@ use sp_runtime::{ BoundedSlice, FixedU128, RuntimeDebug, Saturating, }; use sp_std::{cmp, collections::btree_map::BTreeMap, prelude::*}; -use xcm::latest::XcmHash; +use xcm::{latest::XcmHash, VersionedLocation, VersionedXcm}; +use xcm_builder::InspectMessageQueues; mod benchmarking; pub mod migration; @@ -1608,6 +1609,19 @@ impl UpwardMessageSender for Pallet { } } +impl InspectMessageQueues for Pallet { + fn get_messages() -> Vec<(VersionedLocation, Vec>)> { + use xcm::prelude::*; + + let messages: Vec> = PendingUpwardMessages::::get() + .iter() + .map(|encoded_message| VersionedXcm::<()>::decode(&mut &encoded_message[..]).unwrap()) + .collect(); + + vec![(VersionedLocation::V4(Parent.into()), messages)] + } +} + #[cfg(feature = "runtime-benchmarks")] impl polkadot_runtime_common::xcm_sender::EnsureForParachain for Pallet { fn ensure(para_id: ParaId) { diff --git a/cumulus/pallets/xcmp-queue/Cargo.toml b/cumulus/pallets/xcmp-queue/Cargo.toml index ab196c6d3ec6..e3530ef7bf0e 100644 --- a/cumulus/pallets/xcmp-queue/Cargo.toml +++ b/cumulus/pallets/xcmp-queue/Cargo.toml @@ -28,6 +28,7 @@ polkadot-runtime-common = { path = "../../../polkadot/runtime/common", default-f polkadot-runtime-parachains = { path = "../../../polkadot/runtime/parachains", default-features = false } xcm = { package = "staging-xcm", path = "../../../polkadot/xcm", default-features = false } xcm-executor = { package = "staging-xcm-executor", path = "../../../polkadot/xcm/xcm-executor", default-features = false } +xcm-builder = { package = "staging-xcm-builder", path = "../../../polkadot/xcm/xcm-builder", default-features = false } # Cumulus cumulus-primitives-core = { path = "../../primitives/core", default-features = false } @@ -46,9 +47,6 @@ sp-core = { path = "../../../substrate/primitives/core" } pallet-balances = { path = "../../../substrate/frame/balances" } frame-support = { path = "../../../substrate/frame/support", features = ["experimental"] } -# Polkadot -xcm-builder = { package = "staging-xcm-builder", path = "../../../polkadot/xcm/xcm-builder" } - # Cumulus cumulus-pallet-parachain-system = { path = "../parachain-system", features = ["parameterized-consensus-hook"] } @@ -71,6 +69,7 @@ std = [ "sp-io/std", "sp-runtime/std", "sp-std/std", + "xcm-builder/std", "xcm-executor/std", "xcm/std", ] diff --git a/cumulus/pallets/xcmp-queue/src/lib.rs b/cumulus/pallets/xcmp-queue/src/lib.rs index 7de2fd809421..cc785b66150e 100644 --- a/cumulus/pallets/xcmp-queue/src/lib.rs +++ b/cumulus/pallets/xcmp-queue/src/lib.rs @@ -70,7 +70,8 @@ use scale_info::TypeInfo; use sp_core::MAX_POSSIBLE_ALLOCATION; use sp_runtime::{FixedU128, RuntimeDebug, Saturating}; use sp_std::prelude::*; -use xcm::{latest::prelude::*, VersionedXcm, WrapVersion, MAX_XCM_DECODE_DEPTH}; +use xcm::{latest::prelude::*, VersionedLocation, VersionedXcm, WrapVersion, MAX_XCM_DECODE_DEPTH}; +use xcm_builder::InspectMessageQueues; use xcm_executor::traits::ConvertOrigin; pub use pallet::*; @@ -947,6 +948,38 @@ impl SendXcm for Pallet { } } +impl InspectMessageQueues for Pallet { + fn get_messages() -> Vec<(VersionedLocation, Vec>)> { + use xcm::prelude::*; + + OutboundXcmpMessages::::iter() + .map(|(para_id, _, messages)| { + let mut data = &messages[..]; + let decoded_format = + XcmpMessageFormat::decode_with_depth_limit(MAX_XCM_DECODE_DEPTH, &mut data) + .unwrap(); + if decoded_format != XcmpMessageFormat::ConcatenatedVersionedXcm { + panic!("Unexpected format.") + } + let mut decoded_messages = Vec::new(); + while !data.is_empty() { + let decoded_message = VersionedXcm::<()>::decode_with_depth_limit( + MAX_XCM_DECODE_DEPTH, + &mut data, + ) + .unwrap(); + decoded_messages.push(decoded_message); + } + + ( + VersionedLocation::V4((Parent, Parachain(para_id.into())).into()), + decoded_messages, + ) + }) + .collect() + } +} + impl FeeTracker for Pallet { type Id = ParaId; diff --git a/cumulus/pallets/xcmp-queue/src/mock.rs b/cumulus/pallets/xcmp-queue/src/mock.rs index 9d9a723cf8b5..e258576aa3f6 100644 --- a/cumulus/pallets/xcmp-queue/src/mock.rs +++ b/cumulus/pallets/xcmp-queue/src/mock.rs @@ -178,6 +178,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = (); } pub type XcmRouter = ( diff --git a/cumulus/pallets/xcmp-queue/src/tests.rs b/cumulus/pallets/xcmp-queue/src/tests.rs index 0b41095828f2..f48e9eec3ac0 100644 --- a/cumulus/pallets/xcmp-queue/src/tests.rs +++ b/cumulus/pallets/xcmp-queue/src/tests.rs @@ -844,3 +844,43 @@ fn verify_fee_factor_increase_and_decrease() { assert!(DeliveryFeeFactor::::get(sibling_para_id) < FixedU128::from_float(1.63)); }); } + +#[test] +fn get_messages_works() { + new_test_ext().execute_with(|| { + use xcm_builder::InspectMessageQueues; + let sibling_para_id = ParaId::from(2001); + ParachainSystem::open_outbound_hrmp_channel_for_benchmarks_or_tests(sibling_para_id); + let destination: Location = (Parent, Parachain(sibling_para_id.into())).into(); + let other_sibling_para_id = ParaId::from(2002); + let other_destination: Location = (Parent, Parachain(other_sibling_para_id.into())).into(); + let message = Xcm(vec![ClearOrigin]); + assert_ok!(send_xcm::(destination.clone(), message.clone())); + assert_ok!(send_xcm::(destination.clone(), message.clone())); + assert_ok!(send_xcm::(destination.clone(), message.clone())); + ParachainSystem::open_outbound_hrmp_channel_for_benchmarks_or_tests(other_sibling_para_id); + assert_ok!(send_xcm::(other_destination.clone(), message.clone())); + assert_ok!(send_xcm::(other_destination.clone(), message)); + let queued_messages = XcmpQueue::get_messages(); + assert_eq!( + queued_messages, + vec![ + ( + VersionedLocation::V4(other_destination), + vec![ + VersionedXcm::V4(Xcm(vec![ClearOrigin])), + VersionedXcm::V4(Xcm(vec![ClearOrigin])), + ], + ), + ( + VersionedLocation::V4(destination), + vec![ + VersionedXcm::V4(Xcm(vec![ClearOrigin])), + VersionedXcm::V4(Xcm(vec![ClearOrigin])), + VersionedXcm::V4(Xcm(vec![ClearOrigin])), + ], + ), + ], + ); + }); +} diff --git a/cumulus/parachains/integration-tests/emulated/chains/relays/westend/Cargo.toml b/cumulus/parachains/integration-tests/emulated/chains/relays/westend/Cargo.toml index 20aedb50e6a1..e4688a1c9f02 100644 --- a/cumulus/parachains/integration-tests/emulated/chains/relays/westend/Cargo.toml +++ b/cumulus/parachains/integration-tests/emulated/chains/relays/westend/Cargo.toml @@ -25,6 +25,8 @@ pallet-staking = { path = "../../../../../../../substrate/frame/staking", defaul polkadot-primitives = { path = "../../../../../../../polkadot/primitives", default-features = false } westend-runtime-constants = { path = "../../../../../../../polkadot/runtime/westend/constants", default-features = false } westend-runtime = { path = "../../../../../../../polkadot/runtime/westend" } +xcm = { package = "staging-xcm", path = "../../../../../../../polkadot/xcm", default-features = false } +xcm-fee-payment-runtime-api = { path = "../../../../../../../polkadot/xcm/xcm-fee-payment-runtime-api", default-features = false } # Cumulus parachains-common = { path = "../../../../../common" } diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml index 00f4308324a9..0a2b0f6d45ee 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml @@ -16,18 +16,24 @@ assert_matches = "1.5.0" # Substrate sp-runtime = { path = "../../../../../../../substrate/primitives/runtime", default-features = false } +sp-keyring = { path = "../../../../../../../substrate/primitives/keyring", default-features = false } +sp-core = { path = "../../../../../../../substrate/primitives/core", default-features = false } frame-support = { path = "../../../../../../../substrate/frame/support", default-features = false } +frame-system = { path = "../../../../../../../substrate/frame/system", default-features = false } pallet-balances = { path = "../../../../../../../substrate/frame/balances", default-features = false } pallet-assets = { path = "../../../../../../../substrate/frame/assets", default-features = false } pallet-asset-conversion = { path = "../../../../../../../substrate/frame/asset-conversion", default-features = false } pallet-treasury = { path = "../../../../../../../substrate/frame/treasury", default-features = false } pallet-message-queue = { path = "../../../../../../../substrate/frame/message-queue", default-features = false } +pallet-transaction-payment = { path = "../../../../../../../substrate/frame/transaction-payment", default-features = false } +pallet-asset-tx-payment = { path = "../../../../../../../substrate/frame/transaction-payment/asset-tx-payment", default-features = false } # Polkadot polkadot-runtime-common = { path = "../../../../../../../polkadot/runtime/common" } xcm = { package = "staging-xcm", path = "../../../../../../../polkadot/xcm", default-features = false } xcm-executor = { package = "staging-xcm-executor", path = "../../../../../../../polkadot/xcm/xcm-executor", default-features = false } pallet-xcm = { path = "../../../../../../../polkadot/xcm/pallet-xcm", default-features = false } +xcm-fee-payment-runtime-api = { path = "../../../../../../../polkadot/xcm/xcm-fee-payment-runtime-api", default-features = false } westend-runtime = { path = "../../../../../../../polkadot/runtime/westend" } # Cumulus diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs index bf013697b4c7..61eb70524fc9 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs @@ -21,3 +21,4 @@ mod set_xcm_versions; mod swap; mod teleport; mod treasury; +mod xcm_fee_estimation; diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/xcm_fee_estimation.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/xcm_fee_estimation.rs new file mode 100644 index 000000000000..aeec9b44dab4 --- /dev/null +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/xcm_fee_estimation.rs @@ -0,0 +1,370 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests to ensure correct XCM fee estimation for cross-chain asset transfers. + +use crate::imports::*; + +use sp_keyring::AccountKeyring::Alice; +use sp_runtime::{generic, MultiSignature}; +use xcm_fee_payment_runtime_api::{ + dry_run::runtime_decl_for_xcm_dry_run_api::XcmDryRunApiV1, + fees::runtime_decl_for_xcm_payment_api::XcmPaymentApiV1, +}; + +/// We are able to dry-run and estimate the fees for a teleport between relay and system para. +/// Scenario: Alice on Westend relay chain wants to teleport WND to Asset Hub. +/// We want to know the fees using the `XcmDryRunApi` and `XcmPaymentApi`. +#[test] +fn teleport_relay_system_para_works() { + let destination: Location = Parachain(1000).into(); // Asset Hub. + let beneficiary_id = AssetHubWestendReceiver::get(); + let beneficiary: Location = AccountId32 { id: beneficiary_id.clone().into(), network: None } // Test doesn't allow specifying a network here. + .into(); // Beneficiary in Asset Hub. + let teleport_amount = 1_000_000_000_000; // One WND (12 decimals). + let assets: Assets = vec![(Here, teleport_amount).into()].into(); + + // We get them from the Westend closure. + let mut delivery_fees_amount = 0; + let mut remote_message = VersionedXcm::V4(Xcm(Vec::new())); + ::new_ext().execute_with(|| { + type Runtime = ::Runtime; + type RuntimeCall = ::RuntimeCall; + + let call = RuntimeCall::XcmPallet(pallet_xcm::Call::transfer_assets { + dest: Box::new(VersionedLocation::V4(destination.clone())), + beneficiary: Box::new(VersionedLocation::V4(beneficiary)), + assets: Box::new(VersionedAssets::V4(assets)), + fee_asset_item: 0, + weight_limit: Unlimited, + }); + let sender = Alice; // Is the same as `WestendSender`. + let extrinsic = construct_extrinsic_westend(sender, call); + let result = Runtime::dry_run_extrinsic(extrinsic).unwrap(); + assert_eq!(result.forwarded_xcms.len(), 1); + let (destination_to_query, messages_to_query) = &result.forwarded_xcms[0]; + assert_eq!(messages_to_query.len(), 1); + remote_message = messages_to_query[0].clone(); + let delivery_fees = + Runtime::query_delivery_fees(destination_to_query.clone(), remote_message.clone()) + .unwrap(); + delivery_fees_amount = get_amount_from_versioned_assets(delivery_fees); + }); + + // This is set in the AssetHubWestend closure. + let mut remote_execution_fees = 0; + ::execute_with(|| { + type Runtime = ::Runtime; + + let weight = Runtime::query_xcm_weight(remote_message.clone()).unwrap(); + remote_execution_fees = + Runtime::query_weight_to_asset_fee(weight, VersionedAssetId::V4(Parent.into())) + .unwrap(); + }); + + let test_args = TestContext { + sender: WestendSender::get(), // Alice. + receiver: AssetHubWestendReceiver::get(), // Bob in Asset Hub. + args: TestArgs::new_relay(destination, beneficiary_id, teleport_amount), + }; + let mut test = RelayToSystemParaTest::new(test_args); + + let sender_balance_before = test.sender.balance; + let receiver_balance_before = test.receiver.balance; + assert_eq!(sender_balance_before, 1_000_000_000_000_000_000); + assert_eq!(receiver_balance_before, 4_096_000_000_000); + + test.set_dispatchable::(transfer_assets); + test.assert(); + + let sender_balance_after = test.sender.balance; + let receiver_balance_after = test.receiver.balance; + + // We now know the exact fees. + assert_eq!( + sender_balance_after, + sender_balance_before - delivery_fees_amount - teleport_amount + ); + assert_eq!( + receiver_balance_after, + receiver_balance_before + teleport_amount - remote_execution_fees + ); +} + +/// We are able to dry-run and estimate the fees for a multi-hop XCM journey. +/// Scenario: Alice on PenpalA has some WND and wants to send them to PenpalB. +/// We want to know the fees using the `XcmDryRunApi` and `XcmPaymentApi`. +#[test] +fn multi_hop_works() { + let destination = PenpalA::sibling_location_of(PenpalB::para_id()); + let sender = PenpalASender::get(); + let amount_to_send = 1_000_000_000_000; // One WND (12 decimals). + let asset_owner = PenpalAssetOwner::get(); + let assets: Assets = (Parent, amount_to_send).into(); + let relay_native_asset_location = RelayLocation::get(); + let sender_as_seen_by_relay = Westend::child_location_of(PenpalA::para_id()); + let sov_of_sender_on_relay = Westend::sovereign_account_id_of(sender_as_seen_by_relay.clone()); + + // fund Parachain's sender account + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(asset_owner.clone()), + relay_native_asset_location.clone(), + sender.clone(), + amount_to_send * 2, + ); + + // fund the Parachain Origin's SA on Relay Chain with the native tokens held in reserve + Westend::fund_accounts(vec![(sov_of_sender_on_relay.clone().into(), amount_to_send * 2)]); + + // Init values for Parachain Destination + let beneficiary_id = PenpalBReceiver::get(); + let beneficiary: Location = AccountId32 { + id: beneficiary_id.clone().into(), + network: None, // Test doesn't allow specifying a network here. + } + .into(); + + // We get them from the PenpalA closure. + let mut delivery_fees_amount = 0; + let mut remote_message = VersionedXcm::V4(Xcm(Vec::new())); + ::execute_with(|| { + type Runtime = ::Runtime; + type RuntimeCall = ::RuntimeCall; + + let call = RuntimeCall::PolkadotXcm(pallet_xcm::Call::transfer_assets { + dest: Box::new(VersionedLocation::V4(destination.clone())), + beneficiary: Box::new(VersionedLocation::V4(beneficiary)), + assets: Box::new(VersionedAssets::V4(assets.clone())), + fee_asset_item: 0, + weight_limit: Unlimited, + }); + let sender = Alice; // Same as `PenpalASender`. + let extrinsic = construct_extrinsic_penpal(sender, call); + let result = Runtime::dry_run_extrinsic(extrinsic).unwrap(); + assert_eq!(result.forwarded_xcms.len(), 1); + let (destination_to_query, messages_to_query) = &result.forwarded_xcms[0]; + assert_eq!(messages_to_query.len(), 1); + remote_message = messages_to_query[0].clone(); + let delivery_fees = + Runtime::query_delivery_fees(destination_to_query.clone(), remote_message.clone()) + .unwrap(); + delivery_fees_amount = get_amount_from_versioned_assets(delivery_fees); + }); + + // This is set in the Westend closure. + let mut intermediate_execution_fees = 0; + let mut intermediate_delivery_fees_amount = 0; + let mut intermediate_remote_message = VersionedXcm::V4(Xcm::<()>(Vec::new())); + ::execute_with(|| { + type Runtime = ::Runtime; + type RuntimeCall = ::RuntimeCall; + + // First we get the execution fees. + let weight = Runtime::query_xcm_weight(remote_message.clone()).unwrap(); + intermediate_execution_fees = + Runtime::query_weight_to_asset_fee(weight, VersionedAssetId::V4(Here.into())).unwrap(); + + // We have to do this to turn `VersionedXcm<()>` into `VersionedXcm`. + let xcm_program = + VersionedXcm::V4(Xcm::::from(remote_message.clone().try_into().unwrap())); + + // Now we get the delivery fees to the final destination. + let result = + Runtime::dry_run_xcm(sender_as_seen_by_relay.clone().into(), xcm_program).unwrap(); + let (destination_to_query, messages_to_query) = &result.forwarded_xcms[0]; + // There's actually two messages here. + // One created when the message we sent from PenpalA arrived and was executed. + // The second one when we dry-run the xcm. + // We could've gotten the message from the queue without having to dry-run, but + // offchain applications would have to dry-run, so we do it here as well. + intermediate_remote_message = messages_to_query[0].clone(); + let delivery_fees = Runtime::query_delivery_fees( + destination_to_query.clone(), + intermediate_remote_message.clone(), + ) + .unwrap(); + intermediate_delivery_fees_amount = get_amount_from_versioned_assets(delivery_fees); + }); + + // Get the final execution fees in the destination. + let mut final_execution_fees = 0; + ::execute_with(|| { + type Runtime = ::Runtime; + + let weight = Runtime::query_xcm_weight(intermediate_remote_message.clone()).unwrap(); + final_execution_fees = + Runtime::query_weight_to_asset_fee(weight, VersionedAssetId::V4(Parent.into())) + .unwrap(); + }); + + // Dry-running is done. + PenpalA::reset_ext(); + Westend::reset_ext(); + PenpalB::reset_ext(); + + // Fund accounts again. + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(asset_owner), + relay_native_asset_location.clone(), + sender.clone(), + amount_to_send * 2, + ); + Westend::fund_accounts(vec![(sov_of_sender_on_relay.into(), amount_to_send * 2)]); + + // Actually run the extrinsic. + let test_args = TestContext { + sender: PenpalASender::get(), // Alice. + receiver: PenpalBReceiver::get(), // Bob in PenpalB. + args: TestArgs::new_para( + destination, + beneficiary_id.clone(), + amount_to_send, + assets, + None, + 0, + ), + }; + let mut test = ParaToParaThroughRelayTest::new(test_args); + + let sender_assets_before = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(relay_native_asset_location.clone(), &sender) + }); + let receiver_assets_before = PenpalB::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(relay_native_asset_location.clone(), &beneficiary_id) + }); + + test.set_dispatchable::(transfer_assets_para_to_para); + test.assert(); + + let sender_assets_after = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(relay_native_asset_location.clone(), &sender) + }); + let receiver_assets_after = PenpalB::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(relay_native_asset_location, &beneficiary_id) + }); + + // We know the exact fees on every hop. + assert_eq!( + sender_assets_after, + sender_assets_before - amount_to_send - delivery_fees_amount /* This is charged directly + * from the sender's + * account. */ + ); + assert_eq!( + receiver_assets_after, + receiver_assets_before + amount_to_send - + intermediate_execution_fees - + intermediate_delivery_fees_amount - + final_execution_fees + ); +} + +fn get_amount_from_versioned_assets(assets: VersionedAssets) -> u128 { + let latest_assets: Assets = assets.try_into().unwrap(); + let Fungible(amount) = latest_assets.inner()[0].fun else { + unreachable!("asset is fungible"); + }; + amount +} + +fn transfer_assets(test: RelayToSystemParaTest) -> DispatchResult { + ::XcmPallet::transfer_assets( + test.signed_origin, + bx!(test.args.dest.into()), + bx!(test.args.beneficiary.into()), + bx!(test.args.assets.into()), + test.args.fee_asset_item, + test.args.weight_limit, + ) +} + +fn transfer_assets_para_to_para(test: ParaToParaThroughRelayTest) -> DispatchResult { + ::PolkadotXcm::transfer_assets( + test.signed_origin, + bx!(test.args.dest.into()), + bx!(test.args.beneficiary.into()), + bx!(test.args.assets.into()), + test.args.fee_asset_item, + test.args.weight_limit, + ) +} + +// Constructs the SignedExtra component of an extrinsic for the Westend runtime. +fn construct_extrinsic_westend( + sender: sp_keyring::AccountKeyring, + call: westend_runtime::RuntimeCall, +) -> westend_runtime::UncheckedExtrinsic { + type Runtime = ::Runtime; + let account_id = ::AccountId::from(sender.public()); + let tip = 0; + let extra: westend_runtime::SignedExtra = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckMortality::::from(sp_runtime::generic::Era::immortal()), + frame_system::CheckNonce::::from( + frame_system::Pallet::::account(&account_id).nonce, + ), + frame_system::CheckWeight::::new(), + pallet_transaction_payment::ChargeTransactionPayment::::from(tip), + ); + let raw_payload = westend_runtime::SignedPayload::new(call, extra).unwrap(); + let signature = raw_payload.using_encoded(|payload| sender.sign(payload)); + let (call, extra, _) = raw_payload.deconstruct(); + westend_runtime::UncheckedExtrinsic::new_signed( + call, + account_id.into(), + MultiSignature::Sr25519(signature), + extra, + ) +} + +// Constructs the SignedExtra component of an extrinsic for the Westend runtime. +fn construct_extrinsic_penpal( + sender: sp_keyring::AccountKeyring, + call: penpal_runtime::RuntimeCall, +) -> penpal_runtime::UncheckedExtrinsic { + type Runtime = ::Runtime; + let account_id = ::AccountId::from(sender.public()); + let tip = 0; + let extra: penpal_runtime::SignedExtra = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckEra::::from(generic::Era::immortal()), + frame_system::CheckNonce::::from( + frame_system::Pallet::::account(&account_id).nonce, + ), + frame_system::CheckWeight::::new(), + pallet_asset_tx_payment::ChargeAssetTxPayment::::from(tip, None), + ); + type SignedPayload = + generic::SignedPayload; + let raw_payload = SignedPayload::new(call, extra).unwrap(); + let signature = raw_payload.using_encoded(|payload| sender.sign(payload)); + let (call, extra, _) = raw_payload.deconstruct(); + penpal_runtime::UncheckedExtrinsic::new_signed( + call, + account_id.into(), + MultiSignature::Sr25519(signature), + extra, + ) +} diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml index 64abedbaac78..888193c5c6ea 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml @@ -138,6 +138,7 @@ runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", "xcm-builder/runtime-benchmarks", "xcm-executor/runtime-benchmarks", + "xcm-fee-payment-runtime-api/runtime-benchmarks", ] try-runtime = [ "cumulus-pallet-aura-ext/try-runtime", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs index 2a8c5549f610..d89a450baa86 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs @@ -100,7 +100,10 @@ use xcm::{ latest::prelude::{AssetId, BodyId}, IntoVersion, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm, }; -use xcm_fee_payment_runtime_api::Error as XcmPaymentApiError; +use xcm_fee_payment_runtime_api::{ + dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects}, + fees::Error as XcmPaymentApiError, +}; use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}; @@ -1281,7 +1284,7 @@ impl_runtime_apis! { } } - impl xcm_fee_payment_runtime_api::XcmPaymentApi for Runtime { + impl xcm_fee_payment_runtime_api::fees::XcmPaymentApi for Runtime { fn query_acceptable_payment_assets(xcm_version: xcm::Version) -> Result, XcmPaymentApiError> { let acceptable = vec![ // native token @@ -1320,6 +1323,70 @@ impl_runtime_apis! { } } + impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi for Runtime { + fn dry_run_extrinsic(extrinsic: ::Extrinsic) -> Result, XcmDryRunApiError> { + use xcm_builder::InspectMessageQueues; + use xcm_executor::RecordXcm; + use xcm::prelude::*; + + pallet_xcm::Pallet::::set_record_xcm(true); + let result = Executive::apply_extrinsic(extrinsic).map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_extrinsic", + "Applying extrinsic failed with error {:?}", + error, + ); + XcmDryRunApiError::InvalidExtrinsic + })?; + let local_xcm = pallet_xcm::Pallet::::recorded_xcm(); + let forwarded_xcms = xcm_config::XcmRouter::get_messages(); + let events: Vec = System::read_events_no_consensus().map(|record| record.event.clone()).collect(); + Ok(ExtrinsicDryRunEffects { + local_xcm: local_xcm.map(VersionedXcm::<()>::V4), + forwarded_xcms, + emitted_events: events, + execution_result: result, + }) + } + + fn dry_run_xcm(origin_location: VersionedLocation, program: VersionedXcm) -> Result, XcmDryRunApiError> { + use xcm_builder::InspectMessageQueues; + use xcm::prelude::*; + + let origin_location: Location = origin_location.try_into().map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_xcm", + "Location version conversion failed with error: {:?}", + error, + ); + XcmDryRunApiError::VersionedConversionFailed + })?; + let program: Xcm = program.try_into().map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_xcm", + "Xcm version conversion failed with error {:?}", + error, + ); + XcmDryRunApiError::VersionedConversionFailed + })?; + let mut hash = program.using_encoded(sp_core::hashing::blake2_256); + let result = xcm_executor::XcmExecutor::::prepare_and_execute( + origin_location, + program, + &mut hash, + Weight::MAX, // Max limit available for execution. + Weight::zero(), + ); + let forwarded_xcms = xcm_config::XcmRouter::get_messages(); + let events: Vec = System::read_events_no_consensus().map(|record| record.event.clone()).collect(); + Ok(XcmDryRunEffects { + forwarded_xcms, + emitted_events: events, + execution_result: result, + }) + } + } + impl cumulus_primitives_core::CollectCollationInfo for Runtime { fn collect_collation_info(header: &::Header) -> cumulus_primitives_core::CollationInfo { ParachainSystem::collect_collation_info(header) diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs index a73c1cc33ea0..664d2b9c9dd5 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs @@ -424,6 +424,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; } /// Converts a local signed origin into an XCM location. diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml index a767ac5af6c8..1e62965b62f1 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml @@ -135,6 +135,7 @@ runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", "xcm-builder/runtime-benchmarks", "xcm-executor/runtime-benchmarks", + "xcm-fee-payment-runtime-api/runtime-benchmarks", ] try-runtime = [ "cumulus-pallet-aura-ext/try-runtime", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs index f9ab2145ffca..79f1efc7fa9d 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -85,17 +85,24 @@ pub use sp_runtime::BuildStorage; use assets_common::{foreign_creators::ForeignCreators, matching::FromSiblingParachain}; use polkadot_runtime_common::{BlockHashCount, SlowAdjustingFeeUpdate}; +use xcm::{ + prelude::{VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm}, + IntoVersion, +}; + +// We exclude `Assets` since it's the name of a pallet +use xcm::latest::prelude::AssetId; #[cfg(feature = "runtime-benchmarks")] use xcm::latest::prelude::{ Asset, Fungible, Here, InteriorLocation, Junction, Junction::*, Location, NetworkId, NonFungible, Parent, ParentThen, Response, XCM_VERSION, }; -use xcm::{ - latest::prelude::AssetId, IntoVersion, VersionedAssetId, VersionedAssets, VersionedLocation, - VersionedXcm, + +use xcm_fee_payment_runtime_api::{ + dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects}, + fees::Error as XcmPaymentApiError, }; -use xcm_fee_payment_runtime_api::Error as XcmPaymentApiError; use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}; @@ -1309,6 +1316,109 @@ impl_runtime_apis! { } } + impl xcm_fee_payment_runtime_api::fees::XcmPaymentApi for Runtime { + fn query_acceptable_payment_assets(xcm_version: xcm::Version) -> Result, XcmPaymentApiError> { + let acceptable = vec![ + // native token + VersionedAssetId::from(AssetId(xcm_config::WestendLocation::get())) + ]; + + Ok(acceptable + .into_iter() + .filter_map(|asset| asset.into_version(xcm_version).ok()) + .collect()) + } + + fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result { + match asset.try_as::() { + Ok(asset_id) if asset_id.0 == xcm_config::WestendLocation::get() => { + // for native token + Ok(WeightToFee::weight_to_fee(&weight)) + }, + Ok(asset_id) => { + log::trace!(target: "xcm::xcm_fee_payment_runtime_api", "query_weight_to_asset_fee - unhandled asset_id: {asset_id:?}!"); + Err(XcmPaymentApiError::AssetNotFound) + }, + Err(_) => { + log::trace!(target: "xcm::xcm_fee_payment_runtime_api", "query_weight_to_asset_fee - failed to convert asset: {asset:?}!"); + Err(XcmPaymentApiError::VersionedConversionFailed) + } + } + } + + 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) + } + } + + impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi for Runtime { + fn dry_run_extrinsic(extrinsic: ::Extrinsic) -> Result, XcmDryRunApiError> { + use xcm_builder::InspectMessageQueues; + use xcm_executor::RecordXcm; + use xcm::prelude::*; + + pallet_xcm::Pallet::::set_record_xcm(true); + let result = Executive::apply_extrinsic(extrinsic).map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_extrinsic", + "Applying extrinsic failed with error {:?}", + error, + ); + XcmDryRunApiError::InvalidExtrinsic + })?; + let local_xcm = pallet_xcm::Pallet::::recorded_xcm(); + let forwarded_xcms = xcm_config::XcmRouter::get_messages(); + let events: Vec = System::read_events_no_consensus().map(|record| record.event.clone()).collect(); + Ok(ExtrinsicDryRunEffects { + local_xcm: local_xcm.map(VersionedXcm::<()>::V4), + forwarded_xcms, + emitted_events: events, + execution_result: result, + }) + } + + fn dry_run_xcm(origin_location: VersionedLocation, program: VersionedXcm) -> Result, XcmDryRunApiError> { + use xcm_builder::InspectMessageQueues; + use xcm::prelude::*; + + let origin_location: Location = origin_location.try_into().map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_xcm", + "Location version conversion failed with error: {:?}", + error, + ); + XcmDryRunApiError::VersionedConversionFailed + })?; + let program: Xcm = program.try_into().map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_xcm", + "Xcm version conversion failed with error {:?}", + error, + ); + XcmDryRunApiError::VersionedConversionFailed + })?; + let mut hash = program.using_encoded(sp_core::hashing::blake2_256); + let result = xcm_executor::XcmExecutor::::prepare_and_execute( + origin_location, + program, + &mut hash, + Weight::MAX, // Max limit available for execution. + Weight::zero(), + ); + let forwarded_xcms = xcm_config::XcmRouter::get_messages(); + let events: Vec = System::read_events_no_consensus().map(|record| record.event.clone()).collect(); + Ok(XcmDryRunEffects { + forwarded_xcms, + emitted_events: events, + execution_result: result, + }) + } + } + impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentCallApi for Runtime { @@ -1372,45 +1482,6 @@ impl_runtime_apis! { } } - impl xcm_fee_payment_runtime_api::XcmPaymentApi for Runtime { - fn query_acceptable_payment_assets(xcm_version: xcm::Version) -> Result, XcmPaymentApiError> { - let acceptable = vec![ - // native token - VersionedAssetId::from(AssetId(xcm_config::WestendLocation::get())) - ]; - - Ok(acceptable - .into_iter() - .filter_map(|asset| asset.into_version(xcm_version).ok()) - .collect()) - } - - fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result { - match asset.try_as::() { - Ok(asset_id) if asset_id.0 == xcm_config::WestendLocation::get() => { - // for native token - Ok(WeightToFee::weight_to_fee(&weight)) - }, - Ok(asset_id) => { - log::trace!(target: "xcm::xcm_fee_payment_runtime_api", "query_weight_to_asset_fee - unhandled asset_id: {asset_id:?}!"); - Err(XcmPaymentApiError::AssetNotFound) - }, - Err(_) => { - log::trace!(target: "xcm::xcm_fee_payment_runtime_api", "query_weight_to_asset_fee - failed to convert asset: {asset:?}!"); - Err(XcmPaymentApiError::VersionedConversionFailed) - } - } - } - - 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) - } - } - impl cumulus_primitives_core::CollectCollationInfo for Runtime { fn collect_collation_info(header: &::Header) -> cumulus_primitives_core::CollationInfo { ParachainSystem::collect_collation_info(header) diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs index d610bfd768cd..35a42627ad71 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs @@ -440,6 +440,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; } /// Local origins on this chain are allowed to dispatch XCM sends/executions. diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs index bd1445bee22c..a0d2e91dffd2 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs @@ -239,6 +239,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; } pub type PriceForParentDelivery = diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/xcm_config.rs index f147cd9653fe..c2ca8e47f2a6 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/xcm_config.rs @@ -206,6 +206,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; } pub type PriceForParentDelivery = diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/xcm_config.rs index 84697c3e3634..c68f230a16dc 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/xcm_config.rs @@ -220,6 +220,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; } /// Converts a local signed origin into an XCM location. diff --git a/cumulus/parachains/runtimes/contracts/contracts-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/contracts/contracts-rococo/src/xcm_config.rs index ac15ac5b0f0f..8c3371019860 100644 --- a/cumulus/parachains/runtimes/contracts/contracts-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/contracts/contracts-rococo/src/xcm_config.rs @@ -202,6 +202,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; } /// Converts a local signed origin into an XCM location. diff --git a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/xcm_config.rs index 9095b5b1caaa..c16b40b8675f 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/xcm_config.rs @@ -224,6 +224,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; } /// Converts a local signed origin into an XCM location. Forms the basis for local origins diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/xcm_config.rs index defc57e2d7f5..b12765870bfd 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/xcm_config.rs @@ -232,6 +232,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; } /// Converts a local signed origin into an XCM location. Forms the basis for local origins diff --git a/cumulus/parachains/runtimes/glutton/glutton-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/glutton/glutton-westend/src/xcm_config.rs index 9d438a41f8fe..d1fb50c1ab09 100644 --- a/cumulus/parachains/runtimes/glutton/glutton-westend/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/glutton/glutton-westend/src/xcm_config.rs @@ -91,6 +91,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = (); } impl cumulus_pallet_xcm::Config for Runtime { diff --git a/cumulus/parachains/runtimes/people/people-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/people/people-rococo/src/xcm_config.rs index 101d9a180e5f..cca964fb2441 100644 --- a/cumulus/parachains/runtimes/people/people-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/people/people-rococo/src/xcm_config.rs @@ -230,6 +230,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; } /// Converts a local signed origin into an XCM location. Forms the basis for local origins diff --git a/cumulus/parachains/runtimes/people/people-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/people/people-westend/src/xcm_config.rs index 0a903f915056..3926ddcf21ef 100644 --- a/cumulus/parachains/runtimes/people/people-westend/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/people/people-westend/src/xcm_config.rs @@ -238,6 +238,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; } /// Converts a local signed origin into an XCM location. Forms the basis for local origins diff --git a/cumulus/parachains/runtimes/starters/shell/src/xcm_config.rs b/cumulus/parachains/runtimes/starters/shell/src/xcm_config.rs index 7f9de0f64b35..741b3bcd752f 100644 --- a/cumulus/parachains/runtimes/starters/shell/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/starters/shell/src/xcm_config.rs @@ -91,6 +91,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = (); } impl cumulus_pallet_xcm::Config for Runtime { diff --git a/cumulus/parachains/runtimes/testing/penpal/Cargo.toml b/cumulus/parachains/runtimes/testing/penpal/Cargo.toml index 028aa002a91e..4ebb95f26cf6 100644 --- a/cumulus/parachains/runtimes/testing/penpal/Cargo.toml +++ b/cumulus/parachains/runtimes/testing/penpal/Cargo.toml @@ -64,6 +64,7 @@ polkadot-runtime-common = { path = "../../../../../polkadot/runtime/common", def xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false } xcm-builder = { package = "staging-xcm-builder", path = "../../../../../polkadot/xcm/xcm-builder", default-features = false } xcm-executor = { package = "staging-xcm-executor", path = "../../../../../polkadot/xcm/xcm-executor", default-features = false } +xcm-fee-payment-runtime-api = { path = "../../../../../polkadot/xcm/xcm-fee-payment-runtime-api", default-features = false } # Cumulus cumulus-pallet-aura-ext = { path = "../../../../pallets/aura-ext", default-features = false } @@ -134,6 +135,7 @@ std = [ "substrate-wasm-builder", "xcm-builder/std", "xcm-executor/std", + "xcm-fee-payment-runtime-api/std", "xcm/std", ] @@ -164,6 +166,7 @@ runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", "xcm-builder/runtime-benchmarks", "xcm-executor/runtime-benchmarks", + "xcm-fee-payment-runtime-api/runtime-benchmarks", ] try-runtime = [ diff --git a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs index 89885d77378b..582154fec6d2 100644 --- a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs @@ -32,6 +32,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); mod weights; pub mod xcm_config; +use codec::Encode; use cumulus_pallet_parachain_system::RelayNumberStrictlyIncreases; use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; use frame_support::{ @@ -44,7 +45,7 @@ use frame_support::{ AsEnsureOriginWithArg, ConstBool, ConstU32, ConstU64, ConstU8, Everything, TransformOrigin, }, weights::{ - constants::WEIGHT_REF_TIME_PER_SECOND, ConstantMultiplier, FeePolynomial, + constants::WEIGHT_REF_TIME_PER_SECOND, ConstantMultiplier, FeePolynomial, WeightToFee as _, WeightToFeeCoefficient, WeightToFeeCoefficients, WeightToFeePolynomial, }, PalletId, @@ -80,7 +81,14 @@ pub use sp_runtime::BuildStorage; use parachains_common::{AccountId, Signature}; use polkadot_runtime_common::{BlockHashCount, SlowAdjustingFeeUpdate}; use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}; -use xcm::latest::prelude::{AssetId as AssetLocationId, BodyId}; +use xcm::{ + latest::prelude::{AssetId as AssetLocationId, BodyId}, + IntoVersion, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm, +}; +use xcm_fee_payment_runtime_api::{ + dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects}, + fees::Error as XcmPaymentApiError, +}; /// Balance of an account. pub type Balance = u128; @@ -835,6 +843,101 @@ impl_runtime_apis! { } } + impl xcm_fee_payment_runtime_api::fees::XcmPaymentApi for Runtime { + fn query_acceptable_payment_assets(xcm_version: xcm::Version) -> Result, XcmPaymentApiError> { + if !matches!(xcm_version, 3 | 4) { + return Err(XcmPaymentApiError::UnhandledXcmVersion); + } + Ok([VersionedAssetId::V4(xcm_config::RelayLocation::get().into())] + .into_iter() + .filter_map(|asset| asset.into_version(xcm_version).ok()) + .collect()) + } + + fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result { + let local_asset = VersionedAssetId::V4(xcm_config::RelayLocation::get().into()); + let asset = asset + .into_version(4) + .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; + + if asset != local_asset { return Err(XcmPaymentApiError::AssetNotFound); } + + Ok(WeightToFee::weight_to_fee(&weight)) + } + + 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) + } + } + + impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi for Runtime { + fn dry_run_extrinsic(extrinsic: ::Extrinsic) -> Result, XcmDryRunApiError> { + use xcm_builder::InspectMessageQueues; + use xcm_executor::RecordXcm; + use xcm::prelude::*; + + pallet_xcm::Pallet::::set_record_xcm(true); + let result = Executive::apply_extrinsic(extrinsic).map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_extrinsic", + "Applying extrinsic failed with error {:?}", + error, + ); + XcmDryRunApiError::InvalidExtrinsic + })?; + let local_xcm = pallet_xcm::Pallet::::recorded_xcm(); + let forwarded_xcms = xcm_config::XcmRouter::get_messages(); + let events: Vec = System::read_events_no_consensus().map(|record| record.event.clone()).collect(); + Ok(ExtrinsicDryRunEffects { + local_xcm: local_xcm.map(VersionedXcm::<()>::V4), + forwarded_xcms, + emitted_events: events, + execution_result: result, + }) + } + + fn dry_run_xcm(origin_location: VersionedLocation, program: VersionedXcm) -> Result, XcmDryRunApiError> { + use xcm_builder::InspectMessageQueues; + use xcm::prelude::*; + + let origin_location: Location = origin_location.try_into().map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_xcm", + "Location version conversion failed with error: {:?}", + error, + ); + XcmDryRunApiError::VersionedConversionFailed + })?; + let program: Xcm = program.try_into().map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_xcm", + "Xcm version conversion failed with error {:?}", + error, + ); + XcmDryRunApiError::VersionedConversionFailed + })?; + let mut hash = program.using_encoded(sp_core::hashing::blake2_256); + let result = xcm_executor::XcmExecutor::::prepare_and_execute( + origin_location, + program, + &mut hash, + Weight::MAX, // Max limit. + Weight::zero(), + ); + let forwarded_xcms = xcm_config::XcmRouter::get_messages(); + let events: Vec = System::read_events_no_consensus().map(|record| record.event.clone()).collect(); + Ok(XcmDryRunEffects { + forwarded_xcms, + emitted_events: events, + execution_result: result, + }) + } + } + #[cfg(feature = "try-runtime")] impl frame_try_runtime::TryRuntime for Runtime { fn on_runtime_upgrade(checks: frame_try_runtime::UpgradeCheckSelect) -> (Weight, Weight) { diff --git a/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs b/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs index 711041f6d6e2..08a2da260c57 100644 --- a/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs @@ -362,6 +362,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; } /// Multiplier used for dedicated `TakeFirstAssetTrader` with `ForeignAssets` instance. diff --git a/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs b/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs index e762cec9093b..f22e900ba9ef 100644 --- a/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs @@ -490,6 +490,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; } /// Local origins on this chain are allowed to dispatch XCM sends/executions. diff --git a/cumulus/primitives/utility/src/lib.rs b/cumulus/primitives/utility/src/lib.rs index 54f40bd01097..64784eb36f84 100644 --- a/cumulus/primitives/utility/src/lib.rs +++ b/cumulus/primitives/utility/src/lib.rs @@ -34,8 +34,8 @@ use sp_runtime::{ SaturatedConversion, }; use sp_std::{marker::PhantomData, prelude::*}; -use xcm::{latest::prelude::*, WrapVersion}; -use xcm_builder::TakeRevenue; +use xcm::{latest::prelude::*, VersionedLocation, VersionedXcm, WrapVersion}; +use xcm_builder::{InspectMessageQueues, TakeRevenue}; use xcm_executor::{ traits::{MatchesFungibles, TransactAsset, WeightTrader}, AssetsInHolding, @@ -93,6 +93,14 @@ where } } +impl InspectMessageQueues + for ParentAsUmp +{ + fn get_messages() -> Vec<(VersionedLocation, Vec>)> { + T::get_messages() + } +} + /// Contains information to handle refund/payment for xcm-execution #[derive(Clone, Eq, PartialEq, Debug)] struct AssetTraderRefunder { diff --git a/polkadot/node/service/Cargo.toml b/polkadot/node/service/Cargo.toml index 9688ab556473..7c010778d50d 100644 --- a/polkadot/node/service/Cargo.toml +++ b/polkadot/node/service/Cargo.toml @@ -206,6 +206,7 @@ runtime-benchmarks = [ "service/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "westend-runtime?/runtime-benchmarks", + "xcm-fee-payment-runtime-api/runtime-benchmarks", ] try-runtime = [ "frame-support/try-runtime", diff --git a/polkadot/node/service/src/fake_runtime_api.rs b/polkadot/node/service/src/fake_runtime_api.rs index c6cfb7a27d04..45f770cdf168 100644 --- a/polkadot/node/service/src/fake_runtime_api.rs +++ b/polkadot/node/service/src/fake_runtime_api.rs @@ -398,20 +398,30 @@ sp_api::impl_runtime_apis! { } } - impl xcm_fee_payment_runtime_api::XcmPaymentApi for Runtime { - fn query_acceptable_payment_assets(_: xcm::Version) -> Result, xcm_fee_payment_runtime_api::Error> { + impl xcm_fee_payment_runtime_api::fees::XcmPaymentApi for Runtime { + fn query_acceptable_payment_assets(_: xcm::Version) -> Result, xcm_fee_payment_runtime_api::fees::Error> { unimplemented!() } - fn query_weight_to_asset_fee(_: Weight, _: VersionedAssetId) -> Result { + fn query_weight_to_asset_fee(_: Weight, _: VersionedAssetId) -> Result { unimplemented!() } - fn query_xcm_weight(_: VersionedXcm<()>) -> Result { + fn query_xcm_weight(_: VersionedXcm<()>) -> Result { unimplemented!() } - fn query_delivery_fees(_: VersionedLocation, _: VersionedXcm<()>) -> Result { + fn query_delivery_fees(_: VersionedLocation, _: VersionedXcm<()>) -> Result { + unimplemented!() + } + } + + impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi for Runtime { + fn dry_run_extrinsic(_: ::Extrinsic) -> Result, xcm_fee_payment_runtime_api::dry_run::Error> { + unimplemented!() + } + + fn dry_run_xcm(_: VersionedLocation, _: VersionedXcm<()>) -> Result, xcm_fee_payment_runtime_api::dry_run::Error> { unimplemented!() } } diff --git a/polkadot/runtime/common/src/xcm_sender.rs b/polkadot/runtime/common/src/xcm_sender.rs index a712d4381f75..cbec1a8ca103 100644 --- a/polkadot/runtime/common/src/xcm_sender.rs +++ b/polkadot/runtime/common/src/xcm_sender.rs @@ -18,7 +18,7 @@ use frame_support::traits::Get; use frame_system::pallet_prelude::BlockNumberFor; -use parity_scale_codec::Encode; +use parity_scale_codec::{Decode, Encode}; use primitives::Id as ParaId; use runtime_parachains::{ configuration::{self, HostConfiguration}, @@ -27,6 +27,7 @@ use runtime_parachains::{ use sp_runtime::FixedPointNumber; use sp_std::{marker::PhantomData, prelude::*}; use xcm::prelude::*; +use xcm_builder::InspectMessageQueues; use SendError::*; /// Simple value-bearing trait for determining/expressing the assets required to be paid for a @@ -138,6 +139,24 @@ where } } +impl InspectMessageQueues for ChildParachainRouter { + fn get_messages() -> Vec<(VersionedLocation, Vec>)> { + dmp::DownwardMessageQueues::::iter() + .map(|(para_id, messages)| { + let decoded_messages: Vec> = messages + .iter() + .map(|downward_message| { + let message = VersionedXcm::<()>::decode(&mut &downward_message.msg[..]).unwrap(); + log::trace!(target: "xcm::DownwardMessageQueues::get_messages", "Message: {:?}, sent at: {:?}", message, downward_message.sent_at); + message + }) + .collect(); + (VersionedLocation::V4(Parachain(para_id.into()).into()), decoded_messages) + }) + .collect() + } +} + /// Implementation of `xcm_builder::EnsureDelivery` which helps to ensure delivery to the /// `ParaId` parachain (sibling or child). Deposits existential deposit for origin (if needed). /// Deposits estimated fee to the origin account (if needed). diff --git a/polkadot/runtime/parachains/src/dmp.rs b/polkadot/runtime/parachains/src/dmp.rs index 354b16cc3f08..df2f93e19421 100644 --- a/polkadot/runtime/parachains/src/dmp.rs +++ b/polkadot/runtime/parachains/src/dmp.rs @@ -119,7 +119,7 @@ pub mod pallet { /// The downward messages addressed for a certain para. #[pallet::storage] - pub(crate) type DownwardMessageQueues = StorageMap< + pub type DownwardMessageQueues = StorageMap< _, Twox64Concat, ParaId, diff --git a/polkadot/runtime/rococo/Cargo.toml b/polkadot/runtime/rococo/Cargo.toml index bbe19310f970..f4d8fb51b3fa 100644 --- a/polkadot/runtime/rococo/Cargo.toml +++ b/polkadot/runtime/rococo/Cargo.toml @@ -268,6 +268,7 @@ runtime-benchmarks = [ "sp-staking/runtime-benchmarks", "xcm-builder/runtime-benchmarks", "xcm-executor/runtime-benchmarks", + "xcm-fee-payment-runtime-api/runtime-benchmarks", ] try-runtime = [ "frame-executive/try-runtime", diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index cd133de97b8b..93187e5c884c 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -133,7 +133,10 @@ use governance::{ pallet_custom_origins, AuctionAdmin, Fellows, GeneralAdmin, LeaseAdmin, Treasurer, TreasurySpender, }; -use xcm_fee_payment_runtime_api::Error as XcmPaymentApiError; +use xcm_fee_payment_runtime_api::{ + dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects}, + fees::Error as XcmPaymentApiError, +}; #[cfg(test)] mod tests; @@ -1759,7 +1762,7 @@ sp_api::impl_runtime_apis! { } } - impl xcm_fee_payment_runtime_api::XcmPaymentApi for Runtime { + impl xcm_fee_payment_runtime_api::fees::XcmPaymentApi for Runtime { fn query_acceptable_payment_assets(xcm_version: xcm::Version) -> Result, XcmPaymentApiError> { let acceptable = vec![ // native token @@ -1798,6 +1801,66 @@ sp_api::impl_runtime_apis! { } } + impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi for Runtime { + fn dry_run_extrinsic(extrinsic: ::Extrinsic) -> Result, XcmDryRunApiError> { + use xcm_builder::InspectMessageQueues; + use xcm_executor::RecordXcm; + pallet_xcm::Pallet::::set_record_xcm(true); + let result = Executive::apply_extrinsic(extrinsic).map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_extrinsic", + "Applying extrinsic failed with error {:?}", + error, + ); + XcmDryRunApiError::InvalidExtrinsic + })?; + let local_xcm = pallet_xcm::Pallet::::recorded_xcm(); + let forwarded_xcms = xcm_config::XcmRouter::get_messages(); + let events: Vec = System::read_events_no_consensus().map(|record| record.event.clone()).collect(); + Ok(ExtrinsicDryRunEffects { + local_xcm: local_xcm.map(VersionedXcm::<()>::V4), + forwarded_xcms, + emitted_events: events, + execution_result: result, + }) + } + + fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm) -> Result, XcmDryRunApiError> { + use xcm_builder::InspectMessageQueues; + let origin_location: Location = origin_location.try_into().map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_xcm", + "Location version conversion failed with error: {:?}", + error, + ); + XcmDryRunApiError::VersionedConversionFailed + })?; + let xcm: Xcm = xcm.try_into().map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_xcm", + "Xcm version conversion failed with error {:?}", + error, + ); + XcmDryRunApiError::VersionedConversionFailed + })?; + let mut hash = xcm.using_encoded(sp_io::hashing::blake2_256); + let result = xcm_executor::XcmExecutor::::prepare_and_execute( + origin_location, + xcm, + &mut hash, + Weight::MAX, // Max limit available for execution. + Weight::zero(), + ); + let forwarded_xcms = xcm_config::XcmRouter::get_messages(); + let events: Vec = System::read_events_no_consensus().map(|record| record.event.clone()).collect(); + Ok(XcmDryRunEffects { + forwarded_xcms, + emitted_events: events, + execution_result: result, + }) + } + } + impl sp_api::Metadata for Runtime { fn metadata() -> OpaqueMetadata { OpaqueMetadata::new(Runtime::metadata().into()) diff --git a/polkadot/runtime/rococo/src/xcm_config.rs b/polkadot/runtime/rococo/src/xcm_config.rs index c7063bd7ad61..decbc795143f 100644 --- a/polkadot/runtime/rococo/src/xcm_config.rs +++ b/polkadot/runtime/rococo/src/xcm_config.rs @@ -224,6 +224,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = XcmPallet; } parameter_types! { diff --git a/polkadot/runtime/test-runtime/src/xcm_config.rs b/polkadot/runtime/test-runtime/src/xcm_config.rs index 8411b79f2529..fc3d0dc42a3b 100644 --- a/polkadot/runtime/test-runtime/src/xcm_config.rs +++ b/polkadot/runtime/test-runtime/src/xcm_config.rs @@ -156,6 +156,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = (); } impl pallet_xcm::Config for crate::Runtime { diff --git a/polkadot/runtime/westend/Cargo.toml b/polkadot/runtime/westend/Cargo.toml index d726adfb8e6e..f02cae0e9d49 100644 --- a/polkadot/runtime/westend/Cargo.toml +++ b/polkadot/runtime/westend/Cargo.toml @@ -283,6 +283,7 @@ runtime-benchmarks = [ "sp-staking/runtime-benchmarks", "xcm-builder/runtime-benchmarks", "xcm-executor/runtime-benchmarks", + "xcm-fee-payment-runtime-api/runtime-benchmarks", ] try-runtime = [ "frame-election-provider-support/try-runtime", diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 7f44a4c9f8f9..17fa7f55a9b5 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -107,7 +107,10 @@ use xcm::{ }; use xcm_builder::PayOverXcm; -use xcm_fee_payment_runtime_api::Error as XcmPaymentApiError; +use xcm_fee_payment_runtime_api::{ + dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects}, + fees::Error as XcmPaymentApiError, +}; pub use frame_system::Call as SystemCall; pub use pallet_balances::Call as BalancesCall; @@ -2194,7 +2197,7 @@ sp_api::impl_runtime_apis! { } } - impl xcm_fee_payment_runtime_api::XcmPaymentApi for Runtime { + impl xcm_fee_payment_runtime_api::fees::XcmPaymentApi for Runtime { fn query_acceptable_payment_assets(xcm_version: xcm::Version) -> Result, XcmPaymentApiError> { let acceptable = vec![ // native token @@ -2233,6 +2236,66 @@ sp_api::impl_runtime_apis! { } } + impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi for Runtime { + fn dry_run_extrinsic(extrinsic: ::Extrinsic) -> Result, XcmDryRunApiError> { + use xcm_builder::InspectMessageQueues; + use xcm_executor::RecordXcm; + pallet_xcm::Pallet::::set_record_xcm(true); + let result = Executive::apply_extrinsic(extrinsic).map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_extrinsic", + "Applying extrinsic failed with error {:?}", + error, + ); + XcmDryRunApiError::InvalidExtrinsic + })?; + let local_xcm = pallet_xcm::Pallet::::recorded_xcm(); + let forwarded_xcms = xcm_config::XcmRouter::get_messages(); + let events: Vec = System::read_events_no_consensus().map(|record| record.event.clone()).collect(); + Ok(ExtrinsicDryRunEffects { + local_xcm: local_xcm.map(VersionedXcm::<()>::V4), + forwarded_xcms, + emitted_events: events, + execution_result: result, + }) + } + + fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm) -> Result, XcmDryRunApiError> { + use xcm_builder::InspectMessageQueues; + let origin_location: Location = origin_location.try_into().map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_xcm", + "Location version conversion failed with error: {:?}", + error, + ); + XcmDryRunApiError::VersionedConversionFailed + })?; + let xcm: Xcm = xcm.try_into().map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_xcm", + "Xcm version conversion failed with error {:?}", + error, + ); + XcmDryRunApiError::VersionedConversionFailed + })?; + let mut hash = xcm.using_encoded(sp_io::hashing::blake2_256); + let result = xcm_executor::XcmExecutor::::prepare_and_execute( + origin_location, + xcm, + &mut hash, + Weight::MAX, // Max limit available for execution. + Weight::zero(), + ); + let forwarded_xcms = xcm_config::XcmRouter::get_messages(); + let events: Vec = System::read_events_no_consensus().map(|record| record.event.clone()).collect(); + Ok(XcmDryRunEffects { + forwarded_xcms, + emitted_events: events, + execution_result: result, + }) + } + } + impl pallet_nomination_pools_runtime_api::NominationPoolsApi< Block, AccountId, diff --git a/polkadot/runtime/westend/src/xcm_config.rs b/polkadot/runtime/westend/src/xcm_config.rs index f661c4b0e4f4..c6c5fb9e72a4 100644 --- a/polkadot/runtime/westend/src/xcm_config.rs +++ b/polkadot/runtime/westend/src/xcm_config.rs @@ -222,6 +222,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = XcmPallet; } parameter_types! { diff --git a/polkadot/xcm/pallet-xcm-benchmarks/src/fungible/mock.rs b/polkadot/xcm/pallet-xcm-benchmarks/src/fungible/mock.rs index 7233b46d0cd6..c0dfa91afc78 100644 --- a/polkadot/xcm/pallet-xcm-benchmarks/src/fungible/mock.rs +++ b/polkadot/xcm/pallet-xcm-benchmarks/src/fungible/mock.rs @@ -120,6 +120,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = (); } impl crate::Config for Test { diff --git a/polkadot/xcm/pallet-xcm-benchmarks/src/generic/mock.rs b/polkadot/xcm/pallet-xcm-benchmarks/src/generic/mock.rs index a9f4d37d7a55..f51d34092616 100644 --- a/polkadot/xcm/pallet-xcm-benchmarks/src/generic/mock.rs +++ b/polkadot/xcm/pallet-xcm-benchmarks/src/generic/mock.rs @@ -110,6 +110,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = (); } parameter_types! { diff --git a/polkadot/xcm/pallet-xcm/Cargo.toml b/polkadot/xcm/pallet-xcm/Cargo.toml index 460597e6649a..fc4d23426fbc 100644 --- a/polkadot/xcm/pallet-xcm/Cargo.toml +++ b/polkadot/xcm/pallet-xcm/Cargo.toml @@ -69,6 +69,7 @@ runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", "xcm-builder/runtime-benchmarks", "xcm-executor/runtime-benchmarks", + "xcm-fee-payment-runtime-api/runtime-benchmarks", ] try-runtime = [ "frame-support/try-runtime", diff --git a/polkadot/xcm/pallet-xcm/src/lib.rs b/polkadot/xcm/pallet-xcm/src/lib.rs index af3b66121ea1..37fc121ba217 100644 --- a/polkadot/xcm/pallet-xcm/src/lib.rs +++ b/polkadot/xcm/pallet-xcm/src/lib.rs @@ -61,7 +61,7 @@ use xcm_executor::{ }, AssetsInHolding, }; -use xcm_fee_payment_runtime_api::Error as FeePaymentError; +use xcm_fee_payment_runtime_api::fees::Error as XcmPaymentApiError; #[cfg(any(feature = "try-runtime", test))] use sp_runtime::TryRuntimeError; @@ -764,6 +764,25 @@ pub mod pallet { #[pallet::storage] pub(super) type XcmExecutionSuspended = StorageValue<_, bool, ValueQuery>; + /// Whether or not incoming XCMs (both executed locally and received) should be recorded. + /// Only one XCM program will be recorded at a time. + /// This is meant to be used in runtime APIs, and it's advised it stays false + /// for all other use cases, so as to not degrade regular performance. + /// + /// Only relevant if this pallet is being used as the [`xcm_executor::traits::RecordXcm`] + /// implementation in the XCM executor configuration. + #[pallet::storage] + pub(crate) type ShouldRecordXcm = StorageValue<_, bool, ValueQuery>; + + /// If [`ShouldRecordXcm`] is set to true, then the last XCM program executed locally + /// will be stored here. + /// Runtime APIs can fetch the XCM that was executed by accessing this value. + /// + /// Only relevant if this pallet is being used as the [`xcm_executor::traits::RecordXcm`] + /// implementation in the XCM executor configuration. + #[pallet::storage] + pub(crate) type RecordedXcm = StorageValue<_, Xcm<()>>; + #[pallet::genesis_config] pub struct GenesisConfig { #[serde(skip)] @@ -2413,35 +2432,37 @@ impl Pallet { AccountIdConversion::::into_account_truncating(&ID) } - pub fn query_xcm_weight(message: VersionedXcm<()>) -> Result { - let message = - Xcm::<()>::try_from(message).map_err(|_| FeePaymentError::VersionedConversionFailed)?; + pub fn query_xcm_weight(message: VersionedXcm<()>) -> Result { + let message = Xcm::<()>::try_from(message) + .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; T::Weigher::weight(&mut message.into()).map_err(|()| { log::error!(target: "xcm::pallet_xcm::query_xcm_weight", "Error when querying XCM weight"); - FeePaymentError::WeightNotComputable + XcmPaymentApiError::WeightNotComputable }) } pub fn query_delivery_fees( destination: VersionedLocation, message: VersionedXcm<()>, - ) -> Result { + ) -> Result { let result_version = destination.identify_version().max(message.identify_version()); - let destination = - destination.try_into().map_err(|_| FeePaymentError::VersionedConversionFailed)?; + let destination = destination + .try_into() + .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; - let message = message.try_into().map_err(|_| FeePaymentError::VersionedConversionFailed)?; + let message = + message.try_into().map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; let (_, fees) = validate_send::(destination, message).map_err(|error| { log::error!(target: "xcm::pallet_xcm::query_delivery_fees", "Error when querying delivery fees: {:?}", error); - FeePaymentError::Unroutable + XcmPaymentApiError::Unroutable })?; VersionedAssets::from(fees) .into_version(result_version) - .map_err(|_| FeePaymentError::VersionedConversionFailed) + .map_err(|_| XcmPaymentApiError::VersionedConversionFailed) } /// Create a new expectation of a query response with the querier being here. @@ -3105,6 +3126,24 @@ impl CheckSuspension for Pallet { } } +impl xcm_executor::traits::RecordXcm for Pallet { + fn should_record() -> bool { + ShouldRecordXcm::::get() + } + + fn set_record_xcm(enabled: bool) { + ShouldRecordXcm::::put(enabled); + } + + fn recorded_xcm() -> Option> { + RecordedXcm::::get() + } + + fn record(xcm: Xcm<()>) { + RecordedXcm::::put(xcm); + } +} + /// Ensure that the origin `o` represents an XCM (`Transact`) origin. /// /// Returns `Ok` with the location of the XCM sender or an `Err` otherwise. diff --git a/polkadot/xcm/pallet-xcm/src/mock.rs b/polkadot/xcm/pallet-xcm/src/mock.rs index 8e94803e8431..b3b7529217f5 100644 --- a/polkadot/xcm/pallet-xcm/src/mock.rs +++ b/polkadot/xcm/pallet-xcm/src/mock.rs @@ -531,6 +531,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = XcmPallet; } pub type LocalOriginToLocation = SignedToAccountId32; diff --git a/polkadot/xcm/pallet-xcm/src/tests/mod.rs b/polkadot/xcm/pallet-xcm/src/tests/mod.rs index 8faf16e0d2a9..c4bf20cb4d7a 100644 --- a/polkadot/xcm/pallet-xcm/src/tests/mod.rs +++ b/polkadot/xcm/pallet-xcm/src/tests/mod.rs @@ -21,8 +21,8 @@ pub(crate) mod assets_transfer; use crate::{ mock::*, pallet::SupportedVersion, AssetTraps, Config, CurrentMigration, Error, ExecuteControllerWeightInfo, LatestVersionedLocation, Pallet, Queries, QueryStatus, - VersionDiscoveryQueue, VersionMigrationStage, VersionNotifiers, VersionNotifyTargets, - WeightInfo, + RecordedXcm, ShouldRecordXcm, VersionDiscoveryQueue, VersionMigrationStage, VersionNotifiers, + VersionNotifyTargets, WeightInfo, }; use frame_support::{ assert_err_ignore_postinfo, assert_noop, assert_ok, @@ -1249,3 +1249,35 @@ fn multistage_migration_works() { assert!(Pallet::::do_try_state().is_ok()); }) } + +#[test] +fn record_xcm_works() { + let balances = vec![(ALICE, INITIAL_BALANCE)]; + new_test_ext_with_balances(balances).execute_with(|| { + let message = Xcm::::builder() + .withdraw_asset((Here, SEND_AMOUNT)) + .buy_execution((Here, SEND_AMOUNT), Unlimited) + .deposit_asset(AllCounted(1), Junction::AccountId32 { network: None, id: BOB.into() }) + .build(); + // Test default values. + assert_eq!(ShouldRecordXcm::::get(), false); + assert_eq!(RecordedXcm::::get(), None); + + // By default the message won't be recorded. + assert_ok!(XcmPallet::execute( + RuntimeOrigin::signed(ALICE), + Box::new(VersionedXcm::from(message.clone())), + BaseXcmWeight::get() * 3, + )); + assert_eq!(RecordedXcm::::get(), None); + + // We explicitly set the record flag to true so we record the XCM. + ShouldRecordXcm::::put(true); + assert_ok!(XcmPallet::execute( + RuntimeOrigin::signed(ALICE), + Box::new(VersionedXcm::from(message.clone())), + BaseXcmWeight::get() * 3, + )); + assert_eq!(RecordedXcm::::get(), Some(message.into())); + }); +} diff --git a/polkadot/xcm/xcm-builder/src/lib.rs b/polkadot/xcm/xcm-builder/src/lib.rs index 1ba38d0db836..cc06c298a418 100644 --- a/polkadot/xcm/xcm-builder/src/lib.rs +++ b/polkadot/xcm/xcm-builder/src/lib.rs @@ -120,7 +120,9 @@ mod process_xcm_message; pub use process_xcm_message::ProcessXcmMessage; mod routing; -pub use routing::{EnsureDecodableXcm, EnsureDelivery, WithTopicSource, WithUniqueTopic}; +pub use routing::{ + EnsureDecodableXcm, EnsureDelivery, InspectMessageQueues, WithTopicSource, WithUniqueTopic, +}; mod transactional; pub use transactional::FrameTransactionalProcessor; diff --git a/polkadot/xcm/xcm-builder/src/routing.rs b/polkadot/xcm/xcm-builder/src/routing.rs index 921b9ac5922e..5c284aaf1475 100644 --- a/polkadot/xcm/xcm-builder/src/routing.rs +++ b/polkadot/xcm/xcm-builder/src/routing.rs @@ -18,7 +18,7 @@ use frame_system::unique; use parity_scale_codec::Encode; -use sp_std::{marker::PhantomData, result::Result}; +use sp_std::{marker::PhantomData, result::Result, vec::Vec}; use xcm::prelude::*; use xcm_executor::{traits::FeeReason, FeesMode}; @@ -60,6 +60,11 @@ impl SendXcm for WithUniqueTopic { Ok(unique_id) } } +impl InspectMessageQueues for WithUniqueTopic { + fn get_messages() -> Vec<(VersionedLocation, Vec>)> { + Inner::get_messages() + } +} pub trait SourceTopic { fn source_topic(entropy: impl Encode) -> XcmHash; @@ -140,6 +145,26 @@ impl EnsureDelivery for Tuple { } } +/// Inspects messages in queues. +/// Meant to be used in runtime APIs, not in runtimes. +pub trait InspectMessageQueues { + /// Get queued messages and their destinations. + fn get_messages() -> Vec<(VersionedLocation, Vec>)>; +} + +#[impl_trait_for_tuples::impl_for_tuples(30)] +impl InspectMessageQueues for Tuple { + fn get_messages() -> Vec<(VersionedLocation, Vec>)> { + let mut messages = Vec::new(); + + for_tuples!( #( + messages.append(&mut Tuple::get_messages()); + )* ); + + messages + } +} + /// A wrapper router that attempts to *encode* and *decode* passed XCM `message` to ensure that the /// receiving side will be able to decode, at least with the same XCM version. /// diff --git a/polkadot/xcm/xcm-builder/src/tests/mock.rs b/polkadot/xcm/xcm-builder/src/tests/mock.rs index 7532b97d97b3..f45650ec5404 100644 --- a/polkadot/xcm/xcm-builder/src/tests/mock.rs +++ b/polkadot/xcm/xcm-builder/src/tests/mock.rs @@ -748,6 +748,7 @@ impl Config for TestConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = (); } pub fn fungible_multi_asset(location: Location, amount: u128) -> Asset { diff --git a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs index 019113a12b2f..34b204b434d6 100644 --- a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs +++ b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs @@ -221,6 +221,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = XcmPallet; } parameter_types! { diff --git a/polkadot/xcm/xcm-builder/src/universal_exports.rs b/polkadot/xcm/xcm-builder/src/universal_exports.rs index d0e3ef3032ea..04ceb7e51688 100644 --- a/polkadot/xcm/xcm-builder/src/universal_exports.rs +++ b/polkadot/xcm/xcm-builder/src/universal_exports.rs @@ -16,6 +16,7 @@ //! Traits and utilities to help with origin mutation and bridging. +use crate::InspectMessageQueues; use frame_support::{ensure, traits::Get}; use parity_scale_codec::{Decode, Encode}; use sp_std::{convert::TryInto, marker::PhantomData, prelude::*}; @@ -335,6 +336,14 @@ impl InspectMessageQueues + for SovereignPaidRemoteExporter +{ + fn get_messages() -> Vec<(VersionedLocation, Vec>)> { + Router::get_messages() + } +} + pub trait DispatchBlob { /// Takes an incoming blob from over some point-to-point link (usually from some sort of /// inter-consensus bridge) and then does what needs to be done with it. Usually this means diff --git a/polkadot/xcm/xcm-builder/tests/mock/mod.rs b/polkadot/xcm/xcm-builder/tests/mock/mod.rs index 46ec23beebc1..45bfba235563 100644 --- a/polkadot/xcm/xcm-builder/tests/mock/mod.rs +++ b/polkadot/xcm/xcm-builder/tests/mock/mod.rs @@ -209,6 +209,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = XcmPallet; } pub type LocalOriginToLocation = SignedToAccountId32; diff --git a/polkadot/xcm/xcm-executor/src/config.rs b/polkadot/xcm/xcm-executor/src/config.rs index b296d32ca2ad..63b113bc250f 100644 --- a/polkadot/xcm/xcm-executor/src/config.rs +++ b/polkadot/xcm/xcm-executor/src/config.rs @@ -17,8 +17,8 @@ use crate::traits::{ AssetExchange, AssetLock, CallDispatcher, ClaimAssets, ConvertOrigin, DropAssets, ExportXcm, FeeManager, HandleHrmpChannelAccepted, HandleHrmpChannelClosing, - HandleHrmpNewChannelOpenRequest, OnResponse, ProcessTransaction, ShouldExecute, TransactAsset, - VersionChangeNotifier, WeightBounds, WeightTrader, + HandleHrmpNewChannelOpenRequest, OnResponse, ProcessTransaction, RecordXcm, ShouldExecute, + TransactAsset, VersionChangeNotifier, WeightBounds, WeightTrader, }; use frame_support::{ dispatch::{GetDispatchInfo, Parameter, PostDispatchInfo}, @@ -122,4 +122,6 @@ pub trait Config { type HrmpChannelAcceptedHandler: HandleHrmpChannelAccepted; /// Allows optional logic execution for the `HrmpChannelClosing` XCM notification. type HrmpChannelClosingHandler: HandleHrmpChannelClosing; + /// Allows recording the last executed XCM (used by dry-run runtime APIs). + type XcmRecorder: RecordXcm; } diff --git a/polkadot/xcm/xcm-executor/src/lib.rs b/polkadot/xcm/xcm-executor/src/lib.rs index e673a46c4ac6..cbbd85314e79 100644 --- a/polkadot/xcm/xcm-executor/src/lib.rs +++ b/polkadot/xcm/xcm-executor/src/lib.rs @@ -37,6 +37,8 @@ use traits::{ XcmAssetTransfers, }; +pub use traits::RecordXcm; + mod assets; pub use assets::AssetsInHolding; mod config; @@ -204,6 +206,13 @@ impl ExecuteXcm for XcmExecutor. + +//! Trait for recording XCMs and a dummy implementation. + +use xcm::latest::Xcm; + +/// Trait for recording XCMs. +pub trait RecordXcm { + /// Whether or not we should record incoming XCMs. + fn should_record() -> bool; + /// Enable or disable recording. + fn set_record_xcm(enabled: bool); + /// Get recorded XCM. + /// Returns `None` if no message was sent, or if recording was off. + fn recorded_xcm() -> Option>; + /// Record `xcm`. + fn record(xcm: Xcm<()>); +} + +impl RecordXcm for () { + fn should_record() -> bool { + false + } + + fn set_record_xcm(_: bool) {} + + fn recorded_xcm() -> Option> { + None + } + + fn record(_: Xcm<()>) {} +} diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/Cargo.toml b/polkadot/xcm/xcm-fee-payment-runtime-api/Cargo.toml index 30c7c0bac14f..cec76e7327ec 100644 --- a/polkadot/xcm/xcm-fee-payment-runtime-api/Cargo.toml +++ b/polkadot/xcm/xcm-fee-payment-runtime-api/Cargo.toml @@ -26,15 +26,46 @@ sp-weights = { path = "../../../substrate/primitives/weights", default-features xcm = { package = "staging-xcm", path = "../", default-features = false } frame-support = { path = "../../../substrate/frame/support", default-features = false } +[dev-dependencies] +frame-system = { path = "../../../substrate/frame/system", default-features = false } +pallet-xcm = { path = "../pallet-xcm", default-features = false } +xcm-builder = { package = "staging-xcm-builder", path = "../xcm-builder", default-features = false } +sp-io = { path = "../../../substrate/primitives/io", default-features = false } +pallet-balances = { path = "../../../substrate/frame/balances", default-features = false } +pallet-assets = { path = "../../../substrate/frame/assets", default-features = false } +xcm-executor = { package = "staging-xcm-executor", path = "../xcm-executor", default-features = false } +frame-executive = { path = "../../../substrate/frame/executive", default-features = false } +log = { workspace = true } +env_logger = "0.9.0" + [features] default = ["std"] std = [ "codec/std", + "frame-executive/std", "frame-support/std", + "frame-system/std", + "log/std", + "pallet-assets/std", + "pallet-balances/std", + "pallet-xcm/std", "scale-info/std", "sp-api/std", + "sp-io/std", "sp-runtime/std", "sp-std/std", "sp-weights/std", + "xcm-builder/std", + "xcm-executor/std", "xcm/std", ] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-xcm/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", + "xcm-executor/runtime-benchmarks", +] diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/src/dry_run.rs b/polkadot/xcm/xcm-fee-payment-runtime-api/src/dry_run.rs new file mode 100644 index 000000000000..62a422d6efeb --- /dev/null +++ b/polkadot/xcm/xcm-fee-payment-runtime-api/src/dry_run.rs @@ -0,0 +1,83 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Runtime API definition for dry-running XCM-related extrinsics. +//! This API can be used to simulate XCMs and, for example, find the fees +//! that need to be paid. + +use codec::{Decode, Encode}; +use frame_support::pallet_prelude::{DispatchResult, TypeInfo}; +use sp_runtime::traits::Block as BlockT; +use sp_std::vec::Vec; +use xcm::prelude::*; + +/// Effects of dry-running an extrinsic. +#[derive(Encode, Decode, Debug, TypeInfo)] +pub struct ExtrinsicDryRunEffects { + /// The result of executing the extrinsic. + pub execution_result: DispatchResult, + /// The list of events fired by the extrinsic. + pub emitted_events: Vec, + /// The local XCM that was attempted to be executed, if any. + pub local_xcm: Option>, + /// The list of XCMs that were queued for sending. + pub forwarded_xcms: Vec<(VersionedLocation, Vec>)>, +} + +/// Effects of dry-running an XCM program. +#[derive(Encode, Decode, Debug, TypeInfo)] +pub struct XcmDryRunEffects { + /// The outcome of the XCM program execution. + pub execution_result: Outcome, + /// List of events fired by the XCM program execution. + pub emitted_events: Vec, + /// List of queued messages for sending. + pub forwarded_xcms: Vec<(VersionedLocation, Vec>)>, +} + +sp_api::decl_runtime_apis! { + /// API for dry-running extrinsics and XCM programs to get the programs that need to be passed to the fees API. + /// + /// All calls return a vector of tuples (location, xcm) where each "xcm" is executed in "location". + /// If there's local execution, the location will be "Here". + /// This vector can be used to calculate both execution and delivery fees. + /// + /// Extrinsics or XCMs might fail when executed, this doesn't mean the result of these calls will be an `Err`. + /// In those cases, there might still be a valid result, with the execution error inside it. + /// The only reasons why these calls might return an error are listed in the [`Error`] enum. + pub trait XcmDryRunApi { + /// Dry run extrinsic. + fn dry_run_extrinsic(extrinsic: ::Extrinsic) -> Result, Error>; + + /// Dry run XCM program + fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm) -> Result, Error>; + } +} + +#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, Debug, TypeInfo)] +pub enum Error { + /// An API call is unsupported. + #[codec(index = 0)] + Unimplemented, + + /// Converting a versioned data structure from one version to another failed. + #[codec(index = 1)] + VersionedConversionFailed, + + /// Extrinsic was invalid. + #[codec(index = 2)] + InvalidExtrinsic, +} diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/src/fees.rs b/polkadot/xcm/xcm-fee-payment-runtime-api/src/fees.rs new file mode 100644 index 000000000000..572d4edf5338 --- /dev/null +++ b/polkadot/xcm/xcm-fee-payment-runtime-api/src/fees.rs @@ -0,0 +1,97 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Runtime API definition for getting XCM fees. + +use codec::{Decode, Encode}; +use frame_support::pallet_prelude::TypeInfo; +use sp_std::vec::Vec; +use sp_weights::Weight; +use xcm::{Version, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm}; + +sp_api::decl_runtime_apis! { + /// A trait of XCM payment API. + /// + /// API provides functionality for obtaining: + /// + /// * the weight required to execute an XCM message, + /// * a list of acceptable `AssetId`s for message execution payment, + /// * the cost of the weight in the specified acceptable `AssetId`. + /// * the fees for an XCM message delivery. + /// + /// To determine the execution weight of the calls required for + /// [`xcm::latest::Instruction::Transact`] instruction, `TransactionPaymentCallApi` can be used. + pub trait XcmPaymentApi { + /// Returns a list of acceptable payment assets. + /// + /// # Arguments + /// + /// * `xcm_version`: Version. + fn query_acceptable_payment_assets(xcm_version: Version) -> Result, Error>; + + /// Returns a weight needed to execute a XCM. + /// + /// # Arguments + /// + /// * `message`: `VersionedXcm`. + fn query_xcm_weight(message: VersionedXcm<()>) -> Result; + + /// Converts a weight into a fee for the specified `AssetId`. + /// + /// # Arguments + /// + /// * `weight`: convertible `Weight`. + /// * `asset`: `VersionedAssetId`. + fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result; + + /// Get delivery fees for sending a specific `message` to a `destination`. + /// These always come in a specific asset, defined by the chain. + /// + /// # Arguments + /// * `message`: The message that'll be sent, necessary because most delivery fees are based on the + /// size of the message. + /// * `destination`: The destination to send the message to. Different destinations may use + /// different senders that charge different fees. + fn query_delivery_fees(destination: VersionedLocation, message: VersionedXcm<()>) -> Result; + } +} + +#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, Debug, TypeInfo)] +pub enum Error { + /// An API part is unsupported. + #[codec(index = 0)] + Unimplemented, + + /// Converting a versioned data structure from one version to another failed. + #[codec(index = 1)] + VersionedConversionFailed, + + /// XCM message weight calculation failed. + #[codec(index = 2)] + WeightNotComputable, + + /// XCM version not able to be handled. + #[codec(index = 3)] + UnhandledXcmVersion, + + /// The given asset is not handled as a fee asset. + #[codec(index = 4)] + AssetNotFound, + + /// Destination is known to be unroutable. + #[codec(index = 5)] + Unroutable, +} diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/src/lib.rs b/polkadot/xcm/xcm-fee-payment-runtime-api/src/lib.rs index 50fd4692cb0d..616ee4c2eccb 100644 --- a/polkadot/xcm/xcm-fee-payment-runtime-api/src/lib.rs +++ b/polkadot/xcm/xcm-fee-payment-runtime-api/src/lib.rs @@ -14,86 +14,19 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . -//! Runtime API definition for xcm transaction payment. +//! Runtime APIs for estimating xcm fee payment. +//! This crate offers two APIs, one for estimating fees, +//! which can be used for any type of message, and another one +//! for returning the specific messages used for transfers, a common +//! feature. +//! Users of these APIs should call the transfers API and pass the result to the +//! fees API. #![cfg_attr(not(feature = "std"), no_std)] -use codec::{Decode, Encode}; -use frame_support::pallet_prelude::TypeInfo; -use sp_std::vec::Vec; -use sp_weights::Weight; -use xcm::{Version, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm}; - -sp_api::decl_runtime_apis! { - /// A trait of XCM payment API. - /// - /// API provides functionality for obtaining: - /// - /// * the weight required to execute an XCM message, - /// * a list of acceptable `AssetId`s for message execution payment, - /// * the cost of the weight in the specified acceptable `AssetId`. - /// * the fees for an XCM message delivery. - /// - /// To determine the execution weight of the calls required for - /// [`xcm::latest::Instruction::Transact`] instruction, `TransactionPaymentCallApi` can be used. - pub trait XcmPaymentApi { - /// Returns a list of acceptable payment assets. - /// - /// # Arguments - /// - /// * `xcm_version`: desired XCM `Version` of `VersionedAssetId`. - fn query_acceptable_payment_assets(xcm_version: Version) -> Result, Error>; - - /// Returns a weight needed to execute a XCM. - /// - /// # Arguments - /// - /// * `message`: `VersionedXcm`. - fn query_xcm_weight(message: VersionedXcm<()>) -> Result; - - /// Converts a weight into a fee for the specified `AssetId`. - /// - /// # Arguments - /// - /// * `weight`: convertible `Weight`. - /// * `asset`: `VersionedAssetId`. - fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result; - - /// Get delivery fees for sending a specific `message` to a `destination`. - /// These always come in a specific asset, defined by the chain. - /// - /// # Arguments - /// * `message`: The message that'll be sent, necessary because most delivery fees are based on the - /// size of the message. - /// * `destination`: The destination to send the message to. Different destinations may use - /// different senders that charge different fees. - fn query_delivery_fees(destination: VersionedLocation, message: VersionedXcm<()>) -> Result; - } -} - -#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, Debug, TypeInfo)] -pub enum Error { - /// An API part is unsupported. - #[codec(index = 0)] - Unimplemented, - - /// Converting a versioned data structure from one version to another failed. - #[codec(index = 1)] - VersionedConversionFailed, - - /// XCM message weight calculation failed. - #[codec(index = 2)] - WeightNotComputable, - - /// XCM version not able to be handled. - #[codec(index = 3)] - UnhandledXcmVersion, - - /// The given asset is not handled as a fee asset. - #[codec(index = 4)] - AssetNotFound, - - /// Destination is known to be unroutable. - #[codec(index = 5)] - Unroutable, -} +/// Dry-run API. +/// Given an extrinsic or an XCM program, it returns the outcome of its execution. +pub mod dry_run; +/// Fee estimation API. +/// Given an XCM program, it will return the fees needed to execute it properly or send it. +pub mod fees; diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/tests/fee_estimation.rs b/polkadot/xcm/xcm-fee-payment-runtime-api/tests/fee_estimation.rs new file mode 100644 index 000000000000..7a9bfa4a7968 --- /dev/null +++ b/polkadot/xcm/xcm-fee-payment-runtime-api/tests/fee_estimation.rs @@ -0,0 +1,370 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Tests for using both the XCM fee payment API and the dry-run API. + +use frame_support::{ + dispatch::DispatchInfo, + pallet_prelude::{DispatchClass, Pays}, +}; +use sp_api::ProvideRuntimeApi; +use sp_runtime::testing::H256; +use xcm::prelude::*; +use xcm_fee_payment_runtime_api::{dry_run::XcmDryRunApi, fees::XcmPaymentApi}; + +mod mock; +use mock::{ + extra, fake_message_hash, new_test_ext_with_balances, new_test_ext_with_balances_and_assets, + DeliveryFees, ExistentialDeposit, HereLocation, RuntimeCall, RuntimeEvent, TestClient, TestXt, +}; + +// Scenario: User `1` in the local chain (id 2000) wants to transfer assets to account `[0u8; 32]` +// on "AssetHub". He wants to make sure he has enough for fees, so before he calls the +// `transfer_asset` extrinsic to do the transfer, he decides to use the `XcmDryRunApi` and +// `XcmPaymentApi` runtime APIs to estimate fees. This uses a teleport because we're dealing with +// the native token of the chain, which is registered on "AssetHub". The fees are sent as a reserve +// asset transfer, since they're paid in the relay token. +// +// Teleport Parachain(2000) Token +// Reserve Asset Transfer Relay Token for fees +// Parachain(2000) -------------------------------------------> Parachain(1000) +#[test] +fn fee_estimation_for_teleport() { + let _ = env_logger::builder().is_test(true).try_init(); + let who = 1; // AccountId = u64. + let balances = vec![(who, 100 + DeliveryFees::get() + ExistentialDeposit::get())]; + let assets = vec![(1, who, 50)]; + new_test_ext_with_balances_and_assets(balances, assets).execute_with(|| { + let client = TestClient; + let runtime_api = client.runtime_api(); + let extrinsic = TestXt::new( + RuntimeCall::XcmPallet(pallet_xcm::Call::transfer_assets { + dest: Box::new(VersionedLocation::V4((Parent, Parachain(1000)).into())), + beneficiary: Box::new(VersionedLocation::V4( + AccountId32 { id: [0u8; 32], network: None }.into(), + )), + assets: Box::new(VersionedAssets::V4( + vec![(Here, 100u128).into(), (Parent, 20u128).into()].into(), + )), + fee_asset_item: 1, // Fees are paid with the RelayToken + weight_limit: Unlimited, + }), + Some((who, extra())), + ); + let dry_run_effects = + runtime_api.dry_run_extrinsic(H256::zero(), extrinsic).unwrap().unwrap(); + + assert_eq!( + dry_run_effects.local_xcm, + Some(VersionedXcm::V4( + Xcm::builder_unsafe() + .withdraw_asset((Parent, 20u128)) + .burn_asset((Parent, 20u128)) + .withdraw_asset((Here, 100u128)) + .burn_asset((Here, 100u128)) + .build() + )), + ); + let send_destination = Location::new(1, [Parachain(1000)]); + let send_message = Xcm::<()>::builder_unsafe() + .withdraw_asset((Parent, 20u128)) + .buy_execution((Parent, 20u128), Unlimited) + .receive_teleported_asset(((Parent, Parachain(2000)), 100u128)) + .clear_origin() + .deposit_asset(AllCounted(2), [0u8; 32]) + .build(); + assert_eq!( + dry_run_effects.forwarded_xcms, + vec![( + VersionedLocation::V4(send_destination.clone()), + vec![VersionedXcm::V4(send_message.clone())], + ),], + ); + + assert_eq!( + dry_run_effects.emitted_events, + vec![ + RuntimeEvent::System(frame_system::Event::NewAccount { + account: 8660274132218572653 // TODO: Why is this not `1`? + }), + RuntimeEvent::Balances(pallet_balances::Event::Endowed { + account: 8660274132218572653, + free_balance: 100 + }), + RuntimeEvent::Balances(pallet_balances::Event::Minted { + who: 8660274132218572653, + amount: 100 + }), + RuntimeEvent::AssetsPallet(pallet_assets::Event::Burned { + asset_id: 1, + owner: 1, + balance: 20 + }), + RuntimeEvent::Balances(pallet_balances::Event::Burned { who: 1, amount: 100 }), + RuntimeEvent::XcmPallet(pallet_xcm::Event::Attempted { + outcome: Outcome::Complete { used: Weight::from_parts(400, 40) }, + }), + RuntimeEvent::Balances(pallet_balances::Event::Burned { who: 1, amount: 20 }), + RuntimeEvent::XcmPallet(pallet_xcm::Event::FeesPaid { + paying: AccountIndex64 { index: 1, network: None }.into(), + fees: (Here, 20u128).into(), + }), + RuntimeEvent::XcmPallet(pallet_xcm::Event::Sent { + origin: AccountIndex64 { index: 1, network: None }.into(), + destination: (Parent, Parachain(1000)).into(), + message: send_message.clone(), + message_id: fake_message_hash(&send_message), + }), + RuntimeEvent::System(frame_system::Event::ExtrinsicSuccess { + dispatch_info: DispatchInfo { + weight: Weight::from_parts(107074070, 0), /* Will break if weights get + * updated. */ + class: DispatchClass::Normal, + pays_fee: Pays::Yes, + } + }), + ] + ); + + // Weighing the local program is not relevant for extrinsics that already + // take this weight into account. + // In this case, we really only care about delivery fees. + let local_xcm = dry_run_effects.local_xcm.unwrap(); + + // We get a double result since the actual call returns a result and the runtime api returns + // results. + let weight = + runtime_api.query_xcm_weight(H256::zero(), local_xcm.clone()).unwrap().unwrap(); + assert_eq!(weight, Weight::from_parts(400, 40)); + let execution_fees = runtime_api + .query_weight_to_asset_fee( + H256::zero(), + weight, + VersionedAssetId::V4(HereLocation::get().into()), + ) + .unwrap() + .unwrap(); + assert_eq!(execution_fees, 440); + + let mut forwarded_xcms_iter = dry_run_effects.forwarded_xcms.into_iter(); + + let (destination, remote_messages) = forwarded_xcms_iter.next().unwrap(); + let remote_message = &remote_messages[0]; + + let delivery_fees = runtime_api + .query_delivery_fees(H256::zero(), destination.clone(), remote_message.clone()) + .unwrap() + .unwrap(); + assert_eq!(delivery_fees, VersionedAssets::V4((Here, 20u128).into())); + + // This would have to be the runtime API of the destination, + // which we have the location for. + // If I had a mock runtime configured for "AssetHub" then I would use the + // runtime APIs from that. + let remote_execution_weight = runtime_api + .query_xcm_weight(H256::zero(), remote_message.clone()) + .unwrap() + .unwrap(); + let remote_execution_fees = runtime_api + .query_weight_to_asset_fee( + H256::zero(), + remote_execution_weight, + VersionedAssetId::V4(HereLocation::get().into()), + ) + .unwrap() + .unwrap(); + assert_eq!(remote_execution_fees, 550); + + // Now we know that locally we need to use `execution_fees` and + // `delivery_fees`. + // On the message we forward to the destination, we need to + // put `remote_execution_fees` in `BuyExecution`. + // For the `transfer_assets` extrinsic, it just means passing the correct amount + // of fees in the parameters. + }); +} + +// Same scenario as in `fee_estimation_for_teleport`, but the user in parachain 2000 wants +// to send relay tokens over to parachain 1000. +// +// Reserve Asset Transfer Relay Token +// Reserve Asset Transfer Relay Token for fees +// Parachain(2000) -------------------------------------------> Parachain(1000) +#[test] +fn dry_run_reserve_asset_transfer() { + let _ = env_logger::builder().is_test(true).try_init(); + let who = 1; // AccountId = u64. + // Native token used for fees. + let balances = vec![(who, DeliveryFees::get() + ExistentialDeposit::get())]; + // Relay token is the one we want to transfer. + let assets = vec![(1, who, 100)]; // id, account_id, balance. + new_test_ext_with_balances_and_assets(balances, assets).execute_with(|| { + let client = TestClient; + let runtime_api = client.runtime_api(); + let extrinsic = TestXt::new( + RuntimeCall::XcmPallet(pallet_xcm::Call::transfer_assets { + dest: Box::new(VersionedLocation::V4((Parent, Parachain(1000)).into())), + beneficiary: Box::new(VersionedLocation::V4( + AccountId32 { id: [0u8; 32], network: None }.into(), + )), + assets: Box::new(VersionedAssets::V4((Parent, 100u128).into())), + fee_asset_item: 0, + weight_limit: Unlimited, + }), + Some((who, extra())), + ); + let dry_run_effects = + runtime_api.dry_run_extrinsic(H256::zero(), extrinsic).unwrap().unwrap(); + + assert_eq!( + dry_run_effects.local_xcm, + Some(VersionedXcm::V4( + Xcm::builder_unsafe() + .withdraw_asset((Parent, 100u128)) + .burn_asset((Parent, 100u128)) + .build() + )), + ); + + // In this case, the transfer type is `DestinationReserve`, so the remote xcm just withdraws + // the assets. + let send_destination = Location::new(1, Parachain(1000)); + let send_message = Xcm::<()>::builder_unsafe() + .withdraw_asset((Parent, 100u128)) + .clear_origin() + .buy_execution((Parent, 100u128), Unlimited) + .deposit_asset(AllCounted(1), [0u8; 32]) + .build(); + assert_eq!( + dry_run_effects.forwarded_xcms, + vec![( + VersionedLocation::V4(send_destination.clone()), + vec![VersionedXcm::V4(send_message.clone())], + ),], + ); + + assert_eq!( + dry_run_effects.emitted_events, + vec![ + RuntimeEvent::AssetsPallet(pallet_assets::Event::Burned { + asset_id: 1, + owner: 1, + balance: 100 + }), + RuntimeEvent::XcmPallet(pallet_xcm::Event::Attempted { + outcome: Outcome::Complete { used: Weight::from_parts(200, 20) } + }), + RuntimeEvent::Balances(pallet_balances::Event::Burned { who: 1, amount: 20 }), + RuntimeEvent::XcmPallet(pallet_xcm::Event::FeesPaid { + paying: AccountIndex64 { index: 1, network: None }.into(), + fees: (Here, 20u128).into() + }), + RuntimeEvent::XcmPallet(pallet_xcm::Event::Sent { + origin: AccountIndex64 { index: 1, network: None }.into(), + destination: send_destination.clone(), + message: send_message.clone(), + message_id: fake_message_hash(&send_message), + }), + RuntimeEvent::System(frame_system::Event::ExtrinsicSuccess { + dispatch_info: DispatchInfo { + weight: Weight::from_parts(107074066, 0), /* Will break if weights get + * updated. */ + class: DispatchClass::Normal, + pays_fee: Pays::Yes, + } + }), + ] + ); + }); +} + +#[test] +fn dry_run_xcm() { + let _ = env_logger::builder().is_test(true).try_init(); + let who = 1; // AccountId = u64. + let transfer_amount = 100u128; + // We need to build the XCM to weigh it and then build the real XCM that can pay for fees. + let inner_xcm = Xcm::<()>::builder_unsafe() + .buy_execution((Here, 1u128), Unlimited) // We'd need to query the destination chain for fees. + .deposit_asset(AllCounted(1), [0u8; 32]) + .build(); + let xcm_to_weigh = Xcm::::builder_unsafe() + .withdraw_asset((Here, transfer_amount)) + .clear_origin() + .buy_execution((Here, transfer_amount), Unlimited) + .deposit_reserve_asset(AllCounted(1), (Parent, Parachain(2100)), inner_xcm.clone()) + .build(); + let client = TestClient; + let runtime_api = client.runtime_api(); + let xcm_weight = runtime_api + .query_xcm_weight(H256::zero(), VersionedXcm::V4(xcm_to_weigh.clone().into())) + .unwrap() + .unwrap(); + let execution_fees = runtime_api + .query_weight_to_asset_fee(H256::zero(), xcm_weight, VersionedAssetId::V4(Here.into())) + .unwrap() + .unwrap(); + let xcm = Xcm::::builder_unsafe() + .withdraw_asset((Here, transfer_amount + execution_fees)) + .clear_origin() + .buy_execution((Here, execution_fees), Unlimited) + .deposit_reserve_asset(AllCounted(1), (Parent, Parachain(2100)), inner_xcm.clone()) + .build(); + let balances = vec![( + who, + transfer_amount + execution_fees + DeliveryFees::get() + ExistentialDeposit::get(), + )]; + new_test_ext_with_balances(balances).execute_with(|| { + let dry_run_effects = runtime_api + .dry_run_xcm( + H256::zero(), + VersionedLocation::V4(AccountIndex64 { index: 1, network: None }.into()), + VersionedXcm::V4(xcm), + ) + .unwrap() + .unwrap(); + assert_eq!( + dry_run_effects.forwarded_xcms, + vec![( + VersionedLocation::V4((Parent, Parachain(2100)).into()), + vec![VersionedXcm::V4( + Xcm::<()>::builder_unsafe() + .reserve_asset_deposited(( + (Parent, Parachain(2000)), + transfer_amount + execution_fees - DeliveryFees::get() + )) + .clear_origin() + .buy_execution((Here, 1u128), Unlimited) + .deposit_asset(AllCounted(1), [0u8; 32]) + .build() + )], + ),] + ); + + assert_eq!( + dry_run_effects.emitted_events, + vec![ + RuntimeEvent::Balances(pallet_balances::Event::Burned { who: 1, amount: 540 }), + RuntimeEvent::System(frame_system::Event::NewAccount { account: 2100 }), + RuntimeEvent::Balances(pallet_balances::Event::Endowed { + account: 2100, + free_balance: 520 + }), + RuntimeEvent::Balances(pallet_balances::Event::Minted { who: 2100, amount: 520 }), + ] + ); + }); +} diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/tests/mock.rs b/polkadot/xcm/xcm-fee-payment-runtime-api/tests/mock.rs new file mode 100644 index 000000000000..d7b18d90a501 --- /dev/null +++ b/polkadot/xcm/xcm-fee-payment-runtime-api/tests/mock.rs @@ -0,0 +1,525 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Mock runtime for tests. +//! Implements both runtime APIs for fee estimation and getting the messages for transfers. + +use codec::Encode; +use frame_support::{ + construct_runtime, derive_impl, parameter_types, + traits::{ + AsEnsureOriginWithArg, ConstU128, ConstU32, Contains, ContainsPair, Everything, Nothing, + OriginTrait, + }, + weights::WeightToFee as WeightToFeeT, +}; +use frame_system::{EnsureRoot, RawOrigin as SystemRawOrigin}; +use pallet_xcm::TestWeightInfo; +use sp_runtime::{ + traits::{Block as BlockT, Get, IdentityLookup, MaybeEquivalence, TryConvert}, + BuildStorage, SaturatedConversion, +}; +use sp_std::{cell::RefCell, marker::PhantomData}; +use xcm::{prelude::*, Version as XcmVersion}; +use xcm_builder::{ + AllowTopLevelPaidExecutionFrom, ConvertedConcreteId, EnsureXcmOrigin, FixedRateOfFungible, + FixedWeightBounds, FungibleAdapter, FungiblesAdapter, IsConcrete, MintLocation, NoChecking, + TakeWeightCredit, +}; +use xcm_executor::{ + traits::{ConvertLocation, JustTry}, + XcmExecutor, +}; + +use xcm_fee_payment_runtime_api::{ + dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunApi, XcmDryRunEffects}, + fees::{Error as XcmPaymentApiError, XcmPaymentApi}, +}; + +construct_runtime! { + pub enum TestRuntime { + System: frame_system, + Balances: pallet_balances, + AssetsPallet: pallet_assets, + XcmPallet: pallet_xcm, + } +} + +pub type SignedExtra = ( + // frame_system::CheckEra, + // frame_system::CheckNonce, + frame_system::CheckWeight, +); +pub type TestXt = sp_runtime::testing::TestXt; +type Block = sp_runtime::testing::Block; +type Balance = u128; +type AssetIdForAssetsPallet = u32; +type AccountId = u64; + +pub fn extra() -> SignedExtra { + (frame_system::CheckWeight::new(),) +} + +type Executive = frame_executive::Executive< + TestRuntime, + Block, + frame_system::ChainContext, + TestRuntime, + AllPalletsWithSystem, + (), +>; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for TestRuntime { + type Block = Block; + type AccountId = AccountId; + type AccountData = pallet_balances::AccountData; + type Lookup = IdentityLookup; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for TestRuntime { + type AccountStore = System; + type Balance = Balance; + type ExistentialDeposit = ExistentialDeposit; +} + +#[derive_impl(pallet_assets::config_preludes::TestDefaultConfig)] +impl pallet_assets::Config for TestRuntime { + type AssetId = AssetIdForAssetsPallet; + type Balance = Balance; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type Freezer = (); + type AssetDeposit = ConstU128<1>; + type AssetAccountDeposit = ConstU128<10>; + type MetadataDepositBase = ConstU128<1>; + type MetadataDepositPerByte = ConstU128<1>; + type ApprovalDeposit = ConstU128<1>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +thread_local! { + pub static SENT_XCM: RefCell)>> = const { RefCell::new(Vec::new()) }; +} + +pub(crate) fn sent_xcm() -> Vec<(Location, Xcm<()>)> { + SENT_XCM.with(|q| (*q.borrow()).clone()) +} + +pub struct TestXcmSender; +impl SendXcm for TestXcmSender { + type Ticket = (Location, Xcm<()>); + fn validate( + dest: &mut Option, + msg: &mut Option>, + ) -> SendResult { + let ticket = (dest.take().unwrap(), msg.take().unwrap()); + let fees: Assets = (HereLocation::get(), DeliveryFees::get()).into(); + Ok((ticket, fees)) + } + fn deliver(ticket: Self::Ticket) -> Result { + let hash = fake_message_hash(&ticket.1); + SENT_XCM.with(|q| q.borrow_mut().push(ticket)); + Ok(hash) + } +} + +pub(crate) fn fake_message_hash(message: &Xcm) -> XcmHash { + message.using_encoded(sp_io::hashing::blake2_256) +} + +pub type XcmRouter = TestXcmSender; + +parameter_types! { + pub const DeliveryFees: u128 = 20; // Random value. + pub const ExistentialDeposit: u128 = 1; // Random value. + pub const BaseXcmWeight: Weight = Weight::from_parts(100, 10); // Random value. + pub const MaxInstructions: u32 = 100; + pub const NativeTokenPerSecondPerByte: (AssetId, u128, u128) = (AssetId(HereLocation::get()), 1, 1); + pub UniversalLocation: InteriorLocation = [GlobalConsensus(NetworkId::Westend), Parachain(2000)].into(); + pub static AdvertisedXcmVersion: XcmVersion = 4; + pub const HereLocation: Location = Location::here(); + pub const RelayLocation: Location = Location::parent(); + pub const MaxAssetsIntoHolding: u32 = 64; + pub CheckAccount: AccountId = XcmPallet::check_account(); + pub LocalCheckAccount: (AccountId, MintLocation) = (CheckAccount::get(), MintLocation::Local); + pub const AnyNetwork: Option = None; +} + +/// Simple `WeightToFee` implementation that adds the ref_time by the proof_size. +pub struct WeightToFee; +impl WeightToFeeT for WeightToFee { + type Balance = Balance; + fn weight_to_fee(weight: &Weight) -> Self::Balance { + Self::Balance::saturated_from(weight.ref_time()) + .saturating_add(Self::Balance::saturated_from(weight.proof_size())) + } +} + +type Weigher = FixedWeightBounds; + +/// Matches the pair (NativeToken, AssetHub). +/// This is used in the `IsTeleporter` configuration item, meaning we accept our native token +/// coming from AssetHub as a teleport. +pub struct NativeTokenToAssetHub; +impl ContainsPair for NativeTokenToAssetHub { + fn contains(asset: &Asset, origin: &Location) -> bool { + matches!(asset.id.0.unpack(), (0, [])) && matches!(origin.unpack(), (1, [Parachain(1000)])) + } +} + +/// Matches the pair (RelayToken, AssetHub). +/// This is used in the `IsReserve` configuration item, meaning we accept the relay token +/// coming from AssetHub as a reserve asset transfer. +pub struct RelayTokenToAssetHub; +impl ContainsPair for RelayTokenToAssetHub { + fn contains(asset: &Asset, origin: &Location) -> bool { + matches!(asset.id.0.unpack(), (1, [])) && matches!(origin.unpack(), (1, [Parachain(1000)])) + } +} + +/// Converts locations that are only the `AccountIndex64` junction into local u64 accounts. +pub struct AccountIndex64Aliases(PhantomData<(Network, AccountId)>); +impl>, AccountId: From> ConvertLocation + for AccountIndex64Aliases +{ + fn convert_location(location: &Location) -> Option { + let index = match location.unpack() { + (0, [AccountIndex64 { index, network: None }]) => index, + (0, [AccountIndex64 { index, network }]) if *network == Network::get() => index, + _ => return None, + }; + Some((*index).into()) + } +} + +/// Custom location converter to turn sibling chains into u64 accounts. +pub struct SiblingChainToIndex64; +impl ConvertLocation for SiblingChainToIndex64 { + fn convert_location(location: &Location) -> Option { + let index = match location.unpack() { + (1, [Parachain(id)]) => id, + _ => return None, + }; + Some((*index).into()) + } +} + +/// We alias local account locations to actual local accounts. +/// We also allow sovereign accounts for other sibling chains. +pub type LocationToAccountId = (AccountIndex64Aliases, SiblingChainToIndex64); + +pub type NativeTokenTransactor = FungibleAdapter< + // We use pallet-balances for handling this fungible asset. + Balances, + // The fungible asset handled by this transactor is the native token of the chain. + IsConcrete, + // How we convert locations to accounts. + LocationToAccountId, + // We need to specify the AccountId type. + AccountId, + // We mint the native tokens locally, so we track how many we've sent away via teleports. + LocalCheckAccount, +>; + +pub struct LocationToAssetIdForAssetsPallet; +impl MaybeEquivalence for LocationToAssetIdForAssetsPallet { + fn convert(location: &Location) -> Option { + match location.unpack() { + (1, []) => Some(1 as AssetIdForAssetsPallet), + _ => None, + } + } + + fn convert_back(id: &AssetIdForAssetsPallet) -> Option { + match id { + 1 => Some(Location::new(1, [])), + _ => None, + } + } +} + +/// AssetTransactor for handling the relay chain token. +pub type RelayTokenTransactor = FungiblesAdapter< + // We use pallet-assets for handling the relay token. + AssetsPallet, + // Matches the relay token. + ConvertedConcreteId, + // How we convert locations to accounts. + LocationToAccountId, + // We need to specify the AccountId type. + AccountId, + // We don't track teleports. + NoChecking, + (), +>; + +pub type AssetTransactors = (NativeTokenTransactor, RelayTokenTransactor); + +pub struct HereAndInnerLocations; +impl Contains for HereAndInnerLocations { + fn contains(location: &Location) -> bool { + matches!(location.unpack(), (0, []) | (0, _)) + } +} + +pub type Barrier = ( + TakeWeightCredit, // We need this for pallet-xcm's extrinsics to work. + AllowTopLevelPaidExecutionFrom, /* TODO: Technically, we should allow + * messages from "AssetHub". */ +); + +pub type Trader = FixedRateOfFungible; + +pub struct XcmConfig; +impl xcm_executor::Config for XcmConfig { + type RuntimeCall = RuntimeCall; + type XcmSender = XcmRouter; + type AssetTransactor = AssetTransactors; + type OriginConverter = (); + type IsReserve = RelayTokenToAssetHub; + type IsTeleporter = NativeTokenToAssetHub; + type UniversalLocation = UniversalLocation; + type Barrier = Barrier; + type Weigher = Weigher; + type Trader = Trader; + type ResponseHandler = (); + type AssetTrap = (); + type AssetLocker = (); + type AssetExchanger = (); + type AssetClaims = (); + type SubscriptionService = (); + type PalletInstancesInfo = AllPalletsWithSystem; + type MaxAssetsIntoHolding = MaxAssetsIntoHolding; + type FeeManager = (); + type MessageExporter = (); + type UniversalAliases = (); + type CallDispatcher = RuntimeCall; + type SafeCallFilter = Nothing; + type Aliasers = Nothing; + type TransactionalProcessor = (); + type HrmpNewChannelOpenRequestHandler = (); + type HrmpChannelAcceptedHandler = (); + type HrmpChannelClosingHandler = (); + type XcmRecorder = XcmPallet; +} + +/// Converts a signed origin of a u64 account into a location with only the `AccountIndex64` +/// junction. +pub struct SignedToAccountIndex64( + PhantomData<(RuntimeOrigin, AccountId)>, +); +impl> TryConvert + for SignedToAccountIndex64 +where + RuntimeOrigin::PalletsOrigin: From> + + TryInto, Error = RuntimeOrigin::PalletsOrigin>, +{ + fn try_convert(origin: RuntimeOrigin) -> Result { + origin.try_with_caller(|caller| match caller.try_into() { + Ok(SystemRawOrigin::Signed(who)) => + Ok(Junction::AccountIndex64 { network: None, index: who.into() }.into()), + Ok(other) => Err(other.into()), + Err(other) => Err(other), + }) + } +} + +pub type LocalOriginToLocation = SignedToAccountIndex64; + +impl pallet_xcm::Config for TestRuntime { + type RuntimeEvent = RuntimeEvent; + type SendXcmOrigin = EnsureXcmOrigin; + type XcmRouter = XcmRouter; + type ExecuteXcmOrigin = EnsureXcmOrigin; + type XcmExecuteFilter = Nothing; + type XcmExecutor = XcmExecutor; + type XcmTeleportFilter = Everything; // Put everything instead of something more restricted. + type XcmReserveTransferFilter = Everything; // Same. + type Weigher = Weigher; + type UniversalLocation = UniversalLocation; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = AdvertisedXcmVersion; + type AdminOrigin = EnsureRoot; + type TrustedLockers = (); + type SovereignAccountOf = (); + type Currency = Balances; + type CurrencyMatcher = IsConcrete; + type MaxLockers = ConstU32<0>; + type MaxRemoteLockConsumers = ConstU32<0>; + type RemoteLockConsumerIdentifier = (); + type WeightInfo = TestWeightInfo; +} + +pub fn new_test_ext_with_balances(balances: Vec<(AccountId, Balance)>) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { balances } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +pub fn new_test_ext_with_balances_and_assets( + balances: Vec<(AccountId, Balance)>, + assets: Vec<(AssetIdForAssetsPallet, AccountId, Balance)>, +) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { balances } + .assimilate_storage(&mut t) + .unwrap(); + + pallet_assets::GenesisConfig:: { + assets: vec![ + // id, owner, is_sufficient, min_balance. + // We don't actually need this to be sufficient, since we use the native assets in + // tests for the existential deposit. + (1, 0, true, 1), + ], + metadata: vec![ + // id, name, symbol, decimals. + (1, "Relay Token".into(), "RLY".into(), 12), + ], + accounts: assets, + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +#[derive(Clone)] +pub(crate) struct TestClient; + +pub(crate) struct RuntimeApi { + _inner: TestClient, +} + +impl sp_api::ProvideRuntimeApi for TestClient { + type Api = RuntimeApi; + fn runtime_api(&self) -> sp_api::ApiRef { + RuntimeApi { _inner: self.clone() }.into() + } +} + +sp_api::mock_impl_runtime_apis! { + impl XcmPaymentApi for RuntimeApi { + fn query_acceptable_payment_assets(xcm_version: XcmVersion) -> Result, XcmPaymentApiError> { + if xcm_version != 4 { return Err(XcmPaymentApiError::UnhandledXcmVersion) }; + Ok(vec![VersionedAssetId::V4(HereLocation::get().into())]) + } + + fn query_xcm_weight(message: VersionedXcm<()>) -> Result { + XcmPallet::query_xcm_weight(message) + } + + fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result { + let local_asset = VersionedAssetId::V4(HereLocation::get().into()); + let asset = asset + .into_version(4) + .map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?; + + if asset != local_asset { return Err(XcmPaymentApiError::AssetNotFound); } + + Ok(WeightToFee::weight_to_fee(&weight)) + } + + fn query_delivery_fees(destination: VersionedLocation, message: VersionedXcm<()>) -> Result { + XcmPallet::query_delivery_fees(destination, message) + } + } + + impl XcmDryRunApi for RuntimeApi { + fn dry_run_extrinsic(extrinsic: ::Extrinsic) -> Result, XcmDryRunApiError> { + use xcm_executor::RecordXcm; + // We want to record the XCM that's executed, so we can return it. + pallet_xcm::Pallet::::set_record_xcm(true); + let result = Executive::apply_extrinsic(extrinsic).map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_extrinsic", + "Applying extrinsic failed with error {:?}", + error, + ); + XcmDryRunApiError::InvalidExtrinsic + })?; + // Nothing gets committed to storage in runtime APIs, so there's no harm in leaving the flag as true. + let local_xcm = pallet_xcm::Pallet::::recorded_xcm(); + let forwarded_xcms = sent_xcm() + .into_iter() + .map(|(location, message)| ( + VersionedLocation::V4(location), + vec![VersionedXcm::V4(message)], + )).collect(); + let events: Vec = System::events().iter().map(|record| record.event.clone()).collect(); + Ok(ExtrinsicDryRunEffects { + local_xcm: local_xcm.map(VersionedXcm::<()>::V4), + forwarded_xcms, + emitted_events: events, + execution_result: result, + }) + } + + fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm) -> Result, XcmDryRunApiError> { + let origin_location: Location = origin_location.try_into().map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_xcm", + "Location version conversion failed with error: {:?}", + error, + ); + XcmDryRunApiError::VersionedConversionFailed + })?; + let xcm: Xcm = xcm.try_into().map_err(|error| { + log::error!( + target: "xcm::XcmDryRunApi::dry_run_xcm", + "Xcm version conversion failed with error {:?}", + error, + ); + XcmDryRunApiError::VersionedConversionFailed + })?; + let mut hash = fake_message_hash(&xcm); + let result = XcmExecutor::::prepare_and_execute( + origin_location, + xcm, + &mut hash, + Weight::MAX, // Max limit available for execution. + Weight::zero(), + ); + let forwarded_xcms = sent_xcm() + .into_iter() + .map(|(location, message)| ( + VersionedLocation::V4(location), + vec![VersionedXcm::V4(message)], + )).collect(); + let events: Vec = System::events().iter().map(|record| record.event.clone()).collect(); + Ok(XcmDryRunEffects { + forwarded_xcms, + emitted_events: events, + execution_result: result, + }) + } + } +} diff --git a/polkadot/xcm/xcm-simulator/fuzzer/src/parachain.rs b/polkadot/xcm/xcm-simulator/fuzzer/src/parachain.rs index d8d65fbf0ce7..502bcca2d442 100644 --- a/polkadot/xcm/xcm-simulator/fuzzer/src/parachain.rs +++ b/polkadot/xcm/xcm-simulator/fuzzer/src/parachain.rs @@ -159,6 +159,7 @@ impl Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = (); } #[frame_support::pallet] diff --git a/polkadot/xcm/xcm-simulator/fuzzer/src/relay_chain.rs b/polkadot/xcm/xcm-simulator/fuzzer/src/relay_chain.rs index 47209b765d15..d20eb1218fbe 100644 --- a/polkadot/xcm/xcm-simulator/fuzzer/src/relay_chain.rs +++ b/polkadot/xcm/xcm-simulator/fuzzer/src/relay_chain.rs @@ -160,6 +160,7 @@ impl Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = (); } pub type LocalOriginToLocation = SignedToAccountId32; diff --git a/prdoc/pr_3872.prdoc b/prdoc/pr_3872.prdoc new file mode 100644 index 000000000000..3a5be3d2bc74 --- /dev/null +++ b/prdoc/pr_3872.prdoc @@ -0,0 +1,86 @@ +title: XcmDryRunApi - Runtime API for dry-running extrinsics and XCM programs. + +doc: + - audience: Runtime Dev + description: | + This PR introduces a new runtime API, the XcmDryRunApi, that allows dry-running + extrinsics and XCM programs to get their execution effects. + These effects include: + - Local execution result, either pass or fail + - Emitted events + - Forwarded XCMs + - In the case of extrinsics, the XCM program that they execute + This API can be used on its own to test extrinsics or XCM programs, + or used alongside the XcmPaymentApi to estimate execution and delivery + fees. + + This PR also adds a new configuration item to XCM: XcmRecorder. + This can be set to either (), the xcm pallet, or some custom implementation. + If set to (), the dry run API will not return the local XCM program executed + by running an extrinsic. + After this PR, it is necessary to add the new configuration item to your xcm + configs. + - audience: Runtime User + description: | + This PR introduces a new runtime API, the XcmDryRunApi, that allows dry-running + extrinsics and XCM programs to get their execution effects. + These effects include: + - Local execution result, either pass or fail + - Emitted events + - Forwarded XCMs + - In the case of extrinsics, the XCM program that they execute + This API can be used on its own to test extrinsics or XCM programs, + or used alongside the XcmPaymentApi to estimate execution and delivery + fees. + +crates: + - name: xcm-fee-payment-runtime-api + bump: major + - name: pallet-xcm + bump: minor + - name: staging-xcm-executor + bump: minor + - name: staging-xcm-builder + bump: minor + - name: rococo-runtime + bump: minor + - name: westend-runtime + bump: minor + - name: pallet-xcm-benchmarks + bump: minor + - name: asset-hub-rococo-runtime + bump: minor + - name: asset-hub-westend-runtime + bump: minor + - name: bridge-hub-rococo-runtime + bump: minor + - name: bridge-hub-westend-runtime + bump: minor + - name: collectives-westend-runtime + bump: minor + - name: contracts-rococo-runtime + bump: minor + - name: coretime-rococo-runtime + bump: minor + - name: coretime-westend-runtime + bump: minor + - name: glutton-westend-runtime + bump: minor + - name: people-rococo-runtime + bump: minor + - name: people-westend-runtime + bump: minor + - name: shell-runtime + bump: minor + - name: penpal-runtime + bump: minor + - name: rococo-parachain-runtime + bump: minor + - name: polkadot-service + bump: minor + - name: polkadot-test-runtime + bump: minor + - name: parachain-template-runtime + bump: minor + - name: pallet-contracts-mock-network + bump: minor diff --git a/substrate/frame/contracts/mock-network/src/parachain.rs b/substrate/frame/contracts/mock-network/src/parachain.rs index 843efab1502e..b46d7df6c2bc 100644 --- a/substrate/frame/contracts/mock-network/src/parachain.rs +++ b/substrate/frame/contracts/mock-network/src/parachain.rs @@ -285,6 +285,7 @@ impl Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; } impl mock_msg_queue::Config for Runtime { diff --git a/substrate/frame/contracts/mock-network/src/relay_chain.rs b/substrate/frame/contracts/mock-network/src/relay_chain.rs index d5e0ec9c83fa..36a7de499ba9 100644 --- a/substrate/frame/contracts/mock-network/src/relay_chain.rs +++ b/substrate/frame/contracts/mock-network/src/relay_chain.rs @@ -185,6 +185,7 @@ impl Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = XcmPallet; } pub type LocalOriginToLocation = SignedToAccountId32; diff --git a/templates/parachain/runtime/src/configs/xcm_config.rs b/templates/parachain/runtime/src/configs/xcm_config.rs index c6b6e8da1b89..e162bcbf8868 100644 --- a/templates/parachain/runtime/src/configs/xcm_config.rs +++ b/templates/parachain/runtime/src/configs/xcm_config.rs @@ -142,6 +142,7 @@ impl xcm_executor::Config for XcmConfig { type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; } /// No local origins on this chain are allowed to dispatch XCM sends/executions.