Skip to main content

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:
  1. Select input UTXOs
  2. Add recipient outputs
  3. Set fee rate
  4. Review transaction
  5. Sign transaction
  6. Broadcast to network

Input Selection

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):
  1. Original transaction must signal RBF
  2. Replacement must pay higher absolute fee
  3. Replacement must pay higher fee rate
  4. Replacement cannot add new unconfirmed inputs
  5. 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:
FeatureCPFPRBF
RequiresUnconfirmed outputOriginal transaction
Who can useAnyone with outputOriginal sender only
Fee paymentChild transactionReplacement transaction
ComplexityHigher (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

  1. Always enable RBF - Allows fee adjustment if needed
  2. Use appropriate fee rates - Higher for urgent, lower for patient
  3. Minimize inputs - Fewer inputs = lower fees
  4. Batch when possible - Combine multiple payments
  5. Verify addresses - Double-check recipient addresses
  6. Test on testnet - Practice with test coins first
  7. Monitor mempool - Adjust strategy based on network conditions
  8. 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.