> ## 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 a Partially Signed Bitcoin Transaction (PSBT) with Embedded Wallets

> [card] Recommended: JavaScript SDK with React Hooks
>
> For new React apps, we recommend the JavaScript SDK with React Hooks (`@dynamic-labs-sdk/react-hooks`) instead of the legacy React SDK documented here. The JS SDK comes with many benefits such as a much smaller bundle size and other optimizations. Use the [React quickstart (JavaScript SDK)](https://docs.dynamic.xyz/docs/javascript/reference/react-quickstart.md) to get started.

# Signing PSBTs (Partially Signed Bitcoin Transactions) for Bitcoin Embedded Wallets

This guide explains how to use the `signPsbt` method to sign Partially Signed Bitcoin Transactions (PSBTs) for Bitcoin embedded wallets. By default the method signs only the inputs related to the user's wallet and returns a signed (but not finalized) PSBT for review and finalization. You can also opt in to co-signing a P2WSH multisig input.

## Overview

PSBTs allow multiple parties to collaboratively build and sign Bitcoin transactions. The `signPsbt` method:

* **Signs only inputs** that belong to the user's wallet address, plus any P2WSH multisig input whose witness script you allow with `allowedWitnessScripts`
* **Does NOT finalize** the PSBT, which remains partially signed for review
* **Returns a signed PSBT** that can be reviewed, combined with other signatures, and finalized by the user

## Method Signature

```typescript theme={"system"}
// From BitcoinWallet with embedded wallet (simplified interface)
const signedPsbt = await wallet.signPsbt(request: EmbeddedWalletSignPsbtRequest): Promise<BitcoinSignPsbtResponse>;
```

## Type Definitions

### For Embedded Wallets

```typescript theme={"system"}
type EmbeddedWalletSignPsbtRequest = {
  unsignedPsbtBase64: string; // The unsigned PSBT in Base64 format (the only required field)
  signingIndexes?: number[]; // Sign only these input indexes
  allowedSighash?: number[]; // Reject any input that declares a sighash type outside this list
  allowedWitnessScripts?: string[]; // Hex-encoded witness scripts of P2WSH multisig inputs to co-sign
};

type BitcoinSignPsbtResponse = {
  signedPsbt: string; // The signed (but not finalized) PSBT in Base64 format
};
```

> [!NOTE]
>
> `signingIndexes`, `allowedSighash`, and `allowedWitnessScripts` are available in `@dynamic-labs/bitcoin` 5.14.0 and later.

**Important**: Embedded wallets:

* Only require `unsignedPsbtBase64`. All other fields are optional.
* Read the sighash type from the PSBT. An input that declares none is signed with SIGHASH\_ALL (0x01), or SIGHASH\_DEFAULT (0x00) on a Taproot wallet.
* Sign all inputs that belong to the wallet address, unless you limit them with `signingIndexes`.
* Sign a P2WSH multisig input only when you list its witness script in `allowedWitnessScripts`.
* Do not support the `signature` parameter

## Important Notes

### PSBT is NOT Finalized

The `signPsbt` method **does NOT finalize** the PSBT. It only signs the inputs that belong to the user's wallet. The returned PSBT:

* Contains signatures for the user's inputs
* Remains in PSBT format (not a finalized transaction)
* Can be reviewed by the user before finalization
* Can be combined with signatures from other parties
* Must be finalized before broadcasting

### Signing Only User-Related Inputs

By default the method signs all inputs that belong to the user's wallet address. To sign only some of them, pass `signingIndexes`. Every index you list must belong to the wallet, otherwise the call throws an error and signs nothing. Inputs that belong to other addresses are left unsigned, unless they are P2WSH multisig inputs you allow with [`allowedWitnessScripts`](#co-signing-p2wsh-multisig-inputs).

### 🔍 User Review and Finalization

After signing, the PSBT should be:

1. **Reviewed** by the user to verify transaction details
2. **Combined** with other signatures if it's a multi-party transaction
3. **Finalized** using a Bitcoin library (e.g., `bitcoinjs-lib`)
4. **Broadcast** to the Bitcoin network

## Usage Examples

### Basic PSBT Signing for Embedded Wallets

This example signs all inputs that belong to the user's embedded wallet:

```tsx theme={"system"}
import { useDynamicContext } from '@dynamic-labs/sdk-react-core';
import { isBitcoinWallet } from '@dynamic-labs/bitcoin';
import { EmbeddedWalletSignPsbtRequest } from '@dynamic-labs/bitcoin';

const MyComponent = () => {
  const { primaryWallet } = useDynamicContext();

  const handleSignPsbt = async (unsignedPsbtBase64: string) => {
    if (!primaryWallet || !isBitcoinWallet(primaryWallet)) {
      throw new Error('Bitcoin wallet not found');
    }

    try {
      // For embedded wallets, only unsignedPsbtBase64 is required
      // It signs all inputs belonging to the wallet, using the sighash type declared in the PSBT
      const request: EmbeddedWalletSignPsbtRequest = {
        unsignedPsbtBase64: unsignedPsbtBase64,
      };

      const { signedPsbt } = await primaryWallet.signPsbt(request);

      console.log('Signed PSBT (not finalized):', signedPsbt);
      // The PSBT is signed but not finalized - user should review and finalize
      return signedPsbt;
    } catch (error) {
      console.error('Error signing PSBT:', error);
      throw error;
    }
  };

  return (
    <button onClick={() => handleSignPsbt('your-unsigned-psbt-base64')}>
      Sign PSBT
    </button>
  );
};
```

### Complete Example with Review and Finalization

This example shows the complete flow: signing, reviewing, and finalizing a PSBT:

```tsx theme={"system"}
import { FC, useState } from 'react';
import { useDynamicContext } from '@dynamic-labs/sdk-react-core';
import { isBitcoinWallet } from '@dynamic-labs/bitcoin';
import { Psbt } from 'bitcoinjs-lib';
import { networks } from 'bitcoinjs-lib';

export const SignPsbtExample: FC = () => {
  const { primaryWallet } = useDynamicContext();
  const [unsignedPsbt, setUnsignedPsbt] = useState<string>('');
  const [signedPsbt, setSignedPsbt] = useState<string>('');
  const [error, setError] = useState<string>('');
  const [isLoading, setIsLoading] = useState<boolean>(false);

  const handleSignPsbt = async () => {
    if (!unsignedPsbt.trim()) {
      setError('Please enter an unsigned PSBT');
      return;
    }

    if (!primaryWallet || !isBitcoinWallet(primaryWallet)) {
      setError('Bitcoin wallet not found');
      return;
    }

    setIsLoading(true);
    setError('');
    setSignedPsbt('');

    try {
      // Sign the PSBT (does not finalize)
      // For embedded wallets, only unsignedPsbtBase64 is required
      const { signedPsbt: result } = await primaryWallet.signPsbt({
        unsignedPsbtBase64: unsignedPsbt.trim(),
      });

      setSignedPsbt(result);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to sign PSBT');
    } finally {
      setIsLoading(false);
    }
  };

  const handleReviewPsbt = () => {
    if (!signedPsbt) {
      setError('No signed PSBT to review');
      return;
    }

    try {
      // Parse the signed PSBT for review
      const psbt = Psbt.fromBase64(signedPsbt, { network: networks.bitcoin });

      // Extract transaction information
      const inputs = psbt.txInputs.map((input, index) => ({
        index,
        hash: input.hash.reverse().toString('hex'),
        index: input.index,
      }));

      const outputs = psbt.txOutputs.map((output, index) => ({
        index,
        address: output.address,
        value: output.value,
      }));

      console.log('PSBT Review:', {
        inputs,
        outputs,
        isFinalized: psbt.finalized,
      });

      // Show review UI to user
      alert(
        `PSBT Review:\nInputs: ${inputs.length}\nOutputs: ${outputs.length}\nFinalized: ${psbt.finalized}`,
      );
    } catch (err) {
      setError('Failed to parse PSBT for review');
    }
  };

  const handleFinalizePsbt = () => {
    if (!signedPsbt) {
      setError('No signed PSBT to finalize');
      return;
    }

    try {
      // Parse the signed PSBT
      const psbt = Psbt.fromBase64(signedPsbt, { network: networks.bitcoin });

      // Finalize all inputs
      psbt.finalizeAllInputs();

      // Extract the finalized transaction
      const finalizedTx = psbt.extractTransaction();

      // Get the transaction hex for broadcasting
      const txHex = finalizedTx.toHex();

      console.log('Finalized transaction hex:', txHex);
      alert(
        'PSBT finalized! Transaction hex: ' + txHex.substring(0, 50) + '...',
      );

      // The transaction can now be broadcast to the Bitcoin network
      return txHex;
    } catch (err) {
      setError(
        'Failed to finalize PSBT: ' +
          (err instanceof Error ? err.message : String(err)),
      );
    }
  };

  return (
    <div className='sign-psbt-example'>
      <h2>Sign PSBT</h2>

      <div>
        <label htmlFor='unsigned-psbt'>Unsigned PSBT (Base64):</label>
        <textarea
          id='unsigned-psbt'
          value={unsignedPsbt}
          onChange={(e) => setUnsignedPsbt(e.target.value)}
          placeholder='Enter unsigned PSBT in Base64 format'
          rows={5}
          style={{ width: '100%', margin: '10px 0' }}
        />
      </div>

      <button
        onClick={handleSignPsbt}
        disabled={isLoading || !unsignedPsbt.trim()}
        style={{ margin: '10px 0' }}
      >
        {isLoading ? 'Signing...' : 'Sign PSBT'}
      </button>

      {signedPsbt && (
        <div>
          <h3>Signed PSBT (Not Finalized)</h3>
          <textarea
            value={signedPsbt}
            readOnly
            rows={5}
            style={{ width: '100%', margin: '10px 0' }}
          />
          <div>
            <button onClick={handleReviewPsbt} style={{ margin: '10px 5px' }}>
              Review PSBT
            </button>
            <button onClick={handleFinalizePsbt} style={{ margin: '10px 5px' }}>
              Finalize PSBT
            </button>
          </div>
        </div>
      )}

      {error && <div style={{ color: 'red', margin: '10px 0' }}>{error}</div>}
    </div>
  );
};
```

## Co-signing P2WSH multisig inputs

To co-sign a P2WSH multisig, for example a 2-of-3 that holds the embedded wallet's key next to keys held elsewhere, pass the multisig's witness script in `allowedWitnessScripts`. This requires `@dynamic-labs/bitcoin` 5.14.0 or later.

`allowedWitnessScripts` is an array of hex-encoded witness scripts. The wallet signs a P2WSH input only when all of these hold:

* The input's 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.

```tsx theme={"system"}
const { signedPsbt } = await primaryWallet.signPsbt({
  unsignedPsbtBase64: psbt.toBase64(),
  allowedSighash: [0x01], // SIGHASH_ALL
  allowedWitnessScripts: [witnessScriptHex],
});
```

The returned PSBT carries this wallet's partial signature and is not finalized. Merge the signatures from the other co-signers and finalize it yourself before broadcasting.

Only a Native SegWit wallet can co-sign P2WSH inputs. A Taproot wallet never signs one, even when its script is listed. Build the multisig script with the wallet's compressed public key, which is the `publicKey` on the wallet's Native SegWit entry in `primaryWallet.additionalAddresses`.

An input that fails any check is not signed. If you also pass `signingIndexes` naming that input, the call throws `signingIndexes contains indexes that do not belong to the signing address`.

> [!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, and keep `allowedSighash` set to the types you expect.

## Signature Hash Types

Embedded wallets read the sighash type from the PSBT (BIP-174 `PSBT_IN_SIGHASH_TYPE`). An input that declares no type is signed with SIGHASH\_ALL (0x01), or SIGHASH\_DEFAULT (0x00) on a Taproot wallet.

Use `allowedSighash` to restrict which declared types the wallet accepts. The option can only reject a type, never select one. If an input declares a type outside the list, the call throws an error and signs nothing. An omitted or empty `allowedSighash` accepts any declared type.

> [!WARNING]
>
> `SIGHASH_NONE` produces a signature that does not commit to the transaction outputs, and `SIGHASH_SINGLE` commits to only one output index. Anyone holding the signed PSBT can change the remaining outputs before broadcasting. Set `allowedSighash` to the types your application expects, for example `[0x01, 0x81]` for `SIGHASH_ALL` and `SIGHASH_ALL | ANYONECANPAY`.

## Embedded Wallet Specific Notes

For embedded wallets:

1. **Simple default**: Only `unsignedPsbtBase64` is required:

   ```typescript theme={"system"}
   await wallet.signPsbt({
     unsignedPsbtBase64: psbt,
   });
   ```

2. **Input selection**: The method signs **all inputs** that belong to the wallet address. Pass `signingIndexes` to sign a subset.

3. **Sighash**: The type comes from the PSBT. Use `allowedSighash` to restrict which types are accepted.

4. **P2WSH multisig**: Inputs of this kind are signed only when you list their witness script in `allowedWitnessScripts`.

5. **No Signature Parameter**: The `signature` parameter is not supported (TypeScript will prevent you from using it).

6. **MFA Support**: The signing process may require MFA authentication if enabled.

## Error Handling

Common errors and how to handle them:

### Invalid PSBT Format

```typescript theme={"system"}
try {
  // For embedded wallets
  await wallet.signPsbt({
    unsignedPsbtBase64: 'invalid-psbt',
  });
} catch (error) {
  // Handle invalid PSBT format
  console.error('Invalid PSBT format:', error);
}
```

## Best Practices

1. **Always Review Before Finalization**: Parse and display the PSBT details to the user before finalization
2. **Validate PSBT Format**: Check that the PSBT is valid before attempting to sign
3. **Handle Errors Gracefully**: Wrap `signPsbt` calls in try-catch blocks
4. **Restrict What You Sign**: Set `allowedSighash` to the types you expect, and `signingIndexes` when you only want some inputs signed
5. **Don't Finalize Automatically**: Let the user review and finalize the PSBT manually
6. **Store Signed PSBTs Securely**: Signed PSBTs contain sensitive information, so handle them securely

## PSBT Workflow

The typical PSBT workflow is:

1. **Build PSBT**: Create an unsigned PSBT with transaction details
2. **Sign PSBT**: Use `signPsbt` to sign inputs belonging to the user
3. **Review PSBT**: Parse and display transaction details for user review
4. **Combine Signatures**: If multi-party, combine with other signatures
5. **Finalize PSBT**: Convert the PSBT to a finalized transaction
6. **Broadcast Transaction**: Send the finalized transaction to the Bitcoin network

## Additional Resources

* [Dynamic Labs Documentation](https://dynamic.xyz/docs)
* [BIP 174: Partially Signed Bitcoin Transaction Format](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki)
* [bitcoinjs-lib PSBT Documentation](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/psbt.ts)
* [Create a PSBT](https://docs.dynamic.xyz/docs/react/wallets/using-wallets/bitcoin/create-a-psbt.md) guide for testing
* [Sign a PSBT (External Wallets)](https://docs.dynamic.xyz/docs/react/wallets/using-wallets/bitcoin/sign-a-psbt.md) for external wallet flows


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