Send Transactions with SDKs
Introduction
Section titled “Introduction”Sending transactions on Polkadot SDK-based blockchains involves constructing an extrinsic (transaction), signing it with your account's private key, and submitting it to the network. Each SDK provides different methods for transaction construction, signing, and submission.
This guide demonstrates how to send transactions using five popular SDKs:
- Polkadot API (PAPI): Modern TypeScript library with type-safe APIs
- Polkadot.js API: Comprehensive JavaScript library (maintenance mode)
- Dedot: Lightweight TypeScript library optimized for performance
- Python Substrate Interface: Python library for Substrate chains
- Subxt: Rust library with compile-time type safety
Select your preferred SDK below to see complete, runnable examples that send balance transfer transactions on Polkadot Hub.
Prerequisites
Section titled “Prerequisites”- Access to a Polkadot-SDK-compatible wallet, with its mnemonic phrase or private key.
- A funded account on Polkadot Hub, with some testnet tokens. You can use the Polkadot Faucet to obtain test tokens.
Send Transactions
Section titled “Send Transactions”Prerequisites
- Node.js v18 or higher
- npm, pnpm, or yarn package manager
Environment Setup
-
Create and initialize a new project:
Terminal window mkdir papi-send-tx-example && cd papi-send-tx-example && \npm init -y && npm pkg set type=module -
Install dependencies:
Terminal window npm install polkadot-api@3.3.0 @polkadot/util-crypto @polkadot/keyring && \npm install --save-dev @types/node tsx typescript -
Generate types for Polkadot Hub TestNet:
Terminal window npx papi add polkadotTestNet -w wss://asset-hub-paseo.dotters.network
Send Balance Transfer
The following example constructs, signs, and submits a balance transfer transaction.
Create a file named send-transfer.ts and add the following code to it:
import { createClient } from 'polkadot-api';import { getWsProvider } from 'polkadot-api/ws';import { polkadotTestNet } from '@polkadot-api/descriptors';import { cryptoWaitReady } from '@polkadot/util-crypto';import { Keyring } from '@polkadot/keyring';import { getTxCreator } from 'polkadot-api/tx-creator';
const POLKADOT_TESTNET_RPC = 'INSERT_WS_ENDPOINT';const SENDER_MNEMONIC = 'INSERT_MNEMONIC';const DEST_ADDRESS = 'INSERT_DEST_ADDRESS';const AMOUNT = 1_000_000_000n; // 1 PAS (adjust decimals as needed)
async function main() { try { await cryptoWaitReady();
const keyring = new Keyring({ type: 'sr25519' }); const sender = keyring.addFromMnemonic(SENDER_MNEMONIC);
console.log(`Sender address: ${sender.address}`);
// Create the client connection const client = createClient(getWsProvider(POLKADOT_TESTNET_RPC));
// Get the typed API const api = client.getTypedApi(polkadotTestNet); console.log('Connected to Polkadot Testnet');
// Create signer using getTxCreator const signer = getTxCreator( sender.publicKey, 'Sr25519', async (input) => sender.sign(input) );
// Construct and submit the transfer transaction const tx = api.tx.Balances.transfer_keep_alive({ dest: { type: 'Id', value: DEST_ADDRESS }, value: AMOUNT, });
console.log('\nSigning and submitting transaction...'); // Polkadot Hub TestNet requires a value for its RestrictOrigins extension const { txHash } = await tx.createAndSubmit(signer, { customSignedExtensions: { RestrictOrigins: { value: true } }, }); console.log(`Transaction submitted with hash: ${txHash}`);
await client.destroy(); console.log('Disconnected'); } catch (error) { console.error('Error:', error); process.exit(1); }}
main();Run the script:
npx tsx send-transfer.tsYou should see output similar to:
Prerequisites
- Node.js v18 or higher
- npm, pnpm, or yarn package manager
Environment Setup
-
Create and initialize a new project:
Terminal window mkdir pjs-send-tx-example && cd pjs-send-tx-example && \npm init -y && npm pkg set type=module -
Install dependencies:
Terminal window npm install @polkadot/api @polkadot/keyring @polkadot/util-crypto
Send Balance Transfer
The following example constructs, signs, and submits a balance transfer transaction.
Create a file named send-transfer.js and add the following code:
import { ApiPromise, WsProvider } from '@polkadot/api';import { Keyring } from '@polkadot/keyring';import { cryptoWaitReady } from '@polkadot/util-crypto';
const POLKADOT_TESTNET_RPC = 'INSERT_WS_ENDPOINT';const SENDER_MNEMONIC = 'INSERT_MNEMONIC';const DEST_ADDRESS = 'INSERT_DEST_ADDRESS';const AMOUNT = 1_000_000_000n; // 1 PAS (adjust decimals as needed)
async function main() { // Wait for crypto to be ready await cryptoWaitReady();
// Create a WebSocket provider const wsProvider = new WsProvider(POLKADOT_TESTNET_RPC);
// Initialize the API const api = await ApiPromise.create({ provider: wsProvider }); console.log('Connected to Polkadot Testnet');
// Set up keyring and get sender account const keyring = new Keyring({ type: 'sr25519' }); const sender = keyring.addFromMnemonic(SENDER_MNEMONIC); const senderAddress = sender.address;
console.log(`Sender address: ${senderAddress}`); console.log(`Recipient address: ${DEST_ADDRESS}`); console.log(`Amount: ${AMOUNT} (${Number(AMOUNT) / 1_000_000_000} PAS)\n`);
// Get sender's account info to check balance const accountInfo = await api.query.system.account(senderAddress); console.log( `Sender balance: ${accountInfo.data.free.toString()} (${ Number(accountInfo.data.free.toBigInt()) / 1_000_000_000 } PAS)` );
// Construct and sign the transfer transaction console.log('\nSigning and submitting transaction...');
await new Promise((resolve, reject) => { api.tx.balances .transferKeepAlive(DEST_ADDRESS, AMOUNT) .signAndSend(sender, ({ status, txHash, dispatchError }) => { if (status.isInBlock) { console.log( `Transaction included in block: ${status.asInBlock.toHex()}` ); } else if (status.isFinalized) { console.log( `Transaction finalized in block: ${status.asFinalized.toHex()}` ); console.log(`Transaction hash: ${txHash.toHex()}`);
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 { console.log('Transaction successful!'); resolve(txHash.toHex()); } } }) .catch(reject); });
// Disconnect from the node await api.disconnect();}
main().catch(console.error);Run the script:
node send-transfer.jsYou should see output similar to:
Prerequisites
- Node.js v18 or higher
- npm, pnpm, or yarn package manager
Environment Setup
-
Create and initialize a new project:
Terminal window mkdir dedot-send-tx-example && cd dedot-send-tx-example && \npm init -y && npm pkg set type=module -
Install dependencies:
Terminal window npm install dedot @polkadot/keyring @polkadot/util-crypto && \npm install --save-dev @dedot/chaintypes @types/node tsx typescript
Send Balance Transfer
The following example constructs, signs, and submits a balance transfer transaction.
Create a file named send-transfer.ts and add the following code to it:
import { DedotClient, WsProvider } from 'dedot';import type { PolkadotAssetHubApi } from '@dedot/chaintypes';import { cryptoWaitReady } from '@polkadot/util-crypto';import { Keyring } from '@polkadot/keyring';
const POLKADOT_TESTNET_RPC = 'INSERT_WS_ENDPOINT';const SENDER_MNEMONIC = 'INSERT_MNEMONIC';const DEST_ADDRESS = 'INSERT_DEST_ADDRESS';const AMOUNT = 1_000_000_000n; // 1 PAS (adjust decimals as needed)
async function main() { // Wait for crypto to be ready await cryptoWaitReady();
// Initialize provider and client with Asset Hub types const provider = new WsProvider(POLKADOT_TESTNET_RPC); const client = await DedotClient.new<PolkadotAssetHubApi>(provider);
console.log('Connected to Polkadot Testnet');
// Set up keyring and get sender account const keyring = new Keyring({ type: 'sr25519' }); const sender = keyring.addFromMnemonic(SENDER_MNEMONIC); const senderAddress = sender.address;
console.log(`Sender address: ${senderAddress}`); console.log(`Recipient address: ${DEST_ADDRESS}`); console.log(`Amount: ${AMOUNT} (${AMOUNT / 1_000_000_000n} PAS)\n`);
// Get sender's account info to check balance const accountInfo = await client.query.system.account(senderAddress); console.log(`Sender balance: ${accountInfo.data.free}`);
// Sign and submit the transfer transaction console.log('\nSigning and submitting transaction...');
// Wait for transaction to complete using a Promise await new Promise<void>((resolve, reject) => { client.tx.balances .transferKeepAlive(DEST_ADDRESS, AMOUNT) .signAndSend(sender, async ({ status, txHash, dispatchError }) => { console.log(`Transaction status: ${status.type}`);
// Log transaction hash immediately if (txHash) { console.log( `Transaction hash: ${ typeof txHash === 'string' ? txHash : txHash.toHex() }` ); }
if (status.type === 'BestChainBlockIncluded') { console.log( `Transaction included in block: ${status.value.blockHash}` ); }
if (status.type === 'Finalized') { console.log( `Transaction finalized in block: ${status.value.blockHash}` );
// Check for dispatch errors if (dispatchError) { if (dispatchError.type === 'Module') { const decoded = client.registry.findMetaError( dispatchError.value ); console.error( `Dispatch error: ${decoded.section}.${decoded.name}: ${decoded.docs}` ); reject( new Error( `Transaction failed: ${decoded.section}.${decoded.name}` ) ); } else { console.error(`Dispatch error: ${dispatchError.type}`); reject(new Error(`Transaction failed: ${dispatchError.type}`)); } } else { console.log('Transaction successful!'); resolve(); } } }) .catch(reject); });
// Disconnect the client after transaction completes await client.disconnect(); console.log('Disconnected from Polkadot Hub');}
main().catch(console.error);Run the script:
npx tsx send-transfer.tsYou should see output similar to:
Prerequisites
- Python 3.8 or higher
- pip package manager
Environment Setup
-
Create a new project directory and set up a virtual environment:
Terminal window mkdir psi-send-tx-example && cd psi-send-tx-example && \python3 -m venv venv && source venv/bin/activate -
Install the substrate-interface package:
Terminal window pip install substrate-interface
Send Balance Transfer
The following example constructs, signs, and submits a balance transfer transaction.
Create a file named send_transfer.py and add the following code to it:
from substrateinterface import SubstrateInterface, Keypair
POLKADOT_TESTNET_RPC = "INSERT_WS_ENDPOINT"SENDER_MNEMONIC = "INSERT_MNEMONIC"DEST_ADDRESS = "INSERT_DEST_ADDRESS"AMOUNT = 1_000_000_000 # 1 PAS (adjust decimals as needed)
def main(): # Connect to Polkadot Hub substrate = SubstrateInterface(url=POLKADOT_TESTNET_RPC)
print("Connected to Polkadot Testnet")
# Create keypair from mnemonic keypair = Keypair.create_from_mnemonic(SENDER_MNEMONIC) sender_address = keypair.ss58_address
print(f"Sender address: {sender_address}") print(f"Recipient address: {DEST_ADDRESS}") print(f"Amount: {AMOUNT} ({AMOUNT / 1_000_000_000} PAS)\n")
# Get sender's account info to check balance account_info = substrate.query( module="System", storage_function="Account", params=[sender_address] ) print(f"Sender balance: {account_info.value['data']['free']}")
# Compose the transfer call call = substrate.compose_call( call_module="Balances", call_function="transfer_keep_alive", call_params={"dest": DEST_ADDRESS, "value": AMOUNT}, )
# Create a signed extrinsic print("\nSigning and submitting transaction...") extrinsic = substrate.create_signed_extrinsic(call=call, keypair=keypair)
# Submit and wait for inclusion receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True)
if receipt.is_success: print("\nTransaction successful!") print(f"Extrinsic Hash: {receipt.extrinsic_hash}") print(f"Block Hash: {receipt.block_hash}") else: print(f"\nTransaction failed: {receipt.error_message}")
# Close connection substrate.close()
if __name__ == "__main__": main()Run the script:
python send_transfer.pyYou should see output similar to:
Subxt is a Rust library that provides compile-time type safety through code generation from chain metadata.
Prerequisites
- Rust toolchain (latest stable)
- Cargo package manager
Environment Setup
-
Create a new Rust project:
Terminal window cargo new subxt-send-tx-example && cd subxt-send-tx-example -
Install the Subxt CLI:
Terminal window cargo install subxt-cli@0.50.1 -
Download the Polkadot Hub metadata:
Terminal window subxt metadata --url INSERT_WS_ENDPOINT -o asset_hub_metadata.scale -
Update
Cargo.tomlwith the required dependencies:Cargo.toml [package]name = "subxt-send-tx-example"version = "0.1.0"edition = "2021"[[bin]]name = "send_transfer"path = "src/bin/send_transfer.rs"[dependencies]subxt = { version = "0.50.0", features = ["native"] }subxt-signer = "0.50.0"tokio = { version = "1", features = ["rt", "macros"] }
Send Balance Transfer
The following example constructs, signs, and submits a balance transfer transaction.
Create a file at src/bin/send_transfer.rs and add the following code to it:
use std::str::FromStr;use subxt::utils::AccountId32;use subxt::{OnlineClient, PolkadotConfig};use subxt_signer::{bip39::Mnemonic, sr25519::Keypair};
// Generate an interface from the node's metadata#[subxt::subxt(runtime_metadata_path = "asset_hub_metadata.scale")]pub mod asset_hub {}
const ASSET_HUB_RPC: &str = "INSERT_WS_ENDPOINT";const SENDER_MNEMONIC: &str = "INSERT_SENDER_MNEMONIC";const DEST_ADDRESS: &str = "INSERT_DEST_ADDRESS";const AMOUNT: u128 = 1_000_000_000_000; // 1 DOT (12 decimals)
#[tokio::main(flavor = "current_thread")]async fn main() -> Result<(), Box<dyn std::error::Error>> { // Initialize the Subxt client let api = OnlineClient::<PolkadotConfig>::from_url(ASSET_HUB_RPC).await?;
// Anchor to the current block let at_block = api.at_current_block().await?;
println!("Connected to Polkadot Hub");
// Load the sender's keypair from a mnemonic phrase let mnemonic = Mnemonic::parse(SENDER_MNEMONIC)?; let sender_keypair = Keypair::from_phrase(&mnemonic, None)?; let sender_address = AccountId32::from(sender_keypair.public_key());
println!("Sender address: {}", sender_address); println!("Recipient address: {}", DEST_ADDRESS); println!("Amount: {} ({} DOT)\n", AMOUNT, AMOUNT / 1_000_000_000_000);
// Get sender's account info to check balance let account_storage = at_block .storage() .entry(asset_hub::storage().system().account())?; let account_info = account_storage .fetch((sender_address,)) .await? .decode()?;
println!("Sender balance: {}", account_info.data.free);
// Convert the recipient address into an AccountId32 let dest = AccountId32::from_str(DEST_ADDRESS)?;
// Build the balance transfer extrinsic let balance_transfer_tx = asset_hub::transactions() .balances() .transfer_keep_alive(dest.into(), AMOUNT);
// Sign and submit the extrinsic, then wait for it to be finalized println!("\nSigning and submitting transaction..."); let events = at_block .transactions() .sign_and_submit_then_watch_default(&balance_transfer_tx, &sender_keypair) .await? .wait_for_finalized_success() .await?;
// Check for a successful transfer event if let Some(event) = events.find_first::<asset_hub::balances::events::Transfer>() { println!("\nTransaction successful!"); println!("Transfer event: {:?}", event); }
Ok(())}Run the script:
cargo run --bin send_transferYou should see output similar to:
Where to Go Next
Section titled “Where to Go Next”-
Query On-Chain State
Learn how to query storage and runtime data with the SDKs used in this guide.
-
Calculate Transaction Fees
Estimate fees before sending transactions