polkadot-api (PAPI)
Read state with a typed API client, sign messages, and submit extrinsics through the wallet on Paseo testnet.
polkadot-api (PAPI) is the modern Polkadot TypeScript SDK. butr discovers and
manages the wallet; PAPI owns the typed chain descriptor, the RPC client
(createClient), and the extrinsic builder; the wallet's injectedWeb3
interface supplies signing.
Connect a PAPI client to the chain
PAPI uses a light-client or WebSocket provider. Point it at Paseo testnet and read balances without routing through the wallet:
import { createClient } from "polkadot-api";
import { getWsProvider } from "polkadot-api/ws";
import { paseo } from "@polkadot-api/descriptors";
const client = createClient(getWsProvider("wss://paseo.rpc.amforc.com"));
const api = client.getTypedApi(paseo);
const { data } = await api.query.System.Account.getValue(wallet.account.walletAddress);
// Paseo testnet uses PAS with 10 decimals
const pas = `${Number(data.free) / 1e10} PAS`;Sign a message
The Polkadot adapter wires signMessage for raw-bytes signing — call it on the
connector and get the signature back:
const message = new TextEncoder().encode("Hello from butr + polkadot-api");
const { signature } = await wallet.connector.signMessage(message);
// `signature` is a Uint8Array — render as hex for display.Build and submit an extrinsic
getSigner() returns a PolkadotSignerHandle ({ extensionName, address }), not a PAPI
PolkadotSigner directly. Bridge it to one using connectInjectedExtension from
polkadot-api/pjs-signer, then submit with signSubmitAndWatch:
import { MultiAddress } from "@polkadot-api/descriptors";
import { connectInjectedExtension } from "polkadot-api/pjs-signer";
import type { PolkadotSignerHandle } from "@usebutr/polkadot";
// getSigner() returns { extensionName, address } — not a PAPI signer.
const handle = (await wallet.connector.getSigner()) as PolkadotSignerHandle;
// Bridge: pjs-signer reads the injected extension and produces a PolkadotSigner.
const extension = await connectInjectedExtension(handle.extensionName);
const account = extension.getAccounts().find((a) => a.address === handle.address);
if (!account) throw new Error("Active account not found in the injected extension");
// Self-transfer of 0.1 PAS (1_000_000_000 Planck; Paseo uses 10 decimals).
const tx = api.tx.Balances.transfer_keep_alive({
// oxlint-disable-next-line new-cap -- MultiAddress.Id is a polkadot-api enum-variant constructor
dest: MultiAddress.Id(handle.address),
value: 1_000_000_000n,
});
tx.signSubmitAndWatch(account.polkadotSigner).subscribe({
next: (event) => console.log("Tx:", event.type),
error: (err) => console.error("Tx error:", err),
complete: () => console.log("Finalized"),
});
// Look up on https://paseo.subscan.io after "Finalized"Unlike EVM, Polkadot extrinsics are SCALE-encoded and include a mortal era (block expiry). PAPI
handles encoding. getSigner() returns a handle (extensionName + address); you bridge it to a
PAPI PolkadotSigner via connectInjectedExtension. Signing is always done in the wallet
extension — butr never touches the private key.
Source: apps/demo-with-polkadot/src/app.tsx in the butr
repository. Targets
Paseo testnet. Run pnpm dev --filter=demo-with-polkadot → http://localhost:5185.