The Asset Conversion Pallet
allows us to use a Non-Native Asset to create a Liquidity Pool with a Native Asset or another Non-Native Asset (but this option is yet to be implemented on a System Chain). This in turn grants us the possibility of using that Non-Native Asset to pay for transaction fees via the ChargeAssetConversionTxPayment
signed extension, as long as it has a Liquidity Pool against a Native Asset.
Here we aim to illustrate how to use the ChargeAssetConversionTxPayment
signed extension to pay for the fees of a balances.transfer_keep_alive()
call with a Non-Native Asset. For this, we will first create and mint the asset using the Assets Pallet, and then we'll create a Liquidity Pool against the Native Asset and add liquidity to it using the Asset Conversion Pallet.
For this example we will use polkadot-js. At the time of writing the ChargeAssetConversionTxPayment
signed extension is not operational in the polkadot-js api and subsequently it's not available in the tools that use the api, such as txwrapper-core
. This issue is patched in our example, but a more permanent fix is expected: Same as with the
assetConversionApi.quotePriceExactTokensForTokens
that expects XcmV3MultiLocation
for the assets, while Asset Hub Westend only
supports MultiLocation
, problem patched by passing our own definition of assetConversionApi.quotePriceExactTokensForTokens
at the time of creation of the ApiPromise.
We will also use zombienet to spawn the Westend Relay Chain and Westend Asset Hub nodes. For this we use the binaries built from the polkadot-sdk repo, with the consideration of building the polkadot binary with the flag --features=fast-runtime
, in order to decrease the epoch time. We also need to build the three binaries for running the nodes as local.
To run this example first you need to have a zombienet running. For this, from the root directory run:
~ ./zombienet/<zombienet-binary-for-your-OS> -p native spawn ./zombienet/westend_network.toml
Then, cd into the polkadot-js directory and run:
~ yarn example
And there you go, you can check the outputs for the different stages of the example.
Asset Hub Rococo doesn't support XcmV3MultiLocation, so the default implementation of the assetConversionApi
will fail, for that we have to inject a version which uses MultiLocation
instead, so that we can use assetConversionApi.quotePriceExactTokensForTokens
. This correction is injected as follows:
const api = await ApiPromise.create({
provider: wsProvider,
typesBundle: apiConfigRuntime,
},
);
With the modified version of assetConversionApi.quotePriceExactTokensForTokens
constructed as follows:
const apiConfigRuntime = {
spec: {
statemine: {
runtime: {
AssetConversionApi: [
{
methods: {
quote_price_exact_tokens_for_tokens: {
description: 'Quote price: tokens for exact tokens',
params: [
{
name: 'asset1',
type: 'MultiLocation',
},
{
name: 'asset2',
type: 'MultiLocation',
},
{
name: 'amount',
type: 'u128',
},
{
name: 'include_fee',
type: 'bool',
},
],
type: 'Option<(Balance)>',
},
},
version: 1,
},
],
},
},
},
};
After that, we proceed to create a batch of transactions in which we create the asset and set its metadata, as well as creating the liquidity pool and adding liquidity to it, minting liquidity pool tokens, after defining our Native and Custom Assets in the shape of MultiLocations:
const asset = {
parents: 0,
interior: {
X2: [
{ palletInstance: 50 },
{ generalIndex: ASSET_ID },
]
}
};
const native = {
parents: 1,
interior: {
Here: '',
},
};
const setupTxs = [];
const create = api.tx.assets.create(ASSET_ID, alice.address, ASSET_MIN);
const setMetadata = api.tx.assets.setMetadata(ASSET_ID, ASSET_NAME, ASSET_TICKER, ASSET_DECIMALS);
const mint = api.tx.assets.mint(ASSET_ID, alice.address, 100000000);
const createPool = api.tx.assetConversion.createPool(native, asset);
const addLiquidity = api.tx.assetConversion.addLiquidity(native, asset, 1000000000000, 500000, 0, 0, alice.address);
setupTxs.push(create);
setupTxs.push(setMetadata);
setupTxs.push(mint);
setupTxs.push(createPool);
setupTxs.push(addLiquidity);
await api.tx.utility.batchAll(setupTxs).signAndSend(alice);
Here we can see when our Liqudity Pool was created:
And here when the liqudity was added and the liquidity pool tokens were issued:
We also want to estimate how much the fees will be for our transaction, for which we use paymentInfo()
:
const transferInfo = await api.tx.balances.transferKeepAlive(bob.address, 2000000).paymentInfo(alice);
Now we have the fee estimation, we can estimate the fee in the Non-Native Asset through the runtime api assetConversionApi.quotePriceExactTokensForTokens
:
const convertedFee = await api.call.assetConversionApi.quotePriceExactTokensForTokens(native, asset, transferInfo.partialFee, true);
Now we can finally make our transfer and pay the fees with our Non-Native Asset. For this we just have to specify the MultiLocation
of our Non-Native Asset as the assetId
:
const tx = await api.tx.balances
.transferKeepAlive(bob.address, 2000000)
.signAsync(alice, { assetId: asset });
tx.send(alice)
To then send the transfer:
And here we can see when the Tx Fee was paid with our Custom Asset:
And when the swap was made for the payment:
And if we look closely, the amount paid is close to our estimation.
With this, we have succesfully gone through the whole process of creating and minting an asset, creating its own liquidity pool against the Native Asset, and using it to pay the fees of a transaction despite our Custom Asset not being sufficient. This grants more flexibility to the use of Custom Assets in environments where the Asset Conversion Pallet is implemented.
Thank you for your attention and we hope this example was useful.
NOTE: Some pieces of code have been omitted to keep this example at a reasonable length, but the full code can be seen in this repo.