> ## 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.

# Sign BTC Transactions

> Learn how to sign Bitcoin transactions using Dynamic's Node SDK

## 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

* [Created a Bitcoin wallet](https://docs.dynamic.xyz/docs/node/btc/create-wallet.md)
* [Installed Bitcoin dependencies](https://github.com/bitcoinjs/bitcoinjs-lib)

## Step 1: Install Dependencies

```bash theme={"system"}
bun add bitcoinjs-lib
```

> [!NOTE]
>
> These examples use `bitcoinjs-lib` v7, where satoshi amounts are `bigint` values (for example `50_000n`), not numbers.

## Step 2: Create and Sign a PSBT

The approach for signing transactions depends on how you created your wallet:

### With Automatic Backup (Recommended)

If you created your wallet with `backUpToDynamic: true`, you can sign directly without retrieving key shares. `walletMetadata` is the object returned by `createWalletAccount` (or `importPrivateKey`), and `password` is the one you set at creation:

```typescript theme={"system"}
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: 50_000n, // Amount in satoshis
  },
});

// Add output (recipient)
psbt.addOutput({
  address: 'bc1q...recipient-address',
  value: 40_000n, // 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,
  walletMetadata,
  network: BitcoinNetwork.MAINNET,
  password: process.env.WALLET_PASSWORD!,
});

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

### Signing on Testnet

For testnet, use `bitcoin.networks.testnet` for the PSBT and `BitcoinNetwork.TESTNET` for `network`. Build the PSBT with the wallet's testnet address (`tb1q...`), which you get from `deriveAccountAddress` with `network: BitcoinNetwork.TESTNET`, and keep passing the `walletMetadata` from `createWalletAccount` unchanged:

```typescript theme={"system"}
const { accountAddress: testnetAddress } = btcClient.deriveAccountAddress({
  rawPublicKey,
  addressType: BitcoinAddressType.NATIVE_SEGWIT,
  network: BitcoinNetwork.TESTNET,
});

const network = bitcoin.networks.testnet;
const psbt = new bitcoin.Psbt({ network });

psbt.addInput({
  hash: utxo.txid,
  index: utxo.vout,
  witnessUtxo: {
    script: bitcoin.address.toOutputScript(testnetAddress, network),
    value: BigInt(utxo.value),
  },
});
psbt.addOutput({ address: testnetAddress, value: 1_000n });

const signedPsbtBase64 = await btcClient.signTransaction({
  transaction: psbt.toBase64(),
  walletMetadata,
  network: BitcoinNetwork.TESTNET,
  password: process.env.WALLET_PASSWORD!,
});

// Broadcast to https://blockstream.info/testnet/api/tx
```

A missing or incorrect `network` can make signing fail with `No inputs found that belong to the account address`. See [Working with testnet](https://docs.dynamic.xyz/docs/node/btc/example-usage.md#working-with-testnet).

### With Manual Backup

If you created your wallet with `backUpToDynamic: false`, you must retrieve and provide external key shares:

```typescript theme={"system"}
// First, get external server key shares (required for manual backup)
const keyShares = await btcClient.getExternalServerKeyShares({
  accountAddress: walletMetadata.accountAddress,
  walletMetadata,
  password: process.env.WALLET_PASSWORD, // Only if wallet was created with password
});

const signedPsbtBase64 = await btcClient.signTransaction({
  transaction: psbtBase64,
  walletMetadata,
  network: BitcoinNetwork.MAINNET,
  externalServerKeyShares: keyShares, // Required for manual backup!
  password: process.env.WALLET_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
* Wallets created with `backUpToDynamic: true` always have a password, because one is required at creation

## Step 3: Finalize and Broadcast

After signing, finalize the PSBT and extract the raw transaction for broadcasting:

```typescript theme={"system"}
// 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:

```typescript theme={"system"}
import { DynamicBtcWalletClient } from '@dynamic-labs-wallet/node-btc';
import type { WalletMetadata } from '@dynamic-labs-wallet/node';
import { BitcoinNetwork } from '@dynamic-labs-wallet/core';
import * as bitcoin from 'bitcoinjs-lib';

async function sendBitcoin({
  walletMetadata,
  recipientAddress,
  amountSats,
  feeSats,
  utxo,
}: {
  walletMetadata: WalletMetadata;
  recipientAddress: string;
  amountSats: bigint;
  feeSats: bigint;
  utxo: {
    txid: string;
    vout: number;
    value: bigint;
    script: Buffer;
  };
}) {
  const btcClient = await authenticatedBtcClient();
  const senderAddress = walletMetadata.accountAddress;
  const externalServerKeyShares = await vault.read(`wallet:${senderAddress}/shares`);

  // 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 > 546n) { // Dust threshold
    psbt.addOutput({
      address: senderAddress,
      value: change,
    });
  }

  // Sign the PSBT
  const signedPsbtBase64 = await btcClient.signTransaction({
    transaction: psbt.toBase64(),
    walletMetadata,
    externalServerKeyShares,
    network: BitcoinNetwork.MAINNET,
    password: process.env.WALLET_PASSWORD, // Only if wallet was created with password
  });

  // 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:

```typescript theme={"system"}
// 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: 50_000n,
  },
  tapInternalKey: Buffer.from('...', 'hex'), // 32-byte x-only public key
});

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

// Sign - the SDK handles Schnorr signatures automatically
const signedPsbtBase64 = await btcClient.signTransaction({
  transaction: psbt.toBase64(),
  walletMetadata,
  network: BitcoinNetwork.MAINNET,
  password: process.env.WALLET_PASSWORD!,
});
```

> [!NOTE]
>
> Taproot transaction signing requires `@dynamic-labs-wallet/node-btc` 1.1.59 or later. In earlier versions it fails with `Uint8Array expected`.

## Multi-Input Transactions

The SDK only signs inputs that belong to the sender address. Other inputs remain unsigned. To co-sign a P2WSH multisig input, see [Co-signing P2WSH Multisig Inputs](#co-signing-p2wsh-multisig-inputs):

```typescript theme={"system"}
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: 50_000n },
});

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

// Sign - only your input gets signed
const signedPsbtBase64 = await btcClient.signTransaction({
  transaction: psbt.toBase64(),
  walletMetadata,
  network: BitcoinNetwork.MAINNET,
  password: process.env.WALLET_PASSWORD!,
});
```

## Co-signing P2WSH Multisig Inputs

By default `signTransaction` signs only inputs that belong to the wallet's own address. To co-sign a P2WSH multisig, such as a 2-of-3 that holds this wallet's key next to keys held elsewhere, pass the witness script in `allowedWitnessScripts`. This requires `@dynamic-labs-wallet/node-btc` 1.1.59 or later and a Native SegWit wallet.

`allowedWitnessScripts` is an array of hex-encoded witness scripts. An input is signed only when all of these hold:

* Its witness script is in `allowedWitnessScripts`.
* The SHA-256 of the witness script matches the input's `scriptPubKey`.
* The script is a standard `m`-of-`n` multisig.
* The wallet's own compressed public key is one of the keys in the script.

Omitting `allowedWitnessScripts`, or passing an empty array, disables P2WSH signing.

The wallet's `publicKeyHex` is the uncompressed 65-byte key (it starts with `04`). A multisig script needs the compressed 33-byte form, so compress it before building the script:

```typescript theme={"system"}
import * as bitcoin from 'bitcoinjs-lib';
import * as ecc from '@bitcoinerlab/secp256k1';

bitcoin.initEccLib(ecc);

const network = bitcoin.networks.testnet;

const uncompressed = Buffer.from(publicKeyHex, 'hex'); // 65 bytes, 04 || x || y
const nodePubkey = Buffer.from(ecc.pointCompress(uncompressed, true));

// 2-of-3: this wallet plus two other co-signers
const witnessScript = bitcoin.payments.p2ms({
  m: 2,
  pubkeys: [nodePubkey, otherPubkeyA, otherPubkeyB],
  network,
}).output!;

const { address, output } = bitcoin.payments.p2wsh({
  redeem: { output: witnessScript, network },
  network,
});

const psbt = new bitcoin.Psbt({ network });

psbt.addInput({
  hash: utxo.txid,
  index: utxo.vout,
  witnessUtxo: { script: output!, value: BigInt(utxo.value) },
  witnessScript,
});
psbt.addOutput({ address: recipientAddress, value: BigInt(utxo.value) - 500n });

const partiallySignedPsbt = await btcClient.signTransaction({
  transaction: psbt.toBase64(),
  walletMetadata,
  network: BitcoinNetwork.TESTNET,
  password: process.env.WALLET_PASSWORD!,
  allowedWitnessScripts: [witnessScript.toString('hex')],
});
```

The returned PSBT is not finalized. It carries this wallet's partial signature. Merge the signatures from the other co-signers, then finalize and broadcast as in [Step 3](#step-3-finalize-and-broadcast):

```typescript theme={"system"}
const combined = bitcoin.Psbt.fromBase64(partiallySignedPsbt, { network });
combined.combine(bitcoin.Psbt.fromBase64(otherCoSignerPsbt, { network }));
combined.finalizeAllInputs();

const rawTx = combined.extractTransaction().toHex();
```

> [!WARNING]
>
> `allowedWitnessScripts` is your application's consent to co-sign that script. Build the list from scripts you constructed and trust, never from scripts taken from the PSBT or from user input. Pass `allowedSighash` as well to restrict the signature hash types the wallet will sign.

If you pass `signingIndexes` for an input whose witness script is not allowed, or does not contain the wallet's key, signing fails with `No inputs found that belong to the account address`.

## Error Handling

```typescript theme={"system"}
try {
  const signedPsbt = await btcClient.signTransaction({
    transaction: psbt.toBase64(),
    walletMetadata,
    network: BitcoinNetwork.MAINNET,
    password: process.env.WALLET_PASSWORD!,
  });
  console.log('Transaction signed successfully');
} catch (error) {
  const message = error instanceof Error ? error.message : String(error);
  if (message.includes('No inputs found')) {
    console.error('None of the inputs belong to the sender address');
  } else {
    console.error('Transaction signing failed:', message);
  }
}
```

## Next Steps

* [Sign Bitcoin messages](https://docs.dynamic.xyz/docs/node/btc/sign-messages.md)
* [Import existing Bitcoin private keys](https://docs.dynamic.xyz/docs/node/btc/import-private-keys.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.