Skip to content
LogoLogo

PST

Partially signed transactions for multisig, hardware and air-gapped signers, policy co-signers and swaps. Format and roles: PST concept.

FunctionRole
createPst(tx, spentOutputs, …)creator: wrap an unsigned transaction and what it spends
signPst(pst, signingKeys)signer: sign every input whose redeem names one of your keys
combinePst(a, b, …)combiner: verify and merge signatures
finalizePst(pst, preimages?)finalizer: build witnesses, verify, return the signed transaction
decodePst / encodePstbase64 or binary ↔ PST object (the decoder enforces §16.2)
pstStatus(pst)which inputs still need signatures
const pst = createPst(tx, spentOutputs, 'regtest')
const a = signPst(encodePst(pst), aliceKeys)
const b = signPst(encodePst(pst), bobKeys)
const signed = finalizePst(combinePst(a, b))
await sendTransaction(client, { tx: signed })

Example

/**
 * 2-of-3 multisig with a PST: fund a shared vault, then two co-signers sign independently.
 *
 *   ARREKTA_URL=http://127.0.0.1:28332 ARREKTA_ADMIN_TOKEN=... npx tsx examples/multisig-pst.ts
 */
import {
  addPstOrigins,
  addPstRedeem,
  combinePst,
  createClient,
  createMultisigPolicy,
  createPst,
  createTransaction,
  encodeAddress,
  equalLocks,
  finalizePst,
  fromHex,
  generateSeed,
  getChain,
  getCoins,
  isPstComplete,
  keychainFromSeed,
  mine,
  multisigLock,
  pstFromBase64,
  pstToBase64,
  sendPayment,
  sendTransaction,
  signPstWithSeed,
  txid,
} from 'arrekta-sdk';
 
const URL = process.env.ARREKTA_URL ?? 'http://127.0.0.1:28332';
const ADMIN = process.env.ARREKTA_ADMIN_TOKEN;
const SEED = fromHex(process.env.ARREKTA_SEED ?? 'a1'.repeat(32));
 
// 1. Connect; a funder and three co-signers.
const client = createClient({ url: URL, adminToken: ADMIN });
const { network } = await getChain(client);
const funder = keychainFromSeed(SEED, { network });
const seeds = [generateSeed(), generateSeed(), generateSeed()];
const cosigners = seeds.map((s) => keychainFromSeed(s, { network }));
if ((await getCoins(client, { wallet: funder })).length === 0) await mine(client, funder.address(0), 101);
 
// 2. The 2-of-3 policy and its address.
const policy = createMultisigPolicy(2, cosigners.map((kc) => kc.key(0)));
const vaultLock = multisigLock(policy);
const vault = encodeAddress(vaultLock, network);
console.log(`vault ${vault}`);
 
// 3. Fund the vault with 4 ARRK and confirm.
const fund = await sendPayment(client, { wallet: funder, to: vault, amount: '4' });
await mine(client, funder.address(0), 1);
const vout = fund.tx.outputs.findIndex((o) => equalLocks(o.lock, vaultLock));
const coin = fund.tx.outputs[vout]!;
 
// 4. The coordinator creates a PST paying 3.9998 ARRK back to the funder.
const tx = createTransaction({
  inputs: [{ prevout: { txid: txid(fund.tx), vout }, sequence: 0xffffffff }],
  outputs: [{ asset: coin.asset, amount: coin.amount - 20_000n, lock: funder.lock(1) }],
});
const unsigned = addPstOrigins(addPstRedeem(createPst(tx, [coin], network), [policy]), cosigners.map((kc) => kc.origin(0)));
const shared = pstToBase64(unsigned); // base64, sent to each co-signer
 
// 5. Co-signers 1 and 3 sign independently.
const signedA = pstToBase64(signPstWithSeed(pstFromBase64(shared), seeds[0]!));
const signedC = pstToBase64(signPstWithSeed(pstFromBase64(shared), seeds[2]!));
 
// 6. Combine, finalize and broadcast.
const combined = combinePst(pstFromBase64(signedA), pstFromBase64(signedC));
console.log('complete:', isPstComplete(combined));
const final = finalizePst(combined);
console.log('sent', (await sendTransaction(client, { tx: final })).txid);
await mine(client, funder.address(0), 1);

Source: sdk/typescript/examples/multisig-pst.ts (updated 2026-11-02) · sdk/python/examples/multisig_pst.py (updated 2026-11-02). Runnable against a regtest node; CI runs every example.