Skip to content

Use platform services from the SDK

Use the Product SDK (@parity/product-sdk) from app code to reach the platform services most apps need first: chain access, cloud storage, smart-contract calls, and a user's identity / proof-of-personhood status. Packages & tools has the install line.

Before you start

The SDK is a TypeScript umbrella package with subpath entry points you import from directly: ./chain, ./cloud-storage, ./contracts, ./identity, ./host, ./wallet, ./local-storage, ./crypto, ./address, ./react, and ./testing. ./react (ProductSDKProvider, useChain, useWallet, useLocalStorage, useProductSDK) declares react ^18 || ^19 as a peer dependency — a project without React installed gets a module-not-found error on that subpath only.

The SDK runs inside the host container

Every chain call and every storage call is routed through the host container — the Polkadot app (mobile or desktop) or the web gateway at dev-dot.li. Outside a container, createApp() itself throws while initialising local storage, before you reach any service:

Host storage unavailable. Ensure you are running inside a host container
(Polkadot Browser / Desktop).

A chain call made outside a container throws Host provider unavailable for chain <genesisHash>. To exercise your app in CI, use the test host.

Operations that can fail return a Result instead of throwing. Result<T, E> is { ok: true; value: T } | { ok: false; error: E }, so check res.ok before reading res.value. ok, err and isErrorOf are exported from the package root. Every sample below follows this shape.

Connect to the chains

The chain client gives you typed PAPI access to the three platform chains: Asset Hub, the Bulletin chain, and the Individuality / People chain. There are two ways to connect.

Zero-config preset. Call getChainAPI(env) and let the SDK load the right descriptors for you:

import { getChainAPI } from "@parity/product-sdk/chain";

const client = await getChainAPI("devnet");

const account = await client.assetHub.query.System.Account.getValue(address);
const fee = await client.bulletin.query.TransactionStorage.ByteFee.getValue();
client.individuality; // People-chain access
client.raw; // lower-level PAPI handles, keyed by chain name
client.destroy(); // tear the connections down when you are finished

Environment has four values — polkadot, kusama, paseo and devnet — so all four typecheck. polkadot and kusama throw at runtime because their Bulletin and Individuality chains are not live yet; the throw is per chain, not per environment. Connections are cached by each chain's genesis-hash fingerprint.

Bring your own descriptors. For a specific or pre-release chain, pass descriptors yourself. They live in @parity/product-sdk-descriptors, which is a separate install (npm i @parity/product-sdk-descriptors) — it is not pulled in by @parity/product-sdk:

import { createChainClient } from "@parity/product-sdk/chain";
import { devnet_asset_hub } from "@parity/product-sdk-descriptors/devnet-asset-hub";
import { devnet_bulletin } from "@parity/product-sdk-descriptors/devnet-bulletin";

const client = await createChainClient({
  chains: { assetHub: devnet_asset_hub, bulletin: devnet_bulletin },
});

createChainClient is async — without the await the failure surfaces later, as a Property 'assetHub' does not exist on type 'Promise<ChainClient<…>>' error on the next line you write.

The descriptor chooses the chain

A descriptor passed to createChainClient is the network selection — passing paseo_asset_hub here connects your app to Paseo Next, not this Devnet, whatever --env you deployed with. The failure is quiet: the app builds and loads, then throws ChainNotSupportedError at runtime. Match the descriptor prefix to your target network.

flowchart TD
  A[App: getChainAPI env] --> B{env in paseo/devnet?}
  B -- no --> X[throw: chain not live yet]
  B -- yes --> C[loadDescriptors: assetHub + bulletin + individuality]
  C --> D[createChainClient: cache by genesis fingerprint]
  D --> E[host-routed provider]
  E -- inside container --> G[Polkadot app / desktop / gateway]
  E -- not in container --> Y[throw: Host provider unavailable]
  G --> H[(Asset Hub)]
  G --> I[(Bulletin)]
  G --> J[(Individuality / People)]

Sign transactions

Signing goes through the host wallet, not through keys you manage. Connecting returns a Result carrying the available accounts; getSigner() returns a standard PAPI PolkadotSigner, but only once an account is selected:

import { SignerManager } from "@parity/product-sdk/wallet";

const manager = new SignerManager();
const connected = await manager.connect(); // or connect("dev") for Alice/Bob
if (!connected.ok) throw connected.error;

const [account] = connected.value;
if (!account) throw new Error("Host wallet returned no accounts");

manager.selectAccount(account.address);
const signer = manager.getSigner(); // PolkadotSigner | null

Because connect() reports failure on the err channel rather than throwing, a missed check leaves you with a null signer and no error to explain it.

Store and retrieve data (Cloud Storage)

Cloud Storage is content-addressed storage backed by the Polkadot Bulletin chain — the platform's dedicated way for an app to persist data. Store bytes, get back a root CID, read them back by CID. Uploads need a connected wallet with a selected account, and a storage authorization on that account; for the model behind it see Storage & data.

import { createApp } from "@parity/product-sdk";

const app = await createApp({
  name: "my-app",
  cloudStorage: { environment: "devnet" },
});

const cloud = app.cloudStorage;
if (!cloud) throw new Error("Cloud Storage is disabled");

const uploaded = await cloud.upload(bytes);
if (!uploaded.ok) throw uploaded.error;

const fetched = await cloud.fetch(uploaded.value);
if (fetched.ok) {
  render(fetched.value); // Uint8Array
}

cloudStorage.environment defaults to paseo

Omit the option and your app reads and writes Bulletin on Paseo, not on this Devnet — a different chain, so your data is simply not where you expect it and nothing errors. The only two values are paseo and devnet. Passing cloudStorage: false disables the service, which is what makes app.cloudStorage nullable.

cloud.computeCid(data) computes a CID without uploading (calculateCid from the package root does the same without an App). Reads are container-only — there is no IPFS-gateway fallback.

Call smart contracts

Contracts on the devnet are PolkaVM bytecode running on Asset Hub via pallet-revive, exposing Solidity-shaped ABIs. Build a runtime from a raw PAPI client, then a typed contract handle:

import {
  createContract,
  createContractRuntimeFromClient,
} from "@parity/product-sdk/contracts";

// client and manager as above; address and abi come from your CDM package.
const runtime = createContractRuntimeFromClient(client.raw.assetHub, devnet_asset_hub);
const contract = createContract(runtime, address, abi, { signerManager: manager });

The argument order is (runtime, address, abi) — address first. The optional fourth argument supplies the signer: pass { signerManager } for .tx() calls, which resolve the selected account at call time. Read-only .query() calls need no account and fall back to the keyless QUERY_FALLBACK_ORIGIN. ensureContractAccountMapped(runtime, address, signer) maps an account for pallet-revive when a call requires it.

To resolve a contract's address and ABI from a package name, see Deploy & register contracts (CDM), which the CDM Frontend also uses at runtime.

Read identity and personhood

To prove a user controls a .dot identity, sign with the account that owns it. The wallet resolves the username's owner on the People chain and signs with that account:

import { devnet_individuality } from "@parity/product-sdk-descriptors/devnet-individuality";

const proof = await app.wallet.signMessageWithDotNsIdentity({
  peopleChain: devnet_individuality,
  username: "alice", // omit to use the user's primary .dot name
  message: "Sign in to my-app",
});

proof.username; // username used for the lookup
proof.accountId; // owning AccountId32, 0x-prefixed
proof.signature; // Uint8Array

Usernames are matched byte-for-byte against the chain's records, with no .dot suffix handling — pass the exact value that is registered. Omitting username triggers a host identity-permission prompt. If your app already connects the People chain with app.chain.connect({ … }), the lookup reuses that connection.

For privacy-preserving identifiers, derive an alias instead of exposing an address. deriveContextAlias(parentAddress, context) is deterministic and checkable with verifyContextAlias; deriveAnonymousAlias(context, ringLocation) produces a Ring VRF alias that cannot be linked back to the parent account:

import { deriveContextAlias } from "@parity/product-sdk/identity";

const alias = deriveContextAlias(account.address, "voting-round-1");
alias.address; // SS58 alias
alias.h160Address; // the same alias as an EVM address

Both are per-context, so the same user is a different alias in every app. isValidDotNsName and normalizeDotNsName validate and canonicalise a name before you use it.

For the raw host session — login state, product accounts, ring proofs — call getAccountsProvider() from @parity/product-sdk/host. It is async and resolves to null outside a host container, and its methods return neverthrow ResultAsync values (.isOk()), not the SDK's own Result.

To read a user's personhood tier on-chain, call the personhood pallet-revive precompile at the fixed address 0x000000000000000000000000000000000A010000 with a contract handle. Its personhoodStatus(account, bytes32 context) returns a status (0 = None, 1 = Lite, 2 = Full) and a per-application contextAlias. See the Identity & personhood architecture page for the full model.

Test without the real host

@parity/host-api-test-sdk is a thin Playwright host that speaks the real host protocol with auto-signed dev accounts, so you can drive your app end-to-end in CI. It needs @playwright/test as a peer, and the fixture lives on the /playwright subpath:

npm i -D @parity/host-api-test-sdk @playwright/test
import { test as base } from "@playwright/test";
import {
  createTestHostFixture,
  type TestHost,
} from "@parity/host-api-test-sdk/playwright";

const test = base.extend<{ testHost: TestHost }>(
  createTestHostFixture({
    productUrl: "http://localhost:5173",
    accounts: ["alice"],
  }),
);

test("signs a transfer", async ({ testHost }) => {
  await testHost.waitForConnection();
  await testHost.productFrame().getByRole("button", { name: "Send" }).click();

  const signed = await testHost.getSigningLog();
  // assert on what the host was asked to sign
});

The fixture embeds your product in an iframe, injects the dev accounts, auto-signs signing requests, and proxies chain RPC over WebSocket. switchAccount, getSigningLog and the permission / chat / payment logs let tests assert on host behaviour. The shipped networks presets are PASEO_ASSET_HUB (the default), PREVIEWNET and PREVIEWNET_ASSET_HUB — to point the test host at this Devnet, pass your own NetworkConfig ({ id, name, genesisHash, rpcUrl, tokenSymbol, tokenDecimals }) built from Networks & endpoints.

Learn more