PST
Partially signed transactions for multisig, hardware and air-gapped signers, policy co-signers and swaps. Format and roles: PST concept.
| Function | Role |
|---|---|
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 / encodePst | base64 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);"""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=... python examples/multisig_pst.py
"""
import os
from arrekta_sdk import (
Client,
Lock,
OutPoint,
Transaction,
TxIn,
TxOut,
combine_pst,
create_pst,
encode_address,
encode_pst,
finalize_pst,
generate_seed,
keychain_from_seed,
multisig_policy,
pst_status,
sign_pst,
)
URL = os.environ.get("ARREKTA_URL", "http://127.0.0.1:28332")
ADMIN = os.environ.get("ARREKTA_ADMIN_TOKEN")
SEED = bytes.fromhex(os.environ.get("ARREKTA_SEED", "a1" * 32))
def main() -> None:
# 1. Connect; a funder and three co-signers.
client = Client(URL, admin_token=ADMIN)
network = client.get_chain()["network"]
funder = keychain_from_seed(SEED, network=network)
cosigners = [keychain_from_seed(generate_seed(), network=network) for _ in range(3)]
if not client.get_coins(funder):
client.mine(funder.address(0), 101)
# 2. The 2-of-3 policy and its address.
policy = multisig_policy(2, [kc.account_at(0) for kc in cosigners])
policy_lock = Lock.to_multisig(policy)
vault = encode_address(policy_lock, network)
print(f"vault {vault}")
# 3. Fund the vault with 4 ARRK and confirm.
fund = client.send_payment(funder, vault, "4")
client.mine(funder.address(0), 1)
vout = next(i for i, o in enumerate(fund.tx.outputs) if o.lock == policy_lock)
coin = fund.tx.outputs[vout]
# 4. The coordinator creates a PST paying 3.9998 ARRK back to the funder.
tx = Transaction(inputs=(TxIn(OutPoint(fund.tx.txid, vout)),),
outputs=(TxOut(coin.asset, coin.amount - 20_000, funder.lock(1)),))
unsigned = create_pst(tx, [coin], network, redeem=[policy], origins=[kc.origin(0) for kc in cosigners])
shared = encode_pst(unsigned) # base64, sent to each co-signer
# 5. Co-signers 1 and 3 sign independently.
signed_a = encode_pst(sign_pst(shared, cosigners[0]))
signed_c = encode_pst(sign_pst(shared, cosigners[2]))
# 6. Combine, finalize and broadcast.
combined = combine_pst(signed_a, signed_c)
print("complete:", pst_status(combined)["complete"])
final = finalize_pst(combined)
print("sent", client.send_transaction(final)["txid"])
client.mine(funder.address(0), 1)
if __name__ == "__main__":
main()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.