Sign and Submit Transactions
Introduction
Section titled “Introduction”This guide covers the @parity/product-sdk-signer package, which gives your Product a typed, host-aware signing interface for deriving accounts and signing payloads (raw bytes or full transactions) from inside a Polkadot host container. The SDK exposes a single orchestrator, SignerManager, that wraps both the production host path (Polkadot Desktop) and a deterministic dev path behind the same Result-typed API.
Prerequisites
Section titled “Prerequisites”Before starting, ensure you have:
- A Polkadot Product project running locally (see Set Up Your Project) with a TypeScript toolchain
- Node.js 20 or later with ESM support (
@parity/product-sdk-signeris ESM only) - Polkadot Desktop to run your Product inside a host container (see Install Desktop and Pair)
Install the SDK
Section titled “Install the SDK”The SDK installs two ways. The umbrella package, @parity/product-sdk, is the recommended starting point: one dependency that re-exports most of the SDK. Individual packages install only what you use, which keeps your bundle smaller and your dependencies explicit, so switch to them later as a bundle-size optimization. The two use different import specifiers, so switching styles means updating your imports. See Umbrella or Individual Packages for the full comparison.
-
Umbrella package: The whole SDK in one dependency.
Terminal window npm install @parity/product-sdk -
Individual package: Only the signer.
Terminal window npm install @parity/product-sdk-signer
The import specifiers differ between the two. The snippets in this guide use the standalone specifier @parity/product-sdk-signer; on the umbrella, the same exports come from the @parity/product-sdk/wallet subpath, which keeps the older name. SignerManager is also re-exported from the umbrella root.
How Product Accounts Work
Section titled “How Product Accounts Work”Every product-scoped account is identified by a ProductAccountId tuple:
dotNsIdentifier: The dotNS identifier of the Product requesting the account, for example the.dotname your Product is loaded under.derivationIndex: A non-negative integer chosen by the Product. Index0is the conventional default; higher indices give you additional sub-accounts within the same Product.
The same (dotNsIdentifier, derivationIndex) pair always yields the same public key, so your Product can reproduce the same address across sessions without persisting it.
Set Up Signer Manager
Section titled “Set Up Signer Manager”Construct SignerManager once and hold it for the lifetime of your Product. Always set ss58Prefix and dappName explicitly. Use the onConnect callback to request any resources your Product needs immediately after connection; the callback fires exactly once per connect and re-fires automatically after any auto-reconnect.
import { SignerManager } from '@parity/product-sdk-signer';
const manager = new SignerManager({ ss58Prefix: 0, dappName: 'my-product', onConnect: async (_account, { requestResourceAllocation, signal }) => { try { const outcomes = await requestResourceAllocation([ { tag: 'BulletinAllowance', value: undefined }, ]); if (signal.aborted) return; if (outcomes.some((outcome) => outcome !== 'Allocated')) { // Degrade gracefully: the capability is unavailable, not fatal. } } catch (cause) { // Typed host error; the connection itself is unaffected. } },});ss58Prefix: The SS58 address-format prefix for the target network. Use0for the Polkadot relay chain.dappName: A human-readable name for your Product, shown in Desktop UI.onConnect: Fires once per connection transition. Request the resources your Product needs here, before any signing call is made.ctx.requestResourceAllocation: The one call in this package that throws instead of returning aResult, so wrap it intry/catch. Each outcome is'Allocated','Rejected', or'NotAvailable'; treat anything other than allocated as the capability being unavailable.ctx.signal: Aborts if the user disconnects while the request is in flight. Check it before acting on the outcomes.
Connect to the Host
Section titled “Connect to the Host”Call connect() to establish a session with the host and discover available accounts. connect(), selectAccount(), getProductAccount(), and signRaw() all return a Result; always check .ok before accessing .value. (getSigner() returns a nullable directly, not a Result.)
Each step below is a self-contained function over the manager you built above, so you can paste them one at a time.
async function connectAndSelect() { const connectResult = await manager.connect(); if (!connectResult.ok) { // HostUnavailableError when running outside Desktop console.error(connectResult.error.message); return null; }
const accounts = connectResult.value; if (accounts.length === 0) { // Connected, but the host derived no accounts for this Product. return null; }
// Auto-select the first account if none is already selected if (!manager.getState().selectedAccount) { const selectResult = manager.selectAccount(accounts[0].address); if (!selectResult.ok) { console.error(selectResult.error.message); return null; } }
return manager.getState().selectedAccount;}connect() defaults to the host provider. Outside a host container it returns HostUnavailableError; use connect('dev') for local testing instead. See Test Without a Host.
Get a Product Account
Section titled “Get a Product Account”getProductAccount requests the product-scoped account for a given dotNsIdentifier from the host. This is a host-only API; it returns HostUnavailableError when the active provider is 'dev'.
async function loadProductAccount() { const accountResult = await manager.getProductAccount('my-product.dot', 0); if (!accountResult.ok) { console.error(accountResult.error.message); return null; }
return accountResult.value; // SignerAccount}The returned SignerAccount exposes:
address: The SS58-encoded address for the current network.publicKey: The raw 32-byte public key.name: An optional display name, ornull.
Show a Display Name
Section titled “Show a Display Name”There is no built-in primitive for an in-app username, so most Products need to choose a display identity themselves. Usernames in Your Product covers the pattern to follow; this section covers the call it depends on.
When the user holds a personhood tier, the platform already associates an earned name with their account. Read it with getUserId:
async function loadDisplayName() { const idResult = await manager.getUserId(); if (!idResult.ok) { return null; // No host, no permission, or no personhood tier }
return idResult.value.primaryUsername; // e.g. 'joseph.42'}When there is no earned name to read, fall back to a per-Product display name the user sets, stored in local storage (device-local) or cloud storage (shared) and keyed to their per-app account. Either way, treat the result as a label rather than as identity: authorization and uniqueness come from the per-app account and Proof of Personhood.
Sign Arbitrary Bytes
Section titled “Sign Arbitrary Bytes”Use signRaw to sign an arbitrary byte payload with the currently selected account. This is useful for off-chain authentication, message proofs, and any use case that does not require a full transaction.
signRaw returns a Result<Uint8Array, SignerError>; it never throws. Always check .ok before accessing .value.
async function signMessage() { const account = await loadProductAccount(); if (!account) return null;
const selectResult = manager.selectAccount(account.address); if (!selectResult.ok) { console.error(selectResult.error.message); return null; }
const payload = new TextEncoder().encode('hello polkadot'); const result = await manager.signRaw(payload);
if (!result.ok) { console.error(result.error.message); return null; }
return result.value; // Uint8Array - the raw signature}Sign and Submit a Transaction
Section titled “Sign and Submit a Transaction”To sign a transaction, get a PolkadotSigner from SignerManager and pass it to the signAndSubmit method from polkadot-api (PAPI). The SDK handles routing the signing request to Desktop, which renders a signing modal and forwards to the Polkadot App.
@parity/product-sdk-signer provides only the signer interface; chain connectivity uses polkadot-api directly. Set up your PAPI client separately:
import { createClient } from 'polkadot-api';import { getWsProvider } from 'polkadot-api/ws-provider/web';import { dot } from '@polkadot-api/descriptors';
const client = createClient(getWsProvider('wss://rpc.polkadot.io'));const api = client.getTypedApi(dot);The dot descriptor is the typed API surface for the Polkadot relay chain. Run npx papi add dot in your project to pull or regenerate it.
async function transfer() { const recipient = 'INSERT_RECIPIENT_ADDRESS';
const account = await loadProductAccount(); if (!account) return null;
const selectResult = manager.selectAccount(account.address); if (!selectResult.ok) return null;
// getSigner() returns null if no account is selected const signer = manager.getSigner(); if (!signer) return null;
const tx = api.tx.Balances.transfer_keep_alive({ dest: recipient, value: 1_000_000_000_000n, });
return tx.signAndSubmit(signer);}Handle Signing Errors
Section titled “Handle Signing Errors”Two outcomes are worth handling explicitly: the user rejects the request, or the signing session times out.
import { HostRejectedError, TimeoutError,} from '@parity/product-sdk-signer';import type { PolkadotSigner } from 'polkadot-api';
async function submitOrReport( tx: { signAndSubmit(signer: PolkadotSigner): Promise<unknown> }, signer: PolkadotSigner,) { try { return await tx.signAndSubmit(signer); } catch (error) { if (error instanceof HostRejectedError) { // The user dismissed the signing prompt on the Polkadot App return null; } if (error instanceof TimeoutError) { // The signing session expired before the user responded return null; } throw error; }}HostRejectedError: The user explicitly denied the request. Return the user to a stable state and let them retry.TimeoutError: The signing session expired. Treat this the same as a rejection.
Test Without a Host
Section titled “Test Without a Host”During development, use connect('dev') to load the standard Substrate dev accounts (Alice, Bob, Charlie, Dave, Eve, and Ferdie) without needing Polkadot Desktop. The signing API is identical — only the provider changes.
async function devSignRaw() { const result = await manager.connect('dev'); if (!result.ok) return null;
const [alice] = result.value; if (!alice) return null;
const selectResult = manager.selectAccount(alice.address); if (!selectResult.ok) return null;
const signResult = await manager.signRaw(new TextEncoder().encode('test')); if (!signResult.ok) return null;
return signResult.value; // Uint8Array - the raw signature}Limitations
Section titled “Limitations”- The package is ESM only; your Product's build pipeline must support ESM imports.
SignerManager.destroy()is terminal. After calling it, all subsequent method calls returnDestroyedError. Usedisconnect()for a reversible reset.- Account persistence across page reloads requires the host to expose
localStorage. Outside a host container, persistence silently no-ops.
Where to Go Next
Section titled “Where to Go Next”-
Store Data on Chain
Your Product can sign; next, use that signer to store content on the Bulletin Chain and fetch it from anywhere by CID.
-
PAPI Docs
Reference for the typed PAPI signer interface that
@parity/product-sdk-signerexposes. -
Product SDK API Reference
The full
product-sdksurface beyond this recipe: every package, class, and method.