Transaction submission, lifecycle watching, and dev signers for Polkadot chains.
pnpm add @polkadot-apps/tx
Peer dependency: polkadot-api must be installed in your project.
pnpm add polkadot-api
import { submitAndWatch, createDevSigner } from "@polkadot-apps/tx";
const signer = createDevSigner("Alice");
const result = await submitAndWatch(tx, signer, {
waitFor: "finalized",
onStatus: (status) => console.log(status),
});
console.log(result.txHash, result.ok);
Submit multiple transactions as a single atomic batch. Uses Substrate's Utility.batch_all by default (all-or-nothing).
import { batchSubmitAndWatch } from "@polkadot-apps/tx";
const tx1 = api.tx.Balances.transfer_keep_alive({ dest: addr1, value: 1_000n });
const tx2 = api.tx.Balances.transfer_keep_alive({ dest: addr2, value: 2_000n });
const tx3 = api.tx.System.remark({ remark: Binary.fromText("hello") });
const result = await batchSubmitAndWatch([tx1, tx2, tx3], api, signer, {
onStatus: (status) => console.log(status),
});
Three batch modes are available:
| Mode | Behavior |
|---|---|
"batch_all" (default) |
Atomic. Reverts all calls if any single call fails. |
"batch" |
Best-effort. Stops at first failure but earlier successful calls are not reverted. |
"force_batch" |
Like batch but continues after failures (never aborts early). |
// Non-atomic: some calls may fail while others succeed
const result = await batchSubmitAndWatch(calls, api, signer, { mode: "batch" });
WARNING: In
"batch"and"force_batch"modes,result.okistrueeven when individual calls fail. Inspectresult.eventsforUtility.ItemFailedevents to detect individual failures. Only"batch_all"guarantees thatresult.ok === falsewhen any call fails.
Calls can be PAPI transactions (.decodedCall extracted automatically), Ink SDK AsyncTransaction wrappers (.waited resolved automatically), or raw decoded calls.
// Mix of raw decoded calls and transactions
const calls = [
api.tx.Balances.transfer_keep_alive({ dest, value: 1_000n }), // PAPI tx
extractTransaction(await contract.query("mint", { origin, data })), // Ink SDK
someDecodedCallObject, // raw
];
const result = await batchSubmitAndWatch(calls, api, signer);
submitAndWatch drives a transaction through its full lifecycle: signing, broadcasting, block inclusion, and optional finalization. You choose when to resolve the returned promise with the waitFor option.
import { submitAndWatch } from "@polkadot-apps/tx";
const result = await submitAndWatch(tx, signer, {
waitFor: "best-block", // resolve at best-block inclusion (default)
timeoutMs: 300_000, // 5-minute timeout (default)
mortalityPeriod: 256, // ~43 minutes on Polkadot (default)
onStatus: (status) => {
// "signing" -> "broadcasting" -> "in-block" -> "finalized"
updateUI(status);
},
});
if (!result.ok) {
console.error("Dispatch failed:", result.dispatchError);
}
The function accepts both raw PAPI transactions and Ink SDK AsyncTransaction wrappers. Ink SDK wrappers are resolved automatically via the .waited promise.
Create signers from the well-known Substrate dev mnemonic for local testing. All keys derive at //Name using sr25519.
import { createDevSigner, getDevPublicKey } from "@polkadot-apps/tx";
const alice = createDevSigner("Alice");
const bobPubKey = getDevPublicKey("Bob"); // Uint8Array (32 bytes)
Available names: "Alice", "Bob", "Charlie", "Dave", "Eve", "Ferdie".
Extract a submittable transaction from an Ink SDK dry-run result and apply a safety buffer to weight estimates before submission.
import { extractTransaction, applyWeightBuffer } from "@polkadot-apps/tx";
const dryRunResult = await contract.query.myMethod(args);
const tx = extractTransaction(dryRunResult);
const buffered = applyWeightBuffer(dryRunResult.weight_required, {
percent: 25, // default: 25%
});
Map an SS58 address to an H160 address on Asset Hub. The operation is idempotent -- it returns null when the account is already mapped.
import { ensureAccountMapped, isAccountMapped } from "@polkadot-apps/tx";
const mapped = await isAccountMapped(address, checker);
if (!mapped) {
const result = await ensureAccountMapped(address, signer, checker, api);
// result is TxResult or null (if already mapped)
}
withRetry wraps any async function with exponential backoff and jitter. It does not retry TxDispatchError, TxBatchError, TxSigningRejectedError, or TxTimeoutError -- these represent terminal conditions that retrying cannot fix.
import { withRetry, calculateDelay } from "@polkadot-apps/tx";
const result = await withRetry(() => submitAndWatch(tx, signer), {
maxAttempts: 3, // total attempts including the first (default)
baseDelayMs: 1_000, // initial backoff (default)
maxDelayMs: 15_000, // backoff cap (default)
});
// Calculate delay directly for custom retry strategies
const delay = calculateDelay(2, 1_000, 15_000);
All errors extend a common TxError base class. Use the specific error types and utility functions to handle failures precisely.
import {
TxTimeoutError,
TxDispatchError,
TxSigningRejectedError,
TxDryRunError,
TxAccountMappingError,
formatDispatchError,
formatDryRunError,
isSigningRejection,
} from "@polkadot-apps/tx";
try {
await submitAndWatch(tx, signer);
} catch (error) {
if (isSigningRejection(error)) {
console.log("User cancelled signing");
} else if (error instanceof TxDispatchError) {
console.error(error.formatted, error.dispatchError);
} else if (error instanceof TxTimeoutError) {
console.error(`Timed out after ${error.timeoutMs}ms`);
} else if (error instanceof TxDryRunError) {
console.error(error.formatted, error.revertReason);
}
}
submitAndWatch(tx, signer, options?): Promise<TxResult>Submit a transaction and watch its lifecycle through to inclusion or finalization.
| Parameter | Type | Description |
|---|---|---|
tx |
SubmittableTransaction |
Transaction with signSubmitAndWatch. Raw PAPI or Ink SDK. |
signer |
PolkadotSigner |
Signer from a wallet, Host API, or createDevSigner. |
options |
SubmitOptions |
Optional. See below. |
Throws: TxTimeoutError, TxDispatchError, TxSigningRejectedError.
batchSubmitAndWatch(calls, api, signer, options?): Promise<TxResult>Batch multiple transactions into a single Substrate Utility batch and submit with lifecycle tracking.
| Parameter | Type | Description |
|---|---|---|
calls |
BatchableCall[] |
Transactions, AsyncTransactions, or raw decoded calls. |
api |
BatchApi |
Typed API with tx.Utility.batch_all/batch/force_batch. |
signer |
PolkadotSigner |
Signer from a wallet, Host API, or createDevSigner. |
options |
BatchSubmitOptions |
Optional. Extends SubmitOptions with mode. |
Throws: TxBatchError (empty calls), TxTimeoutError, TxDispatchError, TxSigningRejectedError.
createDevSigner(name): PolkadotSignerCreate a signer from the well-known dev mnemonic at //Name (sr25519).
| Parameter | Type | Description |
|---|---|---|
name |
DevAccountName |
One of "Alice", "Bob", "Charlie", "Dave", "Eve", "Ferdie". |
getDevPublicKey(name): Uint8ArrayReturn the 32-byte public key for a dev account.
withRetry<T>(fn, options?): Promise<T>Retry an async function with exponential backoff and jitter. Does not retry TxBatchError, TxDispatchError, TxSigningRejectedError, or TxTimeoutError.
| Parameter | Type | Description |
|---|---|---|
fn |
() => Promise<T> |
Async function to retry. |
options |
RetryOptions |
Optional retry configuration. |
calculateDelay(attempt, baseDelayMs, maxDelayMs): numberCompute the backoff delay for a given attempt number, with jitter.
extractTransaction(result): SubmittableTransactionExtract a submittable transaction from an Ink SDK dry-run result.
applyWeightBuffer(weight, options?): WeightApply a percentage safety buffer to a weight estimate. Default buffer is 25%.
ensureAccountMapped(address, signer, checker, api, options?): Promise<TxResult | null>Map an SS58 address to H160 on Asset Hub. Returns null if the account is already mapped.
isAccountMapped(address, checker): Promise<boolean>Check whether an SS58 address is already mapped to an H160 address.
| Function | Signature | Description |
|---|---|---|
formatDispatchError |
(result) => string |
Format a dispatch error into a readable string. |
formatDryRunError |
(result) => string |
Format a dry-run error into a readable string. |
isSigningRejection |
(error) => boolean |
Check if an error is a signing rejection. |
type TxStatus = "signing" | "broadcasting" | "in-block" | "finalized" | "error";
type WaitFor = "best-block" | "finalized";
interface TxResult {
txHash: string;
ok: boolean;
block: { hash: string; number: number; index: number };
events: unknown[];
dispatchError?: unknown;
}
interface SubmitOptions {
waitFor?: WaitFor; // default: "best-block"
timeoutMs?: number; // default: 300_000
mortalityPeriod?: number; // default: 256
onStatus?: (status: TxStatus) => void;
}
interface RetryOptions {
maxAttempts?: number; // default: 3
baseDelayMs?: number; // default: 1_000
maxDelayMs?: number; // default: 15_000
}
type DevAccountName = "Alice" | "Bob" | "Charlie" | "Dave" | "Eve" | "Ferdie";
interface Weight {
ref_time: bigint;
proof_size: bigint;
}
type BatchMode = "batch_all" | "batch" | "force_batch";
interface BatchSubmitOptions extends SubmitOptions {
mode?: BatchMode; // default: "batch_all"
}
interface BatchApi {
tx: {
Utility: {
batch(args: { calls: unknown[] }): SubmittableTransaction;
batch_all(args: { calls: unknown[] }): SubmittableTransaction;
force_batch(args: { calls: unknown[] }): SubmittableTransaction;
};
};
}
| Class | Extends | Key properties |
|---|---|---|
TxError |
Error |
Base class for all tx errors. |
TxTimeoutError |
TxError |
timeoutMs: number |
TxDispatchError |
TxError |
dispatchError: unknown, formatted: string |
TxSigningRejectedError |
TxError |
User rejected signing. |
TxDryRunError |
TxError |
raw: unknown, formatted: string, revertReason?: string |
TxBatchError |
TxError |
Batch construction failed (e.g., empty calls). |
TxAccountMappingError |
TxError |
Account mapping failed. |
Apache-2.0