It's long been a problem to estimate execution fees for XCM.
This became even more of a problem once delivery fees were introduced.
In order to fix this issue, two new runtime APIs were introduced to existing runtimes and are simple to integrate into any runtime that wants this functionality.
The XcmPaymentApi
allows users to query the execution and delivery fees required by XCMs.
Let's first talk about using these APIs to solve the problem of estimating XCM fees.
To do this, we'll estimate fees for an example.
Alice is using AssetHub but has to create an identity on the People chain.
To do this, she has to transfer some DOT to the People chain via XCM.
What fees are there in this example?
There are:
The overall fees are local_execution_fees + delivery_fees + remote_execution_fees
.
Let's take a look at how we can calculate these.
We'll use PolkadotAPI (PAPI) for these snippets.
We'll be using V4 for sending a Teleport.
const xcm = {
type: 'V4',
value: [
{
type: 'WithdrawAsset',
value: [
{
id: { parents: 1, value: { type: 'Here', value: undefined } },
fun: { type: 'Fungible', value: 100n }
}
],
},
{
type: 'BuyExecution',
value: {
asset: {
id: { parents: 1, interior: { type: 'Here', value: undefined } },
fun: { type: 'Fungible', value: 100n },
},
},
},
{
type: 'InitiateTeleport',
value: {
destination: ...,
assets: [...],
remote_fees: {
id: { parents: 1, interior: { type: 'Here', value: undefined } },
fun: { type: 'Fungible', value: 1n },
},
remote_xcm: [
{ type: 'DepositAsset', value: ... },
],
}
},
],
};
The process is first putting a large value in PayFees
, then estimate the fees and then put them back in.
For getting the local_execution_fees
on AssetHub
, Alice
can use the XcmPaymentApi
.
She first weighs the XCM using query_xcm_weight
, this returns a weight value.
This weight can be turned into a fee, this can be done with query_weight_to_asset_fee
, but an asset also has to be specified.
To get a list of all assets that can be used for fee payment, query_acceptable_payment_assets
can be used.
Since Alice wants to use DOT, she can specify { parents: 1, interior: Here }
, the location of the Polkadot network (relay chain) relative to AssetHub.
Here's what it might look like with PAPI (Typescript):
// These will be set if the runtime API calls are successful.
let localExecutionFees = 0;
// We query the weight of the xcm.
const weightResult = await api.apis.XcmPaymentApi.query_xcm_weight(xcm);
if (weightResult.success) {
// We convert the weight to a fee amount.
// The asset is { parents: 1, interior: Here }, aka, DOT.
const executionFeesResult = await api.apis.XcmPaymentApi.query_weight_to_asset_fee(
weightResult.value,
XcmVersionedAssetId.V4({
parents: 1,
interior: { type: 'Here', value: undefined },
}),
);
if (executionFeesResult.success) {
localExecutionFees = executionFeesResult.value;
}
}
To get all other fees, we need to take a look at the DryRunApi
.
This API lets you dry-run any call or xcm and receive a bunch of information about it.
Since Alice uses pallet-xcm's execute
call to submit her xcm, she has to dry-run the xcm.
This can be done with the dry_run_xcm
function of the API.
This function takes the xcm, but also the origin with which we are executing it.
These means this API can be used with any origin you want, useful for dry-running governance proposals.
Since Alice is an account on AssetHub, her location relative to AssetHub (and thus her origin) is { parents: 0, interior: X1(AccountId32 { id: Alice, network: Polkadot }) }
.
The result of this dry-run will be a collection of things:
execution_result
: Whether or not the actual execution would succeed.emitted_events
: All events emitted during execution.forwarded_xcms
: A list of xcms sent to other locations.More about this return type can be seen in the Rust docs.
We can now use these forwarded_xcms
to know what message was sent where and plug it in XcmPaymentApi::query_delivery_fees
.
This function takes the destination and the message.
Here's how it might look like with PAPI:
let deliveryFees = 0n;
const origin = VersionedLocation.V4({
parents: 1,
interior: { type: 'Here', value: undefined },
});
// We dry run the local xcm.
const dryRunResult = await api.apis.DryRunApi.dry_run_xcm(origin, xcm);
if (dryRunResult.success) {
// We extract the sent xcms from the dry run result.
const { forwarded_xcms: forwardedXcms } = dryRunResult.value;
const xcmsToPeople = forwardedXcms.find(([location, _]) => (
location.type === 'V4' &&
location.value.parents === 1 &&
location.value.interior.type === 'X1' &&
location.value.interior.value[0].type === 'Parachain' &&
location.value.interior.value[0].value === 1004 // People's ParaID.
));
const [destination, messages] = xcmsToPeople[0];
const remoteXcm = messages[0];
const deliveryFeesResult = await api.apis.XcmPaymentApi.query_delivery_fees(destination, remoteXcm);
if (deliveryFeesResult.success) {
const assets = deliveryFeesResult.value;
deliveryFees = (
assets.type === 'V4' &&
assets.value[0].fun.type === 'Fungible' &&
assets.value[0].fun.value.valueOf()
) || 0n;
}
}
For calculating the remote execution fees we'll reuse the result of the dry-run we got from the last section.
Since we have the destination and the xcm that will be executed there, we can identify the destination chain and create a client connecting to it.
Then, we can do the same thing we did in the "Local execution fees" section but on the destination chain.
If there are more hops in the journey, this process can be repeated infinitely.
Now for the runtime section of this guide.
These APIs are already implemented in system parachains, but how do I implement it in my parachain?
This is thankfully very easy thanks to most of the implementation being abstracted away in pallet-xcm
.
Here's a snippet for implementing this API on a runtime:
impl xcm_runtime_apis::fees::XcmPaymentApi<Block> for Runtime {
fn query_acceptable_payment_assets(xcm_version: xcm::Version) -> Result<Vec<VersionedAssetId>, XcmPaymentApiError> {
let acceptable_assets = vec![AssetId(NativeToken::get())];
XcmPallet::query_acceptable_payment_assets(xcm_version, acceptable_assets)
}
fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result<u128, XcmPaymentApiError> {
let latest_asset_id: Result<AssetId, ()> = asset.try_into();
match latest_asset_id {
Ok(asset_id) if asset_id.0 == NativeToken::get() => {
Ok(WeightToFee::weight_to_fee(&weight))
},
_ => todo!("Error handling"),
}
}
fn query_xcm_weight(message: VersionedXcm<()>) -> Result<Weight, XcmPaymentApiError> {
XcmPallet::query_xcm_weight(message)
}
fn query_delivery_fees(destination: VersionedLocation, message: VersionedXcm<()>) -> Result<VersionedAssets, XcmPaymentApiError> {
XcmPallet::query_delivery_fees(destination, message)
}
}
As you can see, the most work you need to do is in query_acceptable_payment_assets
and query_weight_to_asset_fee
since they depend on how many assets your runtime accepts for fee payment.
In the AssetHub runtime, for example, both DOT and any asset in a liquidity pool with DOT are accepted for fee payment.
The implementations of these functions are appropriately more complicated.
This API is even more straightforward to implement.
The most important part is that you correctly specify the generics that pallet-xcm requires.
Here's a snippet for implementing this API on a runtime:
impl xcm_runtime_apis::dry_run::DryRunApi<Block, RuntimeCall, RuntimeEvent, OriginCaller> for Runtime {
fn dry_run_call(origin: OriginCaller, call: RuntimeCall) -> Result<CallDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
XcmPallet::dry_run_call::<Runtime, xcm_config::XcmRouter, OriginCaller, RuntimeCall>(origin, call)
}
fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm<RuntimeCall>) -> Result<XcmDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
XcmPallet::dry_run_xcm::<Runtime, xcm_config::XcmRouter, RuntimeCall, xcm_config::XcmConfig>(origin_location, xcm)
}
}