@parity/product-sdk-tx
Submit Polkadot transactions and follow them to finality.
submitAndWatch signs, broadcasts, and tracks a single extrinsic through its
lifecycle; batchSubmitAndWatch does the same for a list of calls. The package
also bundles the things you almost always reach for next to a real submission:
dry-run helpers, Asset Hub account mapping, retry primitives, dev signers, and
a typed error hierarchy with formatters that turn dispatch errors into readable
messages.
npm install @parity/product-sdk-txExports
Classes
| Name | Summary |
|---|---|
TxAccountMappingError | Error thrown when account mapping fails. |
TxBatchError | Error specific to batch transaction construction (e.g., empty calls array). |
TxDispatchError | The transaction was included on-chain but the dispatch failed. |
TxDryRunError | A dry-run simulation failed before the transaction was submitted on-chain. |
TxError | Base class for all transaction errors. Use instanceof TxError to catch any tx-related error. |
TxSigningRejectedError | The user rejected the signing request in their wallet. |
TxTimeoutError | The transaction did not finalize within the configured timeout. It may still be processing on-chain. |
Functions
| Name | Summary |
|---|---|
applyWeightBuffer() | Apply a safety buffer to weight estimates from a dry-run result. |
batchSubmitAndWatch() | Batch multiple transactions into a single Substrate Utility batch and submit. |
calculateDelay() | Calculate delay with exponential backoff and jitter. |
createDevSigner() | Create a PolkadotSigner for a standard Substrate dev account. |
ensureAccountMapped() | Ensure an account’s SS58 address is mapped to its H160 EVM address on-chain. |
extractTransaction() | Validate an Ink SDK dry-run result and extract the submittable transaction. |
formatDispatchError() | Extract a human-readable error from a transaction result’s dispatch error. |
formatDryRunError() | Extract a human-readable error from a failed dry-run result. |
getDevPublicKey() | Get the public key bytes for a dev account. |
isAccountMapped() | Check if an address is mapped on-chain. |
isSigningRejection() | Check if an error looks like a user-rejected signing request. |
submitAndWatch() | Submit a transaction and watch its lifecycle through signing, broadcasting, |
withRetry() | Wrap an async function with retry logic and exponential backoff. |
Interfaces
| Name | Summary |
|---|---|
BatchApi | Minimal structural type for a PAPI typed API with the Utility pallet. |
BatchSubmitOptions | Options for batchSubmitAndWatch. Extends SubmitOptions with batch mode. |
EnsureAccountMappedOptions | Options for ensureAccountMapped. |
MappingChecker | Minimal interface for checking if an address is mapped on-chain. |
RetryOptions | Options for withRetry. |
ReviveApi | Minimal typed API shape for Revive.map_account(). |
SubmitOptions | Options for submitAndWatch. |
SubmittableTransaction | Structural type for any transaction object that supports Observable-based |
TxResult | Successful transaction result. |
Weight | Substrate weight representing computational and storage resources. |
Type Aliases
| Name | Summary |
|---|---|
BatchableCall | A transaction or decoded call that can be included in a batch. |
BatchMode | Batch execution mode corresponding to Substrate’s Utility pallet. |
DevAccountName | Standard Substrate dev account names. |
TxEvent | PAPI transaction event (discriminated union). |
TxStatus | Transaction lifecycle status for UI callbacks. |
WaitFor | When to resolve the submission promise. |
Classes
class TxAccountMappingError
Error thrown when account mapping fails.
Extends: Error
Constructors
constructor
new TxAccountMappingError(message: string, options?: ErrorOptions): TxAccountMappingErrorclass TxBatchError
Error specific to batch transaction construction (e.g., empty calls array).
Extends: TxError
Constructors
constructor
new TxBatchError(message: string): TxBatchErrorclass TxDispatchError
The transaction was included on-chain but the dispatch failed.
Extends: TxError
Constructors
constructor
new TxDispatchError(dispatchError: unknown, formatted: string): TxDispatchErrorProperties
dispatchError
unknownRaw dispatch error from polkadot-api.
formatted
stringHuman-readable error string (e.g., “Revive.ContractReverted”).
class TxDryRunError
A dry-run simulation failed before the transaction was submitted on-chain.
Thrown by extractTransaction when the dry-run result indicates failure.
Carries structured error information so callers can distinguish revert reasons
from dispatch errors programmatically.
Extends: TxError
Examples
try {
const tx = extractTransaction(await contract.query("mint", { origin, data }));
} catch (e) {
if (e instanceof TxDryRunError) {
console.log(e.revertReason); // "InsufficientBalance" (if contract provided one)
console.log(e.formatted); // "Revive.StorageDepositNotEnoughFunds"
}
}Constructors
constructor
new TxDryRunError(raw: unknown, formatted: string, revertReason?: string): TxDryRunErrorProperties
formatted
stringHuman-readable error string derived from the dry-run result.
raw
unknownThe raw dry-run result for programmatic inspection.
revertReason
stringSolidity revert reason, if the contract provided one.
class TxError
Base class for all transaction errors. Use instanceof TxError to catch any tx-related error.
Extends: Error
Constructors
constructor
new TxError(message: string, options?: ErrorOptions): TxErrorclass TxSigningRejectedError
The user rejected the signing request in their wallet.
Extends: TxError
Constructors
constructor
new TxSigningRejectedError(): TxSigningRejectedErrorclass TxTimeoutError
The transaction did not finalize within the configured timeout. It may still be processing on-chain.
Extends: TxError
Constructors
constructor
new TxTimeoutError(timeoutMs: number): TxTimeoutErrorProperties
timeoutMs
numberFunctions
applyWeightBuffer()
Apply a safety buffer to weight estimates from a dry-run result.
Dry-run weight estimates reflect the exact execution cost at the time of
simulation. On-chain conditions can change between dry-run and actual
submission (storage growth, state changes by other transactions), so a
buffer prevents unexpected OutOfGas failures.
The default 25% buffer matches the convention used across Polkadot ecosystem tooling.
applyWeightBuffer(weight: Weight, options?: { percent?: number }): WeightParameters
weight: Theweight_requiredfrom aReviveApi.callorReviveApi.eth_transactdry-run.options: Override the buffer percentage (default: 25%).
Returns
A new weight with both components scaled up.
Examples
const dryRun = await api.apis.ReviveApi.call(origin, dest, value, undefined, undefined, data);
const tx = api.tx.Revive.call({
dest, value, data,
weight_limit: applyWeightBuffer(dryRun.weight_required),
storage_deposit_limit: dryRun.storage_deposit.value,
});applyWeightBuffer(dryRun.weight_required, { percent: 50 });batchSubmitAndWatch()
Batch multiple transactions into a single Substrate Utility batch and submit.
Extracts .decodedCall from each transaction (handling Ink SDK AsyncTransaction
wrappers), wraps them in Utility.batch_all (or batch/force_batch via the
mode option), and submits via submitAndWatch with full lifecycle tracking.
batchSubmitAndWatch(calls: BatchableCall[], api: BatchApi, signer: PolkadotSigner, options?: BatchSubmitOptions): Promise<TxResult>Parameters
calls: Array of transactions, AsyncTransactions, or raw decoded calls to batch.api: A typed API withtx.Utility.batch_all/batch/force_batch. Works with any chain that has the Utility pallet — no chain-specific imports required. All calls must target the same chain as this API. Do not mix decoded calls from different chains (e.g., Asset Hub and Bulletin) in a single batch.signer: The signer to use. Can come from the Host API (getProductAccountSigner) orcreateDevSigner.options: OptionalBatchSubmitOptions(extendsSubmitOptionswithmode).
Returns
The transaction result from the batch submission.
Throws
- If
callsis empty. - If an AsyncTransaction resolves without a
.decodedCallproperty. - If the batch transaction does not reach the target state within
timeoutMs. - If the on-chain dispatch fails.
- If the user rejects signing in their wallet.
Examples
import { batchSubmitAndWatch } from "@parity/product-sdk-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 result = await batchSubmitAndWatch([tx1, tx2], api, signer, {
onStatus: (status) => console.log(status),
});calculateDelay()
Calculate delay with exponential backoff and jitter.
Jitter prevents thundering-herd when multiple clients retry simultaneously.
The delay is min(baseDelay * 2^attempt, maxDelay) * random(0.5, 1.0).
calculateDelay(attempt: number, baseDelayMs: number, maxDelayMs: number): numbercreateDevSigner()
Create a PolkadotSigner for a standard Substrate dev account.
Dev accounts use the well-known Substrate dev mnemonic (DEV_PHRASE) with
Sr25519 key derivation at the path //{Name}. These accounts have known
private keys and are pre-funded on dev/test chains.
Only for local development, scripts, and testing. Never use in production.
createDevSigner(name: DevAccountName): PolkadotSignerParameters
name: Dev account name (“Alice”, “Bob”, “Charlie”, “Dave”, “Eve”, or “Ferdie”).
Returns
A PolkadotSigner that can sign transactions.
Examples
import { createDevSigner } from "@parity/product-sdk-tx";
const alice = createDevSigner("Alice");
const result = await submitAndWatch(tx, alice);ensureAccountMapped()
Ensure an account’s SS58 address is mapped to its H160 EVM address on-chain.
Account mapping is a prerequisite for any EVM contract interaction on Asset Hub.
This function checks the on-chain mapping status and, if unmapped, submits a
Revive.map_account() transaction and waits for inclusion.
Idempotent — safe to call multiple times. Returns immediately if already mapped.
ensureAccountMapped(address: string, signer: PolkadotSigner, checker: MappingChecker, api: ReviveApi, options?: EnsureAccountMappedOptions): Promise<TxResult | null>Parameters
address: The SS58 address to check/map.signer: The signer for the account (must match the address).checker: An object withaddressIsMapped(). Easiest path: build one from a pallet-revive-capable typed API by queryingRevive.OriginalAccount.api: A typed API withtx.Revive.map_account().options: Optional timeout and status callback.
Returns
The transaction result if mapping was performed, or null if already mapped.
Throws
- If the mapping check or transaction fails.
- If the map_account transaction fails on-chain.
- If the mapping transaction times out.
Examples
import { ensureAccountMapped } from "@parity/product-sdk-tx";
import { ss58ToH160 } from "@parity/product-sdk-address";
const api = client.getTypedApi(paseo_asset_hub);
const checker = {
addressIsMapped: async (addr: string) =>
(await api.query.Revive.OriginalAccount.getValue(ss58ToH160(addr))) !== undefined,
};
await ensureAccountMapped(address, signer, checker, api);
// Account is now mapped — safe to call PolkaVM/Solidity contractsextractTransaction()
Validate an Ink SDK dry-run result and extract the submittable transaction.
Replaces the 5-10 line boilerplate that every contract interaction repeats:
check success, parse the error, verify send() exists, and call it.
Works with any object whose shape matches the Ink SDK contract query result (typed structurally — no Ink SDK import required):
contract.query("method", { origin, data })(Ink SDK)contract.write("method", args, origin)(patched SDK wrappers)- Any object with
{ success: boolean; value?: { send?(): ... } }
extractTransaction(result: { error?: unknown; success: boolean; value?: unknown }): SubmittableTransactionParameters
result: The dry-run result from a contract query or write simulation.
Returns
The submittable transaction, ready to pass to submitAndWatch.
Throws
- If the dry run failed or the result has no
send().
Examples
import { extractTransaction, submitAndWatch, createDevSigner } from "@parity/product-sdk-tx";
const dryRun = await contract.query("createItem", { origin, data: { name, price } });
const tx = extractTransaction(dryRun);
const result = await submitAndWatch(tx, createDevSigner("Alice"));const tx = extractTransaction(await contract.query("transfer", { origin, data }));
const result = await withRetry(() => submitAndWatch(tx, signer));formatDispatchError()
Extract a human-readable error from a transaction result’s dispatch error.
PAPI dispatch errors for pallet modules are nested:
{ type: "Module", value: { type: "Revive", value: { type: "ContractReverted" } } }
This walks the chain to build a string like "Revive.ContractReverted".
formatDispatchError(result: { dispatchError?: unknown; ok: boolean }): stringParameters
result: A transaction result withokand optionaldispatchError.
Returns
A human-readable error string, or "" if the result is ok, or "unknown error" if
the dispatch error cannot be decoded.
formatDryRunError()
Extract a human-readable error from a failed dry-run result.
Handles every error shape found across the Polkadot contract ecosystem:
-
Revert reason (Ink SDK patched results / EVM contracts):
{ value: { revertReason: "InsufficientBalance" } } -
Nested dispatch errors (raw Ink SDK / pallet errors):
{ value: { type: "Module", value: { type: "Revive", value: { type: "StorageDepositNotEnoughFunds" } } } }Delegates toformatDispatchErrorfor the Module.Pallet.Error chain. -
ReviveApi runtime messages (
eth_transact/ReviveApi.call):{ value: { type: "Message", value: "Insufficient balance for gas * price + value" } } -
ReviveApi contract revert data:
{ value: { type: "Data", value: "0x08c379a0..." } } -
Wrapped raw errors (patched SDK wrappers):
{ value: { raw: { type: "Message", value: "..." } } } -
Generic error field:
{ error: { type: "ContractTrapped" } }or{ error: { name: "..." } }
formatDryRunError(result: { error?: unknown; success?: boolean; value?: unknown }): stringParameters
result: A dry-run result with at leastsuccess, and optionallyvalue/error.
Returns
A human-readable error string, or "" if the result succeeded.
getDevPublicKey()
Get the public key bytes for a dev account.
Useful for address derivation or identity checks in tests without needing the full signer.
getDevPublicKey(name: DevAccountName): Uint8ArrayParameters
name: Dev account name.
Returns
32-byte Sr25519 public key.
isAccountMapped()
Check if an address is mapped on-chain.
Convenience wrapper around checker.addressIsMapped() with error handling.
isAccountMapped(address: string, checker: MappingChecker): Promise<boolean>isSigningRejection()
Check if an error looks like a user-rejected signing request.
Different wallets use different error messages when the user rejects signing: “Cancelled”, “Rejected”, “User rejected”, “denied”. This checks for common patterns as a best-effort heuristic. Non-Error values always return false.
isSigningRejection(error: unknown): booleanParameters
error: The error to check.
Returns
true if the error message matches a known rejection pattern.
submitAndWatch()
Submit a transaction and watch its lifecycle through signing, broadcasting, block inclusion, and (optionally) finalization.
submitAndWatch(tx: SubmittableTransaction, signer: PolkadotSigner, options?: SubmitOptions): Promise<TxResult>Parameters
tx: A transaction object withsignSubmitAndWatch. Works with raw PAPI transactions and Ink SDKAsyncTransactionwrappers (resolved automatically).signer: The signer to use. Can come from the Host API (getProductAccountSigner) orcreateDevSigner.options: Submission options (waitFor, timeout, mortality, status callback).
Returns
The transaction result once included/finalized.
Throws
- If the transaction does not reach the target state within
timeoutMs. - If the on-chain dispatch fails (e.g., insufficient balance, contract revert).
- If the user rejects signing in their wallet.
withRetry()
Wrap an async function with retry logic and exponential backoff.
Only retries transient errors (network disconnects, temporary RPC failures).
Deterministic errors (TxDispatchError, TxBatchError), user
rejections (TxSigningRejectedError), and timeouts (TxTimeoutError)
are rethrown immediately without retry.
withRetry(fn: () => Promise<T>, options?: RetryOptions): Promise<T>Parameters
fn: The async function to retry.options: Retry configuration.
Returns
The result of the first successful call.
Examples
const result = await withRetry(
() => submitAndWatch(tx, signer),
{ maxAttempts: 3, baseDelayMs: 1_000 },
);Interfaces
interface BatchApi
Minimal structural type for a PAPI typed API with the Utility pallet.
Structural so it works with any chain that has the Utility pallet, without importing chain-specific descriptors.
Properties
tx
{ Utility: { batch: unknown; batch_all: unknown; force_batch: unknown } }interface BatchSubmitOptions
Options for batchSubmitAndWatch. Extends SubmitOptions with batch mode.
Extends: SubmitOptions
Properties
mode
BatchModeBatch execution mode. Default: "batch_all" (atomic, all-or-nothing).
"batch_all"— 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"— Likebatchbut continues executing remaining calls after failures (never aborts early).
interface EnsureAccountMappedOptions
Options for ensureAccountMapped.
Properties
onStatus
(status: "checking" | "mapping" | "mapped" | "already-mapped") => voidCalled on mapping transaction status changes.
timeoutMs
numberTimeout in ms for the map_account transaction. Default: 60_000 (1 minute).
interface MappingChecker
Minimal interface for checking if an address is mapped on-chain.
Accepted structurally so this module stays runtime-agnostic. Build one
directly from the typed API of any pallet-revive-capable chain — see the
example on ensureAccountMapped.
Methods
addressIsMapped
addressIsMapped(address: string): Promise<boolean>interface RetryOptions
Options for withRetry.
Properties
baseDelayMs
numberBase delay in ms for exponential backoff. Default: 1_000.
maxAttempts
numberTotal attempts including the first. Default: 3.
maxDelayMs
numberMaximum delay in ms. Default: 15_000.
interface ReviveApi
Minimal typed API shape for Revive.map_account().
Accepted structurally so this module works with any PAPI typed API that has the Revive pallet, without importing chain-specific descriptors.
Properties
tx
{ Revive: { map_account: unknown } }interface SubmitOptions
Options for submitAndWatch.
Properties
mortalityPeriod
numberMortality period in blocks. Default: 256 (~43 minutes on Polkadot).
onStatus
(status: TxStatus) => voidCalled on each lifecycle transition for UI progress indicators.
timeoutMs
numberTimeout in milliseconds. Default: 300_000 (5 minutes).
waitFor
WaitForWhen to resolve the promise. Default: "best-block".
interface SubmittableTransaction
Structural type for any transaction object that supports Observable-based sign-submit-and-watch. Works with raw PAPI transactions and Ink SDK resolved transactions.
Properties
decodedCall
unknownThe decoded call data. Present on PAPI transactions.
signSubmitAndWatch
(signer: PolkadotSigner, options?: { mortality?: { mortal: boolean; period: number } }) => { subscribe: (handlers: { error: (error: Error) => void; next: (event: TxEvent) => void }) => { unsubscribe: () => void } }waited
Promise<SubmittableTransaction>Present on Ink SDK AsyncTransaction wrappers.
interface TxResult
Successful transaction result.
Properties
block
{ hash: string; index: number; number: number }Block where the transaction was included.
dispatchError
unknownDispatch error details when ok is false.
events
unknown[]Raw events emitted by the transaction.
ok
booleanWhether the on-chain dispatch succeeded.
txHash
stringTransaction hash.
interface Weight
Substrate weight representing computational and storage resources.
Matches the shape returned by ReviveApi.call and ReviveApi.eth_transact
dry-run results in the weight_required field.
Properties
proof_size
bigintProof size component in bytes.
ref_time
bigintReference time component in picoseconds.
Type Aliases
type BatchableCall
A transaction or decoded call that can be included in a batch.
Accepts:
- A
SubmittableTransaction(has.decodedCall) - An Ink SDK AsyncTransaction (has
.waitedthat resolves to one with.decodedCall) - A raw decoded call object (passed through as
Record<string, unknown>)
The Record<string, unknown> variant is intentionally broad because PAPI decoded
calls are chain-specific enum types that cannot be imported without chain descriptors.
Runtime validation in resolveDecodedCall rejects null, undefined, and primitives.
type BatchableCall = SubmittableTransaction | { decodedCall: unknown } | Record<string, unknown>type BatchMode
Batch execution mode corresponding to Substrate’s Utility pallet.
type BatchMode = "batch_all" | "batch" | "force_batch"type DevAccountName
Standard Substrate dev account names.
type DevAccountName = "Alice" | "Bob" | "Charlie" | "Dave" | "Eve" | "Ferdie"type TxEvent
PAPI transaction event (discriminated union).
type TxEvent = { txHash: string; type: "signed" } | { txHash: string; type: "broadcasted" } | { block?: { hash: string; index: number; number: number }; dispatchError?: unknown; events?: unknown[]; found: boolean; ok?: boolean; txHash: string; type: "txBestBlocksState" } | { block: { hash: string; index: number; number: number }; dispatchError?: unknown; events: unknown[]; ok: boolean; txHash: string; type: "finalized" }type TxStatus
Transaction lifecycle status for UI callbacks.
type TxStatus = "signing" | "broadcasting" | "in-block" | "finalized" | "error"type WaitFor
When to resolve the submission promise.
type WaitFor = "best-block" | "finalized"