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.
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.
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
import * as btc from '@scure/btc-signer';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
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.
Last modified 3mo ago