Links
Comment on page

Partially Signed Bitcoin Transactions (PSBTs)

Sign inputs of a Partially Signed Bitcoin Transaction (PSBT)
PSBTs allow apps to construct custom Bitcoin transactions from users' wallets. For this feature you'll need to be familiar with how bitcoin transactions work.
If you're looking to transfer bitcoin, see our simpler API for transferring funds.

API

interface SignPsbtRequestParams {
hex: string;
allowedSighash?: SignatureHash[];
signAtIndex?: number | number[];
network?: NetworkModes; // default is user's current network
account?: number; // default is user's current account
broadcast?: boolean; // default is false - finalize/broadcast tx
}
const requestParams: SignPsbtRequestParams = { ...params };
const result = await window.btc.request('signPsbt', requestParams);
The API returns a PSBT.

Sample code

Before getting started, you'll first need to know details about a user's wallet. Check out our guide on the getAddresses request.
When making a PSBT signing request, pass the account number that the user authenticated with. This can be extracted from the derivationPath property returned from getAddresses.
Here's a code snippet that helps pull this account number:
function extractAccountNumber(path: string) {
const segments = path.split('/');
const accountNum = parseInt(segments[3].replaceAll("'", ''), 10);
if (isNaN(accountNum)) throw new Error('Cannot parse account number from path');
return accountNum;
}
const accountNumber = extractAccountNumber("m/84'/0'/4/0/0"); // 4
Install and import packages needed to create PSBT. We recommend @scure/btc-signer
import * as btc from '@scure/btc-signer';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';

Broadcasting a transaction

Our API returns a PSBT back to the app, with the newly applied signatures. If you use-case is to broadcast the transaction, you'll first need to finalize it and submit it to the network.
See this example below how to extract a signed transaction from the PSBT
import * as btc from '@scure/btc-signer';
const hexRespFromHiro = resp.result.hex;
const tx = btc.Transaction.fromPSBT(hexToBytes(hexRespFromHiro));
tx.finalize();
await broadcast(tx.hex);
Types and helper functions:
interface BitcoinNetwork {
bech32: string;
pubKeyHash: number;
scriptHash: number;
wif: number;
}
const bitcoinTestnet: BitcoinNetwork = {
bech32: 'tb',
pubKeyHash: 0x6f,
scriptHash: 0xc4,
wif: 0xef,
};
const ecdsaPublicKeyLength = 33;
function ecdsaPublicKeyToSchnorr(pubKey: Uint8Array) {
if (pubKey.byteLength !== ecdsaPublicKeyLength) throw new Error('Invalid public key length');
return pubKey.slice(1);
}
function getTaprootPayment(publicKey: Uint8Array) {
return btc.p2tr(ecdsaPublicKeyToSchnorr(publicKey), undefined, bitcoinTestnet);
}
Helper functions for Native SegWit PsbtRequestOptions:
// Example request options for Native SegWit
function buildNativeSegwitPsbtRequestOptions1(pubKey: Uint8Array): PsbtRequestOptions {
// SegWit pubKey used here is provided through auth response
const p2wpkh = btc.p2wpkh(pubKey, bitcoinTestnet);
const tx = new btc.Transaction();
// Example inputs with included witness
tx.addInput({
index: 0,
txid: '15f34b3bd2aab555a003cd1c6959ac09b36239c6af1cb16ff8198cef64f8db9c',
witnessUtxo: {
amount: BigInt(1000),
script: p2wpkh.script,
},
});
tx.addInput({
index: 1,
txid: 'dca5179afaa63eae112d8a97794de2d30dd823315bcabe6d8b8a6b98e3567705',
witnessUtxo: {
amount: BigInt(2000),
script: p2wpkh.script,
},
});
// Add outputs and/or other psbt data...
const psbt = tx.toPSBT();
// This example is choosing to sign all inputs bc no index(es) are provided
return { hex: bytesToHex(psbt) };
}
Helper functions for Taproot PsbtRequestOptions
// Similar example request options for Taproot
function buildTaprootPsbtRequestOptions1(pubKey: Uint8Array): PsbtRequestOptions {
// Taproot pubKey used here is provided through auth response
const payment = getTaprootPayment(pubKey);
const tx = new btc.Transaction();
tx.addInput({
index: 0,
tapInternalKey: payment.tapInternalKey,
txid: '15f34b3bd2aab555a003cd1c6959ac09b36239c6af1cb16ff8198cef64f8db9c',
witnessUtxo: {
amount: BigInt(1000),
script: payment.script,
},
});
tx.addInput({
index: 1,
tapInternalKey: payment.tapInternalKey,
txid: 'dca5179afaa63eae112d8a97794de2d30dd823315bcabe6d8b8a6b98e3567705',
witnessUtxo: {
amount: BigInt(2000),
script: payment.script,
},
});
// Add outputs and/or other psbt data...
const psbt = tx.toPSBT();
return { hex: bytesToHex(psbt) };
}
Note: After an app receives the returned PSBT in the response, they should finalize the inputs and transaction to broadcast it to the network.