Skip to main content

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. The method signs only the inputs related to the user’s wallet and returns a signed (but not finalized) PSBT for review and finalization.

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
  • Does NOT finalize the PSBT - it remains partially signed for review
  • Returns a signed PSBT that can be reviewed, combined with other signatures, and finalized by the user

Method Signature

// From BitcoinWallet with embedded wallet (simplified interface)
const signedPsbt = await wallet.signPsbt(request: EmbeddedWalletSignPsbtRequest): Promise<BitcoinSignPsbtResponse>;

Type Definitions

For Embedded Wallets

type EmbeddedWalletSignPsbtRequest = {
  unsignedPsbtBase64: string; // The unsigned PSBT in Base64 format (only field required)
};

type BitcoinSignPsbtResponse = {
  signedPsbt: string; // The signed (but not finalized) PSBT in Base64 format
};
Important: Embedded wallets:
  • Only require unsignedPsbtBase64 - no other parameters needed
  • Always use SIGHASH_ALL (0x01) - not configurable for now
  • Automatically sign all inputs that belong to the wallet address
  • Do not support the signature parameter for now

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
The method automatically signs all inputs that belong to the user’s wallet address. You cannot specify which inputs to sign - it signs all of them automatically.

🔍 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:
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 automatically signs all inputs belonging to the wallet using SIGHASH_ALL
      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:
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>
  );
};

Signature Hash Types

Embedded wallets always use SIGHASH_ALL (0x01) - this is not configurable. You don’t need to (and cannot) specify allowedSighash for embedded wallets.

Embedded Wallet Specific Notes

For embedded wallets:
  1. Simplified Interface: Only unsignedPsbtBase64 is required - no other parameters needed:
    await wallet.signPsbt({
      unsignedPsbtBase64: psbt,
    });
    
  2. Automatic Signing: The method automatically signs all inputs that belong to the wallet address. You cannot specify which inputs to sign.
  3. Fixed SIGHASH: Embedded wallets always use SIGHASH_ALL (0x01) - this is not configurable.
  4. No Signature Parameter: The signature parameter is not supported (TypeScript will prevent you from using it).
  5. MFA Support: The signing process may require MFA authentication if enabled.

Error Handling

Common errors and how to handle them:

Invalid PSBT Format

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. Use Simplified Interface: Only provide unsignedPsbtBase64 - no need for allowedSighash or signature
  5. Don’t Finalize Automatically: Let the user review and finalize the PSBT manually
  6. Store Signed PSBTs Securely: Signed PSBTs contain sensitive information - 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