XCM: PayOverXcm config (#6900)

* Move XCM query functionality to trait

* Fix tests

* Add PayOverXcm implementation

* fix the PayOverXcm trait to compile

* moved doc comment out of trait implmeentation and to the trait

* PayOverXCM documentation

* Change documentation a bit

* Added empty benchmark methods implementation and changed docs

* update PayOverXCM to convert AccountIds to MultiLocations

* Implement benchmarking method

* Change v3 to latest

* Descend origin to an asset sender (#6970)

* descend origin to an asset sender

* sender as tuple of dest and sender

* Add more variants to the QueryResponseStatus enum

* Change Beneficiary to Into<[u8; 32]>

* update PayOverXcm to return concrete errors and use AccountId as sender

* use polkadot-primitives for AccountId

* fix dependency to use polkadot-core-primitives

* force Unpaid instruction to the top of the instructions list

* modify report_outcome to accept interior argument

* use new_query directly for building final xcm query, instead of report_outcome

* fix usage of new_query to use the XcmQueryHandler

* fix usage of new_query to use the XcmQueryHandler

* tiny method calling fix

* xcm query handler (#7198)

* drop redundant query status

* rename ReportQueryStatus to OuterQueryStatus

* revert rename of QueryResponseStatus

* update mapping

* Update xcm/xcm-builder/src/pay.rs

Co-authored-by: Gavin Wood <gavin@parity.io>

* Updates

* Docs

* Fix benchmarking stuff

* Destination can be determined based on asset_kind

* Tweaking API to minimise clones

* Some repotting and docs

---------

Co-authored-by: Anthony Alaribe <anthonyalaribe@gmail.com>
Co-authored-by: Muharem Ismailov <ismailov.m.h@gmail.com>
Co-authored-by: Anthony Alaribe <anthony.alaribe@parity.io>
Co-authored-by: Gavin Wood <gavin@parity.io>
This commit is contained in:
Francisco Aguirre
2023-05-31 07:09:44 -03:00
committed by GitHub
parent 50b53fcac3
commit a0e2aaad78
10 changed files with 375 additions and 65 deletions
+2 -1
View File
@@ -546,6 +546,7 @@ pub mod pallet_test_notifier {
use pallet_xcm::ensure_response;
use sp_runtime::DispatchResult;
use xcm::latest::prelude::*;
use xcm_executor::traits::QueryHandler as XcmQueryHandler;
#[pallet::pallet]
pub struct Pallet<T>(_);
@@ -581,7 +582,7 @@ pub mod pallet_test_notifier {
let id = who
.using_encoded(|mut d| <[u8; 32]>::decode(&mut d))
.map_err(|_| Error::<T>::BadAccountFormat)?;
let qid = pallet_xcm::Pallet::<T>::new_query(
let qid = <pallet_xcm::Pallet<T> as XcmQueryHandler>::new_query(
Junction::AccountId32 { network: None, id },
100u32.into(),
Here,
+62 -55
View File
@@ -52,8 +52,8 @@ use frame_system::pallet_prelude::*;
pub use pallet::*;
use xcm_executor::{
traits::{
CheckSuspension, ClaimAssets, DropAssets, MatchesFungible, OnResponse,
VersionChangeNotifier, WeightBounds,
CheckSuspension, ClaimAssets, DropAssets, MatchesFungible, OnResponse, QueryHandler,
QueryResponseStatus, VersionChangeNotifier, WeightBounds,
},
Assets,
};
@@ -1126,6 +1126,66 @@ pub mod pallet {
/// The maximum number of distinct assets allowed to be transferred in a single helper extrinsic.
const MAX_ASSETS_FOR_TRANSFER: usize = 2;
impl<T: Config> QueryHandler for Pallet<T> {
type QueryId = u64;
type BlockNumber = T::BlockNumber;
type Error = XcmError;
type UniversalLocation = T::UniversalLocation;
/// Attempt to create a new query ID and register it as a query that is yet to respond.
fn new_query(
responder: impl Into<MultiLocation>,
timeout: T::BlockNumber,
match_querier: impl Into<MultiLocation>,
) -> Self::QueryId {
Self::do_new_query(responder, None, timeout, match_querier).into()
}
/// To check the status of the query, use `fn query()` passing the resultant `QueryId`
/// value.
fn report_outcome(
message: &mut Xcm<()>,
responder: impl Into<MultiLocation>,
timeout: Self::BlockNumber,
) -> Result<Self::QueryId, Self::Error> {
let responder = responder.into();
let destination = Self::UniversalLocation::get()
.invert_target(&responder)
.map_err(|()| XcmError::LocationNotInvertible)?;
let query_id = Self::new_query(responder, timeout, Here);
let response_info = QueryResponseInfo { destination, query_id, max_weight: Weight::zero() };
let report_error = Xcm(vec![ReportError(response_info)]);
message.0.insert(0, SetAppendix(report_error));
Ok(query_id)
}
/// Removes response when ready and emits [Event::ResponseTaken] event.
fn take_response(query_id: Self::QueryId) -> QueryResponseStatus<Self::BlockNumber> {
match Queries::<T>::get(query_id) {
Some(QueryStatus::Ready { response, at }) => match response.try_into() {
Ok(response) => {
Queries::<T>::remove(query_id);
Self::deposit_event(Event::ResponseTaken { query_id });
QueryResponseStatus::Ready { response, at }
},
Err(_) => QueryResponseStatus::UnexpectedVersion,
},
Some(QueryStatus::Pending { timeout, .. }) => QueryResponseStatus::Pending { timeout },
Some(_) => QueryResponseStatus::UnexpectedVersion,
None => QueryResponseStatus::NotFound,
}
}
#[cfg(feature = "runtime-benchmarks")]
fn expect_response(id: Self::QueryId, response: Response) {
let response = response.into();
Queries::<T>::insert(
id,
QueryStatus::Ready { response, at: frame_system::Pallet::<T>::block_number() },
);
}
}
impl<T: Config> Pallet<T> {
fn do_reserve_transfer_assets(
origin: OriginFor<T>,
@@ -1497,36 +1557,6 @@ impl<T: Config> Pallet<T> {
})
}
/// Consume `message` and return another which is equivalent to it except that it reports
/// back the outcome.
///
/// - `message`: The message whose outcome should be reported.
/// - `responder`: The origin from which a response should be expected.
/// - `timeout`: The block number after which it is permissible for `notify` not to be
/// called even if a response is received.
///
/// `report_outcome` may return an error if the `responder` is not invertible.
///
/// It is assumed that the querier of the response will be `Here`.
///
/// To check the status of the query, use `fn query()` passing the resultant `QueryId`
/// value.
pub fn report_outcome(
message: &mut Xcm<()>,
responder: impl Into<MultiLocation>,
timeout: T::BlockNumber,
) -> Result<QueryId, XcmError> {
let responder = responder.into();
let destination = T::UniversalLocation::get()
.invert_target(&responder)
.map_err(|()| XcmError::LocationNotInvertible)?;
let query_id = Self::new_query(responder, timeout, Here);
let response_info = QueryResponseInfo { destination, query_id, max_weight: Weight::zero() };
let report_error = Xcm(vec![ReportError(response_info)]);
message.0.insert(0, SetAppendix(report_error));
Ok(query_id)
}
/// Consume `message` and return another which is equivalent to it except that it reports
/// back the outcome and dispatches `notify` on this chain.
///
@@ -1568,15 +1598,6 @@ impl<T: Config> Pallet<T> {
Ok(())
}
/// Attempt to create a new query ID and register it as a query that is yet to respond.
pub fn new_query(
responder: impl Into<MultiLocation>,
timeout: T::BlockNumber,
match_querier: impl Into<MultiLocation>,
) -> u64 {
Self::do_new_query(responder, None, timeout, match_querier)
}
/// Attempt to create a new query ID and register it as a query that is yet to respond, and
/// which will call a dispatchable when a response happens.
pub fn new_notify_query(
@@ -1591,20 +1612,6 @@ impl<T: Config> Pallet<T> {
Self::do_new_query(responder, Some(notify), timeout, match_querier)
}
/// Attempt to remove and return the response of query with ID `query_id`.
///
/// Returns `None` if the response is not (yet) available.
pub fn take_response(query_id: QueryId) -> Option<(Response, T::BlockNumber)> {
if let Some(QueryStatus::Ready { response, at }) = Queries::<T>::get(query_id) {
let response = response.try_into().ok()?;
Queries::<T>::remove(query_id);
Self::deposit_event(Event::ResponseTaken { query_id });
Some((response, at))
} else {
None
}
}
/// Note that a particular destination to whom we would like to send a message is unknown
/// and queue it for version discovery.
fn note_unknown_version(dest: &MultiLocation) {
+2 -1
View File
@@ -50,6 +50,7 @@ pub mod pallet_test_notifier {
use frame_system::pallet_prelude::*;
use sp_runtime::DispatchResult;
use xcm::latest::prelude::*;
use xcm_executor::traits::QueryHandler;
#[pallet::pallet]
pub struct Pallet<T>(_);
@@ -85,7 +86,7 @@ pub mod pallet_test_notifier {
let id = who
.using_encoded(|mut d| <[u8; 32]>::decode(&mut d))
.map_err(|_| Error::<T>::BadAccountFormat)?;
let qid = crate::Pallet::<T>::new_query(
let qid = <crate::Pallet<T> as QueryHandler>::new_query(
Junction::AccountId32 { network: None, id },
100u32.into(),
querier,
+5 -3
View File
@@ -28,7 +28,7 @@ use sp_runtime::traits::{AccountIdConversion, BlakeTwo256, Hash};
use xcm::{latest::QueryResponseInfo, prelude::*};
use xcm_builder::AllowKnownQueryResponses;
use xcm_executor::{
traits::{Properties, ShouldExecute},
traits::{Properties, QueryHandler, QueryResponseStatus, ShouldExecute},
XcmExecutor,
};
@@ -170,7 +170,8 @@ fn report_outcome_works() {
})
);
let response = Some((Response::ExecutionResult(None), 1));
let response =
QueryResponseStatus::Ready { response: Response::ExecutionResult(None), at: 1 };
assert_eq!(XcmPallet::take_response(0), response);
});
}
@@ -270,7 +271,8 @@ fn custom_querier_works() {
})
);
let response = Some((Response::ExecutionResult(None), 1));
let response =
QueryResponseStatus::Ready { response: Response::ExecutionResult(None), at: 1 };
assert_eq!(XcmPallet::take_response(0), response);
});
}
+2 -1
View File
@@ -36,7 +36,8 @@ polkadot-test-runtime = { path = "../../runtime/test-runtime" }
default = ["std"]
runtime-benchmarks = [
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks"
"frame-system/runtime-benchmarks",
"xcm-executor/runtime-benchmarks",
]
std = [
"log/std",
+6 -2
View File
@@ -28,8 +28,9 @@ pub mod test_utils;
mod location_conversion;
pub use location_conversion::{
Account32Hash, AccountId32Aliases, AccountKey20Aliases, ChildParachainConvertsVia,
GlobalConsensusParachainConvertsFor, ParentIsPreset, SiblingParachainConvertsVia,
Account32Hash, AccountId32Aliases, AccountKey20Aliases, AliasesIntoAccountId32,
ChildParachainConvertsVia, GlobalConsensusParachainConvertsFor, ParentIsPreset,
SiblingParachainConvertsVia,
};
mod origin_conversion;
@@ -95,3 +96,6 @@ pub use universal_exports::{
ExporterFor, HaulBlob, HaulBlobError, HaulBlobExporter, NetworkExportTable,
SovereignPaidRemoteExporter, UnpaidLocalExporter, UnpaidRemoteExporter,
};
mod pay;
pub use pay::{FixedLocation, LocatableAssetId, PayAccountId32OnChainOverXcm, PayOverXcm};
@@ -232,6 +232,26 @@ impl<Network: Get<Option<NetworkId>>, AccountId: From<[u8; 32]> + Into<[u8; 32]>
}
}
/// Conversion implementation which converts from a `[u8; 32]`-based `AccountId` into a
/// `MultiLocation` consisting solely of a `AccountId32` junction with a fixed value for its
/// network (provided by `Network`) and the `AccountId`'s `[u8; 32]` datum for the `id`.
pub struct AliasesIntoAccountId32<Network, AccountId>(PhantomData<(Network, AccountId)>);
impl<'a, Network: Get<Option<NetworkId>>, AccountId: Clone + Into<[u8; 32]> + Clone>
Convert<&'a AccountId, MultiLocation> for AliasesIntoAccountId32<Network, AccountId>
{
fn convert(who: &AccountId) -> Result<MultiLocation, &'a AccountId> {
Ok(AccountId32 { network: Network::get(), id: who.clone().into() }.into())
}
}
impl<Network: Get<Option<NetworkId>>, AccountId: Into<[u8; 32]> + Clone>
Convert<AccountId, MultiLocation> for AliasesIntoAccountId32<Network, AccountId>
{
fn convert(who: AccountId) -> Result<MultiLocation, AccountId> {
Ok(AccountId32 { network: Network::get(), id: who.into() }.into())
}
}
pub struct AccountKey20Aliases<Network, AccountId>(PhantomData<(Network, AccountId)>);
impl<Network: Get<Option<NetworkId>>, AccountId: From<[u8; 20]> + Into<[u8; 20]> + Clone>
Convert<MultiLocation, AccountId> for AccountKey20Aliases<Network, AccountId>
+205
View File
@@ -0,0 +1,205 @@
// Copyright Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot 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.
// Polkadot 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 <http://www.gnu.org/licenses/>.
//! `PayOverXcm` struct for paying through XCM and getting the status back.
use frame_support::traits::{
tokens::{Pay, PaymentStatus},
Get,
};
use sp_runtime::traits::Convert;
use sp_std::{marker::PhantomData, vec};
use xcm::{opaque::lts::Weight, prelude::*};
use xcm_executor::traits::{QueryHandler, QueryResponseStatus};
/// Implementation of the `frame_support::traits::tokens::Pay` trait, to allow
/// for XCM-based payments of a given `Balance` of some asset ID existing on some chain under
/// ownership of some `Interior` location of the local chain to a particular `Beneficiary`. The
/// `AssetKind` value can be converted into both the XCM `AssetId` (via and `Into` bound) and the
/// the destination chain's location, via the `AssetKindToLocatableAsset` type parameter.
///
/// This relies on the XCM `TransferAsset` instruction. A trait `BeneficiaryRefToLocation` must be
/// provided in order to convert the `Beneficiary` reference into a location usable by
/// `TransferAsset`.
///
/// `PayOverXcm::pay` is asynchronous, and returns a `QueryId` which can then be used in
/// `check_payment` to check the status of the XCM transaction.
///
/// See also `PayAccountId32OverXcm` which is similar to this except that `BeneficiaryRefToLocation`
/// need not be supplied and `Beneficiary` must implement `Into<[u8; 32]>`.
pub struct PayOverXcm<
Interior,
Router,
Querier,
Timeout,
Beneficiary,
AssetKind,
AssetKindToLocatableAsset,
BeneficiaryRefToLocation,
>(
PhantomData<(
Interior,
Router,
Querier,
Timeout,
Beneficiary,
AssetKind,
AssetKindToLocatableAsset,
BeneficiaryRefToLocation,
)>,
);
impl<
Interior: Get<InteriorMultiLocation>,
Router: SendXcm,
Querier: QueryHandler,
Timeout: Get<Querier::BlockNumber>,
Beneficiary: Clone,
AssetKind,
AssetKindToLocatableAsset: Convert<AssetKind, LocatableAssetId>,
BeneficiaryRefToLocation: for<'a> Convert<&'a Beneficiary, MultiLocation>,
> Pay
for PayOverXcm<
Interior,
Router,
Querier,
Timeout,
Beneficiary,
AssetKind,
AssetKindToLocatableAsset,
BeneficiaryRefToLocation,
>
{
type Beneficiary = Beneficiary;
type AssetKind = AssetKind;
type Balance = u128;
type Id = Querier::QueryId;
type Error = xcm::latest::Error;
fn pay(
who: &Self::Beneficiary,
asset_kind: Self::AssetKind,
amount: Self::Balance,
) -> Result<Self::Id, Self::Error> {
let locatable = AssetKindToLocatableAsset::convert(asset_kind);
let LocatableAssetId { asset_id, location: asset_location } = locatable;
let destination = Querier::UniversalLocation::get()
.invert_target(&asset_location)
.map_err(|()| Self::Error::LocationNotInvertible)?;
let beneficiary = BeneficiaryRefToLocation::convert(&who);
let query_id = Querier::new_query(asset_location, Timeout::get(), Interior::get());
let message = Xcm(vec![
UnpaidExecution { weight_limit: Unlimited, check_origin: None },
SetAppendix(Xcm(vec![ReportError(QueryResponseInfo {
destination,
query_id,
max_weight: Weight::zero(),
})])),
DescendOrigin(Interior::get()),
TransferAsset {
beneficiary,
assets: vec![MultiAsset { id: asset_id, fun: Fungibility::Fungible(amount) }]
.into(),
},
]);
let (ticket, _) = Router::validate(&mut Some(asset_location), &mut Some(message))?;
Router::deliver(ticket)?;
Ok(query_id.into())
}
fn check_payment(id: Self::Id) -> PaymentStatus {
use QueryResponseStatus::*;
match Querier::take_response(id) {
Ready { response, .. } => match response {
Response::ExecutionResult(None) => PaymentStatus::Success,
Response::ExecutionResult(Some(_)) => PaymentStatus::Failure,
_ => PaymentStatus::Unknown,
},
Pending { .. } => PaymentStatus::InProgress,
NotFound | UnexpectedVersion => PaymentStatus::Unknown,
}
}
#[cfg(feature = "runtime-benchmarks")]
fn ensure_successful(_: &Self::Beneficiary, _: Self::AssetKind, _: Self::Balance) {
// We cannot generally guarantee this will go through successfully since we don't have any
// control over the XCM transport layers. We just assume that the benchmark environment
// will be sending it somewhere sensible.
}
#[cfg(feature = "runtime-benchmarks")]
fn ensure_concluded(id: Self::Id) {
Querier::expect_response(id, Response::ExecutionResult(None));
}
}
/// Specialization of the [PayOverXcm] trait to allow `[u8; 32]`-based `AccountId` values to be
/// paid on a remote chain.
///
/// Implementation of the [frame_support::traits::tokens::Pay] trait, to allow
/// for XCM payments of a given `Balance` of `AssetKind` existing on a `DestinationChain` under
/// ownership of some `Interior` location of the local chain to a particular `Beneficiary`.
///
/// This relies on the XCM `TransferAsset` instruction. `Beneficiary` must implement
/// `Into<[u8; 32]>` (as 32-byte `AccountId`s generally do), and the actual XCM beneficiary will be
/// the location consisting of a single `AccountId32` junction with an appropriate account and no
/// specific network.
///
/// `PayOverXcm::pay` is asynchronous, and returns a `QueryId` which can then be used in
/// `check_payment` to check the status of the XCM transaction.
pub type PayAccountId32OnChainOverXcm<
DestinationChain,
Interior,
Router,
Querier,
Timeout,
Beneficiary,
AssetKind,
> = PayOverXcm<
Interior,
Router,
Querier,
Timeout,
Beneficiary,
AssetKind,
crate::AliasesIntoAccountId32<(), Beneficiary>,
FixedLocation<DestinationChain>,
>;
/// Simple struct which contains both an XCM `location` and `asset_id` to identift an asset which
/// exists on some chain.
pub struct LocatableAssetId {
/// The asset's ID.
pub asset_id: AssetId,
/// The (relative) location in which the asset ID is meaningful.
pub location: MultiLocation,
}
/// Adapter `struct` which implements a conversion from any `AssetKind` into a [LocatableAsset]
/// value using a fixed `Location` for the `location` field.
pub struct FixedLocation<Location>(sp_std::marker::PhantomData<Location>);
impl<Location: Get<MultiLocation>, AssetKind: Into<AssetId>> Convert<AssetKind, LocatableAssetId>
for FixedLocation<Location>
{
fn convert(value: AssetKind) -> LocatableAssetId {
LocatableAssetId { asset_id: value.into(), location: Location::get() }
}
}
#[test]
fn it_builds() {}
+1 -1
View File
@@ -38,7 +38,7 @@ pub use token_matching::{
Error, MatchesFungible, MatchesFungibles, MatchesNonFungible, MatchesNonFungibles,
};
mod on_response;
pub use on_response::{OnResponse, VersionChangeNotifier};
pub use on_response::{OnResponse, QueryHandler, QueryResponseStatus, VersionChangeNotifier};
mod should_execute;
pub use should_execute::{CheckSuspension, Properties, ShouldExecute};
mod transact_asset;
@@ -14,8 +14,17 @@
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use crate::Xcm;
use core::result;
use frame_support::{
dispatch::fmt::Debug,
pallet_prelude::{Get, TypeInfo},
};
use parity_scale_codec::{FullCodec, MaxEncodedLen};
use sp_arithmetic::traits::Zero;
use xcm::latest::{
Error as XcmError, MultiLocation, QueryId, Response, Result as XcmResult, Weight, XcmContext,
Error as XcmError, InteriorMultiLocation, MultiLocation, QueryId, Response,
Result as XcmResult, Weight, XcmContext,
};
/// Define what needs to be done upon receiving a query response.
@@ -94,3 +103,63 @@ impl VersionChangeNotifier for () {
false
}
}
/// The possible state of an XCM query response.
#[derive(Debug, PartialEq, Eq)]
pub enum QueryResponseStatus<BlockNumber> {
/// The response has arrived, and includes the inner Response and the block number it arrived at.
Ready { response: Response, at: BlockNumber },
/// The response has not yet arrived, the XCM might still be executing or the response might be in transit.
Pending { timeout: BlockNumber },
/// No response with the given `QueryId` was found, or the response was already queried and removed from local storage.
NotFound,
/// Got an unexpected XCM version.
UnexpectedVersion,
}
/// Provides methods to expect responses from XCMs and query their status.
pub trait QueryHandler {
type QueryId: From<u64>
+ FullCodec
+ MaxEncodedLen
+ TypeInfo
+ Clone
+ Eq
+ PartialEq
+ Debug
+ Copy;
type BlockNumber: Zero;
type Error;
type UniversalLocation: Get<InteriorMultiLocation>;
/// Attempt to create a new query ID and register it as a query that is yet to respond.
fn new_query(
responder: impl Into<MultiLocation>,
timeout: Self::BlockNumber,
match_querier: impl Into<MultiLocation>,
) -> QueryId;
/// Consume `message` and return another which is equivalent to it except that it reports
/// back the outcome.
///
/// - `message`: The message whose outcome should be reported.
/// - `responder`: The origin from which a response should be expected.
/// - `timeout`: The block number after which it is permissible to return `NotFound` from `take_response`.
///
/// `report_outcome` may return an error if the `responder` is not invertible.
///
/// It is assumed that the querier of the response will be `Here`.
/// The response can be queried with `take_response`.
fn report_outcome(
message: &mut Xcm<()>,
responder: impl Into<MultiLocation>,
timeout: Self::BlockNumber,
) -> result::Result<Self::QueryId, Self::Error>;
/// Attempt to remove and return the response of query with ID `query_id`.
fn take_response(id: Self::QueryId) -> QueryResponseStatus<Self::BlockNumber>;
/// Makes sure to expect a response with the given id.
#[cfg(feature = "runtime-benchmarks")]
fn expect_response(id: Self::QueryId, response: Response);
}