butr
Integrations

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 and the extrinsic builder; the wallet's injectedWeb3 interface supplies signing.

Get the wallet

import { useSelectedWallet } from "@usebutr/react";

const wallet = useSelectedWallet("polkadot");

Connect a PAPI client to the chain

Point PAPI at Paseo testnet and read balances without routing through the wallet:

import { paseo } from "@polkadot-api/descriptors";
import { createClient } from "polkadot-api";
import { getWsProvider } from "polkadot-api/ws";

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's PAS has 10 decimals.
const pas = `${Number(data.free) / 1e10} PAS`;

Sign a message

signMessage appears once connect() has enabled an extension that exposes signRaw (or, on Wallet Standard, when the wallet advertises polkadot:signMessage), so check it on the connected wallet:

if (!wallet.connector.signMessage) {
  throw new Error(`${wallet.connector.name} cannot sign messages`);
}
const message = new TextEncoder().encode("Hello from butr + polkadot-api");
const { signature, signedMessage } = await wallet.connector.signMessage(message, {
  account: wallet.account,
});
// signedMessage is the input wrapped in <Bytes>…</Bytes>: verify against it.

Build and submit an extrinsic

butr ships no Polkadot RPC, so a Polkadot adapter has no sendTx. Narrow the signer to polkadot-injected, whose extensionName is what PAPI's connectInjectedExtension takes, then sign as the connected wallet's account:

import { MultiAddress } from "@polkadot-api/descriptors";
import { connectInjectedExtension } from "polkadot-api/pjs-signer";

const signer = await wallet.connector.getSigner();
if (signer.kind !== "polkadot-injected") {
  throw new Error("Extrinsics here go through an injectedWeb3 extension");
}
const extension = await connectInjectedExtension(signer.extensionName);
const account = extension
  .getAccounts()
  .find((candidate) => candidate.address === wallet.account.walletAddress);
if (account === undefined) {
  throw new Error("The extension no longer exposes the active account");
}

// Self-transfer of 0.1 PAS (1_000_000_000 planck).
const tx = api.tx.Balances.transfer_keep_alive({
  dest: MultiAddress.Id(wallet.account.walletAddress),
  value: 1_000_000_000n,
});

tx.signSubmitAndWatch(account.polkadotSigner).subscribe({
  complete: () => console.log("Finalized"),
  error: (error) => console.error("Tx error:", error),
  next: (event) => console.log("Tx:", event.type),
});
// Look it up on https://paseo.subscan.io once finalized.

Polkadot extrinsics are SCALE-encoded and carry a mortal era; PAPI handles the encoding. Signing always happens in the wallet extension; butr never touches the private key. A Wallet Standard Polkadot wallet resolves { kind: "wallet-standard", wallet } instead, which this bridge does not cover.

Source: apps/demo-with-polkadot/src/app.tsx in the butr repository. Targets Paseo testnet. Run pnpm dev --filter=demo-with-polkadot → https://usebutr.demo-with-polkadot.localhost.