Documentation Index
Fetch the complete documentation index at: https://mintlify.com/satsigner/satsigner/llms.txt
Use this file to discover all available pages before exploring further.
SatSigner provides comprehensive transaction construction tools with advanced features for fee management, Replace-By-Fee, and time-locked transactions.
Transaction Builder
Builder State
The transaction builder maintains state throughout the construction process:
type TransactionBuilderState = {
inputs: Map<string, Utxo> // Selected UTXOs (txid:vout => Utxo)
outputs: Output[] // Destination outputs
feeRate: number // Satoshis per virtual byte
fee: number // Total transaction fee
timeLock: number // Optional nLockTime
rbf: boolean // Replace-By-Fee enabled
cpfp: boolean // Child-Pays-For-Parent
txBuilderResult?: TxBuilderResult
psbt?: PartiallySignedTransaction
signedTx?: string
broadcasted: boolean
}
Creating Transactions
Basic Transaction Flow:
- Select input UTXOs
- Add recipient outputs
- Set fee rate
- Review transaction
- Sign transaction
- Broadcast to network
Manual Selection
Users select UTXOs via the bubble chart interface:
// Add input to transaction
function addInput(utxo: Utxo) {
const outpoint = getUtxoOutpoint(utxo) // "txid:vout"
transactionBuilder.inputs.set(outpoint, utxo)
}
// Remove input
function removeInput(utxo: Utxo) {
const outpoint = getUtxoOutpoint(utxo)
transactionBuilder.inputs.delete(outpoint)
}
// Check if UTXO selected
function hasInput(utxo: Utxo): boolean {
const outpoint = getUtxoOutpoint(utxo)
return transactionBuilder.inputs.has(outpoint)
}
Automatic Selection
Algorithmic UTXO selection for optimal transactions:
// Efficient selection (lowest fees)
const { inputs, fee, change } = selectEfficientUtxos(
availableUtxos,
targetAmount,
feeRate
)
// Privacy-focused selection
const { inputs, outputs, fee, privacyScore } = selectStonewallUtxos(
availableUtxos,
targetAmount,
feeRate
)
See UTXO Control for detailed selection strategies.
Output Management
Adding Recipients
type Output = {
localId: string // Internal identifier
to: string // Bitcoin address
amount: number // Satoshis
label?: string // Optional label
}
// Add output
function addOutput(output: Omit<Output, 'localId'>) {
transactionBuilder.outputs.push({
localId: randomUuid(),
...output
})
}
// Update output
function updateOutput(localId: string, output: Omit<Output, 'localId'>) {
const index = transactionBuilder.outputs.findIndex(
o => o.localId === localId
)
if (index !== -1) {
transactionBuilder.outputs[index] = { localId, ...output }
}
}
// Remove output
function removeOutput(localId: string) {
const index = transactionBuilder.outputs.findIndex(
o => o.localId === localId
)
if (index !== -1) {
transactionBuilder.outputs.splice(index, 1)
}
}
Change Outputs
Change is automatically calculated and returned to your wallet:
// Calculate change
const totalInput = Array.from(inputs.values())
.reduce((sum, utxo) => sum + utxo.value, 0)
const totalOutput = outputs
.reduce((sum, output) => sum + output.amount, 0)
const change = totalInput - totalOutput - fee
// Handle change
if (change > DUST_THRESHOLD) {
// Add change output to internal address
const changeAddress = await wallet.getInternalAddress()
outputs.push({
to: changeAddress,
amount: change,
label: "Change"
})
} else {
// Add to miner fee if below dust threshold
fee += change
}
Fee Management
Fee Rate Selection
SatSigner provides real-time fee estimates:
type FeeEstimate = {
fastestFee: number // Next block (high priority)
halfHourFee: number // ~3 blocks
hourFee: number // ~6 blocks
economyFee: number // ~24 blocks (low priority)
minimumFee: number // Minimum relay fee (1 sat/vB)
}
Setting Fee Rate:
// Set fee rate (sat/vB)
setFeeRate(10)
// Calculate total fee
const estimatedSize = calculateTxSize(inputs, outputs)
const totalFee = estimatedSize * feeRate
setFee(totalFee)
Fee Calculation
Accurate fee estimation based on transaction structure:
function calculateTxSize(
inputs: Utxo[],
outputs: Output[],
scriptType: ScriptVersionType
): number {
let size = 10 // Base transaction overhead
// Input sizes by script type
const inputSizes = {
'P2PKH': 148,
'P2SH-P2WPKH': 91,
'P2WPKH': 68,
'P2TR': 58
}
// Output sizes
const outputSizes = {
'P2PKH': 34,
'P2SH': 32,
'P2WPKH': 31,
'P2TR': 43
}
// Add input sizes
size += inputs.length * (inputSizes[scriptType] || 148)
// Add output sizes
size += outputs.length * 31 // Average output size
return size
}
Custom Fee Rates
Advanced users can set precise fee rates:
// Set absolute fee amount
setFee(5000) // 5000 sats total
// Calculate resulting fee rate
const feeRate = fee / txSize // sat/vB
Important: Ensure fee rate meets minimum relay fee (typically 1 sat/vB).
Building Transactions with BDK
BDK Transaction Builder
SatSigner uses Bitcoin Dev Kit for transaction construction:
async function buildTransaction(
wallet: Wallet,
data: {
inputs: Utxo[]
outputs: Output[]
fee: number
options: {
rbf: boolean
}
},
network: Network
): Promise<TxBuilderResult> {
// Create transaction builder
const txBuilder = await new TxBuilder().create()
// Add specific UTXOs
await txBuilder.addUtxos(
data.inputs.map(utxo => ({
txid: utxo.txid,
vout: utxo.vout
}))
)
// Only use specified UTXOs
await txBuilder.manuallySelectedOnly()
// Add recipient outputs
for (const output of data.outputs) {
const scriptPubKey = await getScriptPubKeyFromAddress(
output.to,
network
)
await txBuilder.addRecipient(scriptPubKey, output.amount)
}
// Set absolute fee
await txBuilder.feeAbsolute(data.fee)
// Enable RBF if requested
if (data.options.rbf) {
await txBuilder.enableRbf()
}
// Finalize transaction
const txBuilderResult = await txBuilder.finish(wallet)
return txBuilderResult
}
Transaction Details
Review transaction before signing:
type TransactionDetails = {
// Inputs
inputs: {
txid: string
vout: number
value: number
address: string
}[]
// Outputs
outputs: {
address: string
value: number
isChange: boolean
}[]
// Fees
fee: number
feeRate: number
// Size
size: number // Bytes
vsize: number // Virtual bytes (weight / 4)
weight: number // Weight units
// Lock time
lockTime: number
version: number
}
Replace-By-Fee (RBF)
Accelerate stuck transactions by replacing with higher fee:
Enabling RBF
// Enable RBF when building transaction
const txBuilder = await new TxBuilder().create()
await txBuilder.enableRbf()
// RBF signals via sequence number
// Sequence < 0xfffffffe indicates RBF-enabled
Creating Replacement Transaction
function createReplacementTx(
originalTx: Transaction,
newFeeRate: number
): TransactionBuilder {
// Use same inputs
const inputs = originalTx.vin.map(input => ({
txid: input.previousOutput.txid,
vout: input.previousOutput.vout
}))
// Adjust output amounts for higher fee
const newFee = calculateFee(originalTx.vsize, newFeeRate)
const feeIncrease = newFee - originalTx.fee
// Reduce change output by fee increase
const outputs = originalTx.vout.map((output, index) => {
if (output.isChange) {
return {
...output,
value: output.value - feeIncrease
}
}
return output
})
return {
inputs,
outputs,
fee: newFee,
rbf: true
}
}
RBF Rules (BIP 125):
- Original transaction must signal RBF
- Replacement must pay higher absolute fee
- Replacement must pay higher fee rate
- Replacement cannot add new unconfirmed inputs
- Total fee must be at least relay fee increment
Time-Locked Transactions
nLockTime
Delay transaction validity until specific time/block:
// Block height lock (< 500,000,000)
const lockTime = 800000 // Block height
// Unix timestamp lock (>= 500,000,000)
const lockTime = Math.floor(Date.now() / 1000) + (24 * 60 * 60) // +1 day
// Set lock time
await txBuilder.setLockTime(lockTime)
Use Cases:
- Future payments
- Trust minimization
- Escrow releases
- Scheduled transactions
Sequence-Based Locks (BIP 68)
Relative time locks using input sequences:
// Sequence encodes relative lock time
const sequenceValue = (
(1 << 22) | // Type flag (blocks vs time)
(lockTime & 0xFFFF) // Lock time value
)
// Example: Lock for 144 blocks (~1 day)
const sequence = 144
Child-Pays-For-Parent (CPFP)
Accelerate parent transaction by spending its output with high fee:
function createCPFPTransaction(
parentTx: Transaction,
childFeeRate: number
): TransactionBuilder {
// Spend unconfirmed parent output
const parentUtxo = {
txid: parentTx.id,
vout: 0, // Assuming first output
value: parentTx.vout[0].value
}
// Calculate combined fee rate
const parentSize = parentTx.vsize
const childSize = 110 // Estimated child size (1-in, 1-out)
const totalSize = parentSize + childSize
const parentFee = parentTx.fee || 0
const targetFee = totalSize * childFeeRate
const childFee = targetFee - parentFee
return {
inputs: [parentUtxo],
outputs: [{
address: selfAddress,
amount: parentUtxo.value - childFee
}],
fee: childFee,
cpfp: true
}
}
CPFP vs RBF:
| Feature | CPFP | RBF |
|---|
| Requires | Unconfirmed output | Original transaction |
| Who can use | Anyone with output | Original sender only |
| Fee payment | Child transaction | Replacement transaction |
| Complexity | Higher (2 txs) | Lower (1 tx) |
Signing Transactions
Single-Signature
async function signTransaction(
txBuilderResult: TxBuilderResult,
wallet: Wallet
): Promise<PartiallySignedTransaction> {
// Sign with wallet
const psbt = await wallet.sign(txBuilderResult.psbt)
return psbt
}
Multi-Signature
Multisig requires PSBT workflow:
// 1. Create unsigned PSBT
const psbtBase64 = txBuilderResult.psbt.toBase64()
// 2. Share PSBT with cosigners
// (via QR code, file, or secure channel)
// 3. Each cosigner signs
const signedPsbt = await signPSBTWithSeed(
psbtBase64,
mnemonic,
scriptType
)
// 4. Combine signatures
const combinedPsbt = combinePsbts([
psbtBase64,
signedPsbt1,
signedPsbt2
])
// 5. Finalize when threshold met
if (hasEnoughSignatures(combinedPsbt, account.keysRequired)) {
const finalTx = psbt.extractTransaction()
}
See Multi-Signature for complete workflow.
Broadcasting Transactions
Network Broadcast
async function broadcastTransaction(
psbt: PartiallySignedTransaction,
blockchain: Blockchain
): Promise<string> {
// Extract final transaction
const transaction = await psbt.extractTx()
// Broadcast to network
const txid = await blockchain.broadcast(transaction)
return txid
}
Broadcast Verification
// Verify broadcast success
const txid = await broadcastTransaction(psbt, blockchain)
// Check transaction in mempool
const txDetails = await blockchain.getTransaction(txid)
if (txDetails) {
console.log('Transaction broadcast successful')
console.log('TXID:', txid)
} else {
console.error('Transaction not found in mempool')
}
Transaction Validation
Pre-Broadcast Checks
function validateTransaction(
tx: TransactionBuilderState
): { valid: boolean; errors: string[] } {
const errors: string[] = []
// Check inputs
if (tx.inputs.size === 0) {
errors.push('No inputs selected')
}
// Check outputs
if (tx.outputs.length === 0) {
errors.push('No outputs specified')
}
// Check addresses
for (const output of tx.outputs) {
if (!isValidBitcoinAddress(output.to)) {
errors.push(`Invalid address: ${output.to}`)
}
}
// Check amounts
const totalOutput = tx.outputs.reduce((sum, o) => sum + o.amount, 0)
const totalInput = Array.from(tx.inputs.values())
.reduce((sum, u) => sum + u.value, 0)
if (totalOutput + tx.fee > totalInput) {
errors.push('Insufficient funds')
}
// Check dust outputs
for (const output of tx.outputs) {
if (output.amount < DUST_THRESHOLD) {
errors.push(`Output below dust threshold: ${output.amount} sats`)
}
}
// Check fee rate
if (tx.feeRate < 1) {
errors.push('Fee rate below minimum relay fee')
}
return {
valid: errors.length === 0,
errors
}
}
Advanced Features
SegWit Benefits
SegWit transactions provide:
- Lower fees - Witness data gets 75% discount
- Transaction malleability fix - Enables Lightning Network
- Larger block capacity - More transactions per block
// SegWit weight calculation
const baseSize = nonWitnessSize
const totalSize = baseSize + witnessSize
const weight = (baseSize * 4) + witnessSize
const vsize = Math.ceil(weight / 4)
Batch Transactions
Save fees by combining multiple payments:
// Single transaction with multiple outputs
const batchPayment = {
inputs: selectedUtxos,
outputs: [
{ address: recipient1, amount: 100000 },
{ address: recipient2, amount: 200000 },
{ address: recipient3, amount: 150000 },
{ address: changeAddress, amount: changeAmount }
],
fee: calculatedFee
}
// vs. Individual transactions
// Fee savings: ~70% (1 tx vs 3 txs)
Best Practices
- Always enable RBF - Allows fee adjustment if needed
- Use appropriate fee rates - Higher for urgent, lower for patient
- Minimize inputs - Fewer inputs = lower fees
- Batch when possible - Combine multiple payments
- Verify addresses - Double-check recipient addresses
- Test on testnet - Practice with test coins first
- Monitor mempool - Adjust strategy based on network conditions
- Keep change above dust - Avoid uneconomical outputs
Troubleshooting
Transaction Not Confirming
Solutions:
- Use RBF to increase fee
- Use CPFP if RBF not enabled
- Wait for lower mempool congestion
- Check if transaction was actually broadcast
Insufficient Funds Error
Causes:
- Not accounting for fees
- Change output below dust threshold
- Rounding errors
Solutions:
- Reduce payment amount
- Add more input UTXOs
- Increase fee rate to reduce change
Invalid Transaction
Common issues:
- Double-spend attempt
- Invalid script
- Timelock not yet valid
- Insufficient fee
Solutions:
- Verify UTXOs are unspent
- Check script version compatibility
- Wait for timelock expiry
- Increase fee rate
SatSigner’s transaction builder provides professional-grade tools for constructing secure, efficient Bitcoin transactions with complete control over every detail.