Send a Transaction While Paying the Fee with a Different Token
Introduction
Section titled “Introduction”Polkadot Hub allows users to pay transaction fees using alternative tokens instead of the native token. This tutorial demonstrates how to send a DOT transfer transaction while paying the fees using USDT on Polkadot Hub.
You can follow this tutorial using Polkadot-API (PAPI), Polkadot.js API, or Subxt. Select your preferred SDK in the code tabs below.
Prerequisites
Section titled “Prerequisites”Before starting, ensure you have the following installed:
- Chopsticks — to fork Polkadot Hub locally
- Your preferred SDK:
- Polkadot-API (PAPI) (TypeScript)
- Polkadot.js API (JavaScript)
- Subxt (Rust)
Local Polkadot Hub Setup
Section titled “Local Polkadot Hub Setup”Fork the Polkadot Hub locally using Chopsticks:
chopsticks -c polkadot-asset-hubThis command forks the Polkadot Hub chain, making it available at ws://localhost:8000. When running polkadot-asset-hub, you use the Polkadot Hub fork with the configuration specified in the polkadot-asset-hub.yml file. This configuration defines Alice's account with USDT assets. If you want to use a different chain, ensure the account you use has the necessary assets.
Set Up Your Project
Section titled “Set Up Your Project”-
Create a new directory and initialize the project:
Terminal window mkdir fee-payment-tutorial && cd fee-payment-tutorial -
Initialize the project:
Terminal window npm init -y -
Install dev dependencies:
Terminal window npm install --save-dev @types/node@^22.12.0 ts-node@^10.9.2 typescript@^5.7.3 -
Install dependencies:
Terminal window npm install --save @polkadot-labs/hdkd@0.0.32 @polkadot-labs/hdkd-helpers@0.0.34 polkadot-api@3.3.0 -
Create TypeScript configuration:
Terminal window npx tsc --init && npm pkg set type=module -
Generate Polkadot API types for Polkadot Hub:
Terminal window npx papi add assetHub -n polkadot_asset_hub -
Create a new file called
fee-payment-transaction.ts:Terminal window touch fee-payment-transaction.ts
-
Create a new directory and initialize the project:
Terminal window mkdir fee-payment-tutorial && cd fee-payment-tutorial -
Initialize the project:
Terminal window npm init -y && npm pkg set type=module -
Install dependencies:
Terminal window npm install @polkadot/api@17.0.2 -
Create a new file called
fee-payment-transaction.js:Terminal window touch fee-payment-transaction.js
-
Create a new Rust project:
Terminal window cargo new subxt-fee-payment-example && cd subxt-fee-payment-example -
Install the Subxt CLI to download chain metadata:
Terminal window cargo install subxt-cli@0.50.1 -
Download Polkadot Hub metadata from the local Chopsticks fork:
Terminal window mkdir metadata && \subxt metadata --url ws://localhost:8000 -o metadata/asset_hub.scale -
Update
Cargo.tomlwith the required dependencies:Cargo.toml [package]name = "subxt-fee-payment-example"version = "0.1.0"edition = "2021"[[bin]]name = "fee_payment_transaction"path = "src/bin/fee_payment_transaction.rs"[dependencies]codec = { package = "parity-scale-codec", version = "3", features = ["derive"] }subxt = { version = "0.50.0", features = ["jsonrpsee", "native"] }subxt-signer = { version = "0.50.0", features = ["sr25519"] }tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread"] } -
Create the source file:
Terminal window mkdir -p src/bin && touch src/bin/fee_payment_transaction.rs
Implementation
Section titled “Implementation”The following sections cover how to set up imports and constants, create a transaction signer, connect to Polkadot Hub, and send a DOT transfer transaction while paying fees in USDT.
Import Dependencies and Define Constants
Section titled “Import Dependencies and Define Constants”Set up the required imports and define the target address, transfer amount, and USDT asset ID for your transaction:
import { sr25519CreateDerive } from "@polkadot-labs/hdkd";import { DEV_PHRASE, entropyToMiniSecret, mnemonicToEntropy,} from "@polkadot-labs/hdkd-helpers";import { getTxCreator } from "polkadot-api/tx-creator";import { createClient } from "polkadot-api";import { assetHub } from "@polkadot-api/descriptors";import { getWsProvider } from "polkadot-api/ws";import { MultiAddress } from "@polkadot-api/descriptors";
const TARGET_ADDRESS = "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3"; // Bob's addressconst TRANSFER_AMOUNT = 3_000_000_000n; // 3 DOTconst USDT_ASSET_ID = 1984;import { ApiPromise, WsProvider } from '@polkadot/api';import { Keyring } from '@polkadot/keyring';import { cryptoWaitReady } from '@polkadot/util-crypto';
const TARGET_ADDRESS = '14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3'; // Bob's addressconst TRANSFER_AMOUNT = 3_000_000_000n; // 3 DOTconst USDT_ASSET_ID = 1984;In Subxt, you first generate types from the chain metadata using the #[subxt::subxt()] macro. The derive_for_type attribute ensures the Location type implements the traits needed for encoding. You also define a custom AssetHubConfig where type AssetId = Location, which enables specifying an XCM location as the fee payment asset:
use std::str::FromStr;use subxt::config::{Config, DefaultExtrinsicParamsBuilder, DefaultTransactionExtensions, PolkadotConfig};use subxt::utils::AccountId32;use subxt::{OnlineClient, SubstrateConfig};
// Generate types from the Polkadot Hub metadata with Location trait derives#[subxt::subxt( runtime_metadata_path = "metadata/asset_hub.scale", derive_for_type( path = "staging_xcm::v5::location::Location", derive = "Clone, Eq, PartialEq, codec::Encode", recursive ))]pub mod asset_hub {}
// Import XCM location types from the generated metadata moduleuse asset_hub::runtime_types::staging_xcm::v5::{ junction::Junction, junctions::Junctions, location::Location,};
// Define a custom config where AssetId is an XCM Location#[derive(Debug, Default, Clone)]pub struct AssetHubConfig;
impl Config for AssetHubConfig { type AccountId = <PolkadotConfig as Config>::AccountId; type Address = <PolkadotConfig as Config>::Address; type Signature = <PolkadotConfig as Config>::Signature; type Hasher = <PolkadotConfig as Config>::Hasher; type Header = <SubstrateConfig as Config>::Header; type TransactionExtensions = DefaultTransactionExtensions<AssetHubConfig>; type AssetId = Location;}
const POLKADOT_HUB_RPC: &str = "ws://localhost:8000";const TARGET_ADDRESS: &str = "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3";const TRANSFER_AMOUNT: u128 = 3_000_000_000; // 3 DOTconst USDT_ASSET_ID: u128 = 1984;Create a Signer and Connect
Section titled “Create a Signer and Connect”Create a signer using Alice's development account and connect to the local Polkadot Hub:
const createSigner = async () => { const entropy = mnemonicToEntropy(DEV_PHRASE); const miniSecret = entropyToMiniSecret(entropy); const derive = sr25519CreateDerive(miniSecret); const hdkdKeyPair = derive("//Alice"); const polkadotSigner = getTxCreator( hdkdKeyPair.publicKey, "Sr25519", hdkdKeyPair.sign ); return polkadotSigner;};
const client = createClient( getWsProvider("ws://localhost:8000") // Chopsticks Polkadot Hub);
const api = client.getTypedApi(assetHub);The cryptoWaitReady() call ensures the underlying WASM cryptographic libraries are initialized before creating the keyring:
async function main() { // Wait for crypto libraries to be ready await cryptoWaitReady();
// Create a keyring instance and add Alice's account const keyring = new Keyring({ type: 'sr25519' }); const alice = keyring.addFromUri('//Alice');
// Connect to the local Chopsticks Polkadot Hub fork const wsProvider = new WsProvider('ws://localhost:8000'); const api = await ApiPromise.create({ provider: wsProvider }); console.log('Connected to Polkadot Hub (Chopsticks fork)');Notice that the OnlineClient is parameterized with AssetHubConfig instead of the default PolkadotConfig:
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { // Connect to the local Chopsticks Polkadot Hub fork let api = OnlineClient::<AssetHubConfig>::from_url(POLKADOT_HUB_RPC).await?; println!("Connected to Polkadot Hub (Chopsticks fork)");
// Anchor to the current block let at_block = api.at_current_block().await?;
// Create Alice's dev keypair let alice = subxt_signer::sr25519::dev::alice(); println!("Sender (Alice): {}", AccountId32::from(alice.public_key()));Create the Transaction
Section titled “Create the Transaction”Create a standard DOT transfer transaction that sends 3 DOT to Bob's address while keeping Alice's account alive:
const tx = api.tx.Balances.transfer_keep_alive({ dest: MultiAddress.Id(TARGET_ADDRESS), value: BigInt(TRANSFER_AMOUNT),});// Create the transfer transactionconst tx = api.tx.balances.transferKeepAlive(TARGET_ADDRESS, TRANSFER_AMOUNT);// Create the balance transfer transactionlet dest = AccountId32::from_str(TARGET_ADDRESS)?;let tx = asset_hub::transactions() .balances() .transfer_keep_alive(dest.into(), TRANSFER_AMOUNT);Sign and Submit with Alternative Fee Payment
Section titled “Sign and Submit with Alternative Fee Payment”The key part of this tutorial is specifying an alternative asset for fee payment. The USDT asset is identified using the XCM location format, where PalletInstance(50) refers to the Assets pallet and GeneralIndex(1984) identifies the USDT asset on Polkadot Hub:
In PAPI, you specify the alternative asset through the asset parameter in the createAndSubmit options:
const signer = await createSigner();
const result = await tx.createAndSubmit(signer, { asset: { parents: 0, interior: { type: "X2", value: [ { type: "PalletInstance", value: 50 }, { type: "GeneralIndex", value: BigInt(USDT_ASSET_ID) }, ], }, },});
const { txHash, ok, block, events } = result;console.log(`Tx finalized: ${txHash} (ok=${ok})`);console.log(`Block: #${block.number} ${block.hash} [tx index ${block.index}]`);
console.log("Events:");for (const ev of events) { const type = (ev as any).type ?? "unknown"; console.log(`- ${type}`);}
process.exit(0);In Polkadot.js, you define the asset as an XCM multi-location object and pass it as the assetId option to signAndSend:
// Define the USDT asset as an XCM multi-location for fee paymentconst assetId = { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: USDT_ASSET_ID }], },};
// Sign and send the transaction, paying fees with USDTconsole.log('Signing and submitting transaction...');await new Promise((resolve, reject) => { let unsubscribe; tx .signAndSend( alice, { assetId }, ({ status, events, txHash, dispatchError }) => { if (status.isFinalized) { console.log( `\nTransaction finalized in block: ${status.asFinalized.toHex()}` ); console.log(`Transaction hash: ${txHash.toHex()}`);
// Display all events console.log('\nEvents:'); events.forEach(({ event }) => { console.log(` ${event.section}.${event.method}`); });
if (unsubscribe) { unsubscribe(); }
if (dispatchError) { if (dispatchError.isModule) { const decoded = api.registry.findMetaError( dispatchError.asModule ); const { docs, name, section } = decoded; reject(new Error(`${section}.${name}: ${docs.join(' ')}`)); } else { reject(new Error(dispatchError.toString())); } } else { resolve(); } } } ) .then((unsub) => { unsubscribe = unsub; }) .catch((error) => { if (unsubscribe) { unsubscribe(); } reject(error); });});In Subxt, you use DefaultExtrinsicParamsBuilder with tip_of(0, asset_location) to specify the fee asset. The first argument is the tip amount (0), and the second is the XCM Location. Instead of calling sign_and_submit_then_watch_default, you pass the custom tx_params to sign_and_submit_then_watch:
// Define the USDT asset location in XCM formatlet asset_location = Location { parents: 0, interior: Junctions::X2([ Junction::PalletInstance(50), Junction::GeneralIndex(USDT_ASSET_ID), ]),};
// Build transaction params to pay fees with the alternative assetlet tx_params = DefaultExtrinsicParamsBuilder::<AssetHubConfig>::new() .tip_of(0, asset_location) .build();
// Sign, submit, and watch for finalizationprintln!("Signing and submitting transaction...");let progress = at_block .transactions() .sign_and_submit_then_watch(&tx, &alice, tx_params) .await?;
let in_block = progress.wait_for_finalized().await?;let block_hash = in_block.block_hash();let events = in_block.wait_for_success().await?;
// Display transaction resultsprintln!("\nTransaction finalized in block: {:?}", block_hash);
println!("\nEvents:");for event in events.iter() { let event = event?; println!( " {}.{}", event.pallet_name(), event.event_name() );}Full Code
Section titled “Full Code”Complete Code
import { sr25519CreateDerive } from "@polkadot-labs/hdkd";import { DEV_PHRASE, entropyToMiniSecret, mnemonicToEntropy,} from "@polkadot-labs/hdkd-helpers";import { getTxCreator } from "polkadot-api/tx-creator";import { createClient } from "polkadot-api";import { assetHub } from "@polkadot-api/descriptors";import { getWsProvider } from "polkadot-api/ws";import { MultiAddress } from "@polkadot-api/descriptors";
const TARGET_ADDRESS = "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3"; // Bob's addressconst TRANSFER_AMOUNT = 3_000_000_000n; // 3 DOTconst USDT_ASSET_ID = 1984;
const createSigner = async () => { const entropy = mnemonicToEntropy(DEV_PHRASE); const miniSecret = entropyToMiniSecret(entropy); const derive = sr25519CreateDerive(miniSecret); const hdkdKeyPair = derive("//Alice"); const polkadotSigner = getTxCreator( hdkdKeyPair.publicKey, "Sr25519", hdkdKeyPair.sign ); return polkadotSigner;};
const client = createClient( getWsProvider("ws://localhost:8000") // Chopsticks Polkadot Hub);
const api = client.getTypedApi(assetHub);
const tx = api.tx.Balances.transfer_keep_alive({ dest: MultiAddress.Id(TARGET_ADDRESS), value: BigInt(TRANSFER_AMOUNT),});
const signer = await createSigner();
const result = await tx.createAndSubmit(signer, { asset: { parents: 0, interior: { type: "X2", value: [ { type: "PalletInstance", value: 50 }, { type: "GeneralIndex", value: BigInt(USDT_ASSET_ID) }, ], }, },});
const { txHash, ok, block, events } = result;console.log(`Tx finalized: ${txHash} (ok=${ok})`);console.log(`Block: #${block.number} ${block.hash} [tx index ${block.index}]`);
console.log("Events:");for (const ev of events) { const type = (ev as any).type ?? "unknown"; console.log(`- ${type}`);}
process.exit(0);Complete Code
import { ApiPromise, WsProvider } from '@polkadot/api';import { Keyring } from '@polkadot/keyring';import { cryptoWaitReady } from '@polkadot/util-crypto';
const TARGET_ADDRESS = '14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3'; // Bob's addressconst TRANSFER_AMOUNT = 3_000_000_000n; // 3 DOTconst USDT_ASSET_ID = 1984;
async function main() { // Wait for crypto libraries to be ready await cryptoWaitReady();
// Create a keyring instance and add Alice's account const keyring = new Keyring({ type: 'sr25519' }); const alice = keyring.addFromUri('//Alice');
// Connect to the local Chopsticks Polkadot Hub fork const wsProvider = new WsProvider('ws://localhost:8000'); const api = await ApiPromise.create({ provider: wsProvider }); console.log('Connected to Polkadot Hub (Chopsticks fork)');
// Create the transfer transaction const tx = api.tx.balances.transferKeepAlive(TARGET_ADDRESS, TRANSFER_AMOUNT);
// Define the USDT asset as an XCM multi-location for fee payment const assetId = { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: USDT_ASSET_ID }], }, };
// Sign and send the transaction, paying fees with USDT console.log('Signing and submitting transaction...'); await new Promise((resolve, reject) => { let unsubscribe; tx .signAndSend( alice, { assetId }, ({ status, events, txHash, dispatchError }) => { if (status.isFinalized) { console.log( `\nTransaction finalized in block: ${status.asFinalized.toHex()}` ); console.log(`Transaction hash: ${txHash.toHex()}`);
// Display all events console.log('\nEvents:'); events.forEach(({ event }) => { console.log(` ${event.section}.${event.method}`); });
if (unsubscribe) { unsubscribe(); }
if (dispatchError) { if (dispatchError.isModule) { const decoded = api.registry.findMetaError( dispatchError.asModule ); const { docs, name, section } = decoded; reject(new Error(`${section}.${name}: ${docs.join(' ')}`)); } else { reject(new Error(dispatchError.toString())); } } else { resolve(); } } } ) .then((unsub) => { unsubscribe = unsub; }) .catch((error) => { if (unsubscribe) { unsubscribe(); } reject(error); }); });
// Disconnect from the node await api.disconnect();}
main().catch(console.error);Complete Code
use std::str::FromStr;use subxt::config::{Config, DefaultExtrinsicParamsBuilder, DefaultTransactionExtensions, PolkadotConfig};use subxt::utils::AccountId32;use subxt::{OnlineClient, SubstrateConfig};
// Generate types from the Polkadot Hub metadata with Location trait derives#[subxt::subxt( runtime_metadata_path = "metadata/asset_hub.scale", derive_for_type( path = "staging_xcm::v5::location::Location", derive = "Clone, Eq, PartialEq, codec::Encode", recursive ))]pub mod asset_hub {}
// Import XCM location types from the generated metadata moduleuse asset_hub::runtime_types::staging_xcm::v5::{ junction::Junction, junctions::Junctions, location::Location,};
// Define a custom config where AssetId is an XCM Location#[derive(Debug, Default, Clone)]pub struct AssetHubConfig;
impl Config for AssetHubConfig { type AccountId = <PolkadotConfig as Config>::AccountId; type Address = <PolkadotConfig as Config>::Address; type Signature = <PolkadotConfig as Config>::Signature; type Hasher = <PolkadotConfig as Config>::Hasher; type Header = <SubstrateConfig as Config>::Header; type TransactionExtensions = DefaultTransactionExtensions<AssetHubConfig>; type AssetId = Location;}
const POLKADOT_HUB_RPC: &str = "ws://localhost:8000";const TARGET_ADDRESS: &str = "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3";const TRANSFER_AMOUNT: u128 = 3_000_000_000; // 3 DOTconst USDT_ASSET_ID: u128 = 1984;
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { // Connect to the local Chopsticks Polkadot Hub fork let api = OnlineClient::<AssetHubConfig>::from_url(POLKADOT_HUB_RPC).await?; println!("Connected to Polkadot Hub (Chopsticks fork)");
// Anchor to the current block let at_block = api.at_current_block().await?;
// Create Alice's dev keypair let alice = subxt_signer::sr25519::dev::alice(); println!("Sender (Alice): {}", AccountId32::from(alice.public_key()));
// Create the balance transfer transaction let dest = AccountId32::from_str(TARGET_ADDRESS)?; let tx = asset_hub::transactions() .balances() .transfer_keep_alive(dest.into(), TRANSFER_AMOUNT);
// Define the USDT asset location in XCM format let asset_location = Location { parents: 0, interior: Junctions::X2([ Junction::PalletInstance(50), Junction::GeneralIndex(USDT_ASSET_ID), ]), };
// Build transaction params to pay fees with the alternative asset let tx_params = DefaultExtrinsicParamsBuilder::<AssetHubConfig>::new() .tip_of(0, asset_location) .build();
// Sign, submit, and watch for finalization println!("Signing and submitting transaction..."); let progress = at_block .transactions() .sign_and_submit_then_watch(&tx, &alice, tx_params) .await?;
let in_block = progress.wait_for_finalized().await?; let block_hash = in_block.block_hash(); let events = in_block.wait_for_success().await?;
// Display transaction results println!("\nTransaction finalized in block: {:?}", block_hash);
println!("\nEvents:"); for event in events.iter() { let event = event?; println!( " {}.{}", event.pallet_name(), event.event_name() ); }
Ok(())}Run the Script
Section titled “Run the Script”npx ts-node fee-payment-transaction.tsnode fee-payment-transaction.jscargo run --bin fee_payment_transactionExpected Output
Section titled “Expected Output”When you run the script successfully, you should see output similar to:
Tx finalized: 0xfe4e3fa64d374e256c72463c507743f16672caaf1b4e539fe913026de394009e (ok=true) Block: #12255461 0xaf315c306304ad175e4e24c5c8cbf97518c1411183bbf81a6107209a49d84f4d [tx index 2] Events: - Assets - Balances - Assets - AssetConversion - Balances - System - Balances - Balances - Balances - AssetTxPayment - System
Connected to Polkadot Hub (Chopsticks fork) Signing and submitting transaction... Transaction finalized in block: 0x1f4849218bb4c04564a6c6f69c9e40a3940dcdabdc089da01bb49fb471a2c049 Transaction hash: 0x9c967bb79fd09579f5e530a0446ce0171efe9241ba5957d6bcba80bccd5f66da Events: assets.Withdrawn balances.Withdraw assets.Deposited assetConversion.SwapCreditExecuted balances.Upgraded system.NewAccount balances.Endowed balances.Transfer balances.Deposit assetTxPayment.AssetTxFeePaid system.ExtrinsicSuccess
Connected to Polkadot Hub (Chopsticks fork) Sender (Alice): 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY Signing and submitting transaction... Transaction finalized in block: 0x90c92e4dca64631ab4ccaabb14273cebbb3d59205db437dfcf9ace91452b1434 Events: Assets.Withdrawn Balances.Withdraw Assets.Deposited AssetConversion.SwapCreditExecuted Balances.Upgraded System.NewAccount Balances.Endowed Balances.Transfer Balances.Deposit AssetTxPayment.AssetTxFeePaid System.ExtrinsicSuccess
The key events to look for are:
AssetTxPayment: Confirms the fees were paid using the alternative asset.AssetConversion: The alternative asset was swapped to cover native fees.Balances: The transfer was executed and deposit and withdrawal events were emitted.System: The transaction completed successfully.
Conclusion
Section titled “Conclusion”Paying transaction fees with alternative tokens on Polkadot Hub provides significant flexibility for users and applications.
The key takeaway is understanding how to specify alternative assets using the XCM location format, which opens up possibilities for building applications that can operate entirely using specific token ecosystems while still leveraging the full power of the network.
Where to Go Next
Section titled “Where to Go Next”-
Send Transactions with SDKs
Learn how to send various types of transactions using different SDKs.
-
Calculate Transaction Fees
Understand how to estimate transaction fees before submitting.