Send a Transaction While Paying the Fee with a Different TokenΒΆ
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.
Polkadot.js API Maintenance Mode
The Polkadot.js API is no longer actively developed. New projects should use Polkadot-API (PAPI) or Dedot as actively maintained alternatives.
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ΒΆ
Fork the Polkadot Hub locally using Chopsticks:
This 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ΒΆ
-
Create a new directory and initialize the project:
-
Initialize the project:
-
Install dev dependencies:
-
Install dependencies:
-
Create TypeScript configuration:
-
Generate Polkadot API types for Polkadot Hub:
-
Create a new file called
fee-payment-transaction.ts:
-
Create a new directory and initialize the project:
-
Initialize the project:
-
Install dependencies:
-
Create a new file called
fee-payment-transaction.js:
-
Create a new Rust project:
-
Install the Subxt CLI to download chain metadata:
-
Download Polkadot Hub metadata from the local Chopsticks fork:
Note
Ensure your Chopsticks fork is running at
ws://localhost:8000before downloading the metadata. -
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.44.2", features = ["jsonrpsee", "native"] } subxt-signer = { version = "0.44.2", features = ["sr25519"] } tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread"] } -
Create the source file:
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ΒΆ
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 { getPolkadotSigner } from "polkadot-api/signer";
import { createClient } from "polkadot-api";
import { assetHub } from "@polkadot-api/descriptors";
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat";
import { getWsProvider } from "polkadot-api/ws-provider/node";
import { MultiAddress } from "@polkadot-api/descriptors";
const TARGET_ADDRESS = "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3"; // Bob's address
const TRANSFER_AMOUNT = 3_000_000_000n; // 3 DOT
const 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 address
const TRANSFER_AMOUNT = 3_000_000_000n; // 3 DOT
const 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, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder, 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 module
use asset_hub::runtime_types::staging_xcm::v5::{
junction::Junction,
junctions::Junctions,
location::Location,
};
// Define a custom config where AssetId is an XCM Location
pub enum 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 ExtrinsicParams = DefaultExtrinsicParams<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 DOT
const USDT_ASSET_ID: u128 = 1984;
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 = getPolkadotSigner(
hdkdKeyPair.publicKey,
"Sr25519",
hdkdKeyPair.sign
);
return polkadotSigner;
};
const client = createClient(
withPolkadotSdkCompat(
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)");
// Create Alice's dev keypair
let alice = subxt_signer::sr25519::dev::alice();
println!("Sender (Alice): {}", AccountId32::from(alice.public_key()));
Create the TransactionΒΆ
Create a standard DOT transfer transaction that sends 3 DOT to Bob's address while keeping Alice's account alive:
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 signAndSubmit options:
const signer = await createSigner();
const result = await tx.signAndSubmit(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 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);
});
});
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 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 = api
.tx()
.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?;
Full CodeΒΆ
Complete Code
import { sr25519CreateDerive } from "@polkadot-labs/hdkd";
import {
DEV_PHRASE,
entropyToMiniSecret,
mnemonicToEntropy,
} from "@polkadot-labs/hdkd-helpers";
import { getPolkadotSigner } from "polkadot-api/signer";
import { createClient } from "polkadot-api";
import { assetHub } from "@polkadot-api/descriptors";
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat";
import { getWsProvider } from "polkadot-api/ws-provider/node";
import { MultiAddress } from "@polkadot-api/descriptors";
const TARGET_ADDRESS = "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3"; // Bob's address
const TRANSFER_AMOUNT = 3_000_000_000n; // 3 DOT
const 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 = getPolkadotSigner(
hdkdKeyPair.publicKey,
"Sr25519",
hdkdKeyPair.sign
);
return polkadotSigner;
};
const client = createClient(
withPolkadotSdkCompat(
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.signAndSubmit(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 address
const TRANSFER_AMOUNT = 3_000_000_000n; // 3 DOT
const 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, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder, 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 module
use asset_hub::runtime_types::staging_xcm::v5::{
junction::Junction,
junctions::Junctions,
location::Location,
};
// Define a custom config where AssetId is an XCM Location
pub enum 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 ExtrinsicParams = DefaultExtrinsicParams<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 DOT
const 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)");
// 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::tx()
.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 = api
.tx()
.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.variant_name()
);
}
Ok(())
}
Run the ScriptΒΆ
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