Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt

Use this file to discover all available pages before exploring further.

Overview

This guide shows you how to sign Bitcoin transactions using Dynamic’s Node SDK. Bitcoin uses PSBTs (Partially Signed Bitcoin Transactions) for signing. You create a PSBT, pass it to signTransaction() as a base64 string, and receive the signed PSBT back.

Prerequisites

Step 1: Install Dependencies

bun add bitcoinjs-lib

Step 2: Create and Sign a PSBT

The approach for signing transactions depends on how you created your wallet: If you created your wallet with backUpToClientShareService: true, you can sign directly without retrieving key shares:
import { DynamicBtcWalletClient } from '@dynamic-labs-wallet/node-btc';
import { BitcoinNetwork } from '@dynamic-labs-wallet/core';
import * as bitcoin from 'bitcoinjs-lib';

export const authenticatedBtcClient = async () => {
  const client = new DynamicBtcWalletClient({
    environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
  });

  await client.authenticateApiToken(process.env.DYNAMIC_AUTH_TOKEN!);
  return client;
};

const btcClient = await authenticatedBtcClient();

// Create a PSBT (example: spending from a Native SegWit address)
const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });

// Add input (UTXO to spend)
psbt.addInput({
  hash: 'previous_txid_here',
  index: 0,
  witnessUtxo: {
    script: Buffer.from('0014...', 'hex'), // Your address script
    value: 50000, // Amount in satoshis
  },
});

// Add output (recipient)
psbt.addOutput({
  address: 'bc1q...recipient-address',
  value: 40000, // Amount in satoshis (minus fee)
});

// Convert PSBT to base64 for signing
const psbtBase64 = psbt.toBase64();

// Sign the PSBT
const signedPsbtBase64 = await btcClient.signTransaction({
  transaction: psbtBase64,
  senderAddress: 'bc1q...your-wallet-address',
  network: BitcoinNetwork.MAINNET,
});

console.log('Signed PSBT (base64):', signedPsbtBase64);

With Manual Backup

If you created your wallet with backUpToClientShareService: false, you must retrieve and provide external key shares:
// First, get external server key shares (required for manual backup)
const keyShares = await btcClient.getExternalServerKeyShares({
  accountAddress: 'bc1q...your-wallet-address',
});

const signedPsbtBase64 = await btcClient.signTransaction({
  transaction: psbtBase64,
  senderAddress: 'bc1q...your-wallet-address',
  network: BitcoinNetwork.MAINNET,
  externalServerKeyShares: keyShares, // Required for manual backup!
  password: 'your-password', // Only if wallet was created with password
});
Password Handling Notes:
  • If your wallet was created without a password, omit the password parameter
  • If your wallet was created with a password, you must provide it for all operations
  • The password parameter is always optional in the API, but required if the wallet is password-protected

Step 3: Finalize and Broadcast

After signing, finalize the PSBT and extract the raw transaction for broadcasting:
// Parse the signed PSBT
const signedPsbt = bitcoin.Psbt.fromBase64(signedPsbtBase64);

// Finalize all inputs
signedPsbt.finalizeAllInputs();

// Extract the raw transaction hex
const rawTx = signedPsbt.extractTransaction().toHex();

console.log('Raw transaction hex:', rawTx);

// Broadcast using your preferred method (e.g., blockstream.info API)
const response = await fetch('https://blockstream.info/api/tx', {
  method: 'POST',
  body: rawTx,
});

const txid = await response.text();
console.log('Transaction broadcasted:', txid);

Complete Example: Send Bitcoin

Here’s a complete example of creating, signing, and broadcasting a Bitcoin transaction:
import { DynamicBtcWalletClient } from '@dynamic-labs-wallet/node-btc';
import { BitcoinNetwork } from '@dynamic-labs-wallet/core';
import * as bitcoin from 'bitcoinjs-lib';

async function sendBitcoin({
  senderAddress,
  recipientAddress,
  amountSats,
  feeSats,
  utxo,
}: {
  senderAddress: string;
  recipientAddress: string;
  amountSats: number;
  feeSats: number;
  utxo: {
    txid: string;
    vout: number;
    value: number;
    script: Buffer;
  };
}) {
  const btcClient = await authenticatedBtcClient();

  // Create PSBT
  const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });

  // Add the UTXO input
  psbt.addInput({
    hash: utxo.txid,
    index: utxo.vout,
    witnessUtxo: {
      script: utxo.script,
      value: utxo.value,
    },
  });

  // Add recipient output
  psbt.addOutput({
    address: recipientAddress,
    value: amountSats,
  });

  // Add change output (if there's change)
  const change = utxo.value - amountSats - feeSats;
  if (change > 546) { // Dust threshold
    psbt.addOutput({
      address: senderAddress,
      value: change,
    });
  }

  // Sign the PSBT
  const signedPsbtBase64 = await btcClient.signTransaction({
    transaction: psbt.toBase64(),
    senderAddress,
    network: BitcoinNetwork.MAINNET,
  });

  // Finalize and extract
  const signedPsbt = bitcoin.Psbt.fromBase64(signedPsbtBase64);
  signedPsbt.finalizeAllInputs();
  const rawTx = signedPsbt.extractTransaction().toHex();

  // Broadcast
  const response = await fetch('https://blockstream.info/api/tx', {
    method: 'POST',
    body: rawTx,
  });

  return await response.text(); // Returns txid
}

Taproot Transaction Signing

For Taproot addresses, the signing process is the same, but uses Schnorr signatures internally:
// Taproot PSBT signing
const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });

psbt.addInput({
  hash: 'previous_txid_here',
  index: 0,
  witnessUtxo: {
    script: Buffer.from('5120...', 'hex'), // Taproot output script (OP_1 <pubkey>)
    value: 50000,
  },
  tapInternalKey: Buffer.from('...', 'hex'), // 32-byte x-only public key
});

psbt.addOutput({
  address: 'bc1p...recipient-taproot-address',
  value: 40000,
});

// Sign - the SDK handles Schnorr signatures automatically
const signedPsbtBase64 = await btcClient.signTransaction({
  transaction: psbt.toBase64(),
  senderAddress: 'bc1p...your-taproot-address',
  network: BitcoinNetwork.MAINNET,
});

Multi-Input Transactions

The SDK only signs inputs that belong to the sender address. Other inputs remain unsigned:
const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });

// Input from your wallet - will be signed
psbt.addInput({
  hash: 'txid1',
  index: 0,
  witnessUtxo: { script: yourScript, value: 50000 },
});

// Input from another party - will NOT be signed
psbt.addInput({
  hash: 'txid2',
  index: 0,
  witnessUtxo: { script: otherScript, value: 30000 },
});

// Sign - only your input gets signed
const signedPsbtBase64 = await btcClient.signTransaction({
  transaction: psbt.toBase64(),
  senderAddress: yourAddress,
  network: BitcoinNetwork.MAINNET,
});

Error Handling

try {
  const signedPsbt = await btcClient.signTransaction({
    transaction: psbt.toBase64(),
    senderAddress: walletAddress,
    network: BitcoinNetwork.MAINNET,
  });
  console.log('Transaction signed successfully');
} catch (error) {
  if (error.message.includes('No inputs found')) {
    console.error('None of the inputs belong to the sender address');
  } else {
    console.error('Transaction signing failed:', error.message);
  }
}

Next Steps