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.
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:
- Reviewed by the user to verify transaction details
- Combined with other signatures if it’s a multi-party transaction
- Finalized using a Bitcoin library (e.g.,
bitcoinjs-lib)
- 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:
-
Simplified Interface: Only
unsignedPsbtBase64 is required - no other parameters needed:
await wallet.signPsbt({
unsignedPsbtBase64: psbt,
});
-
Automatic Signing: The method automatically signs all inputs that belong to the wallet address. You cannot specify which inputs to sign.
-
Fixed SIGHASH: Embedded wallets always use SIGHASH_ALL (0x01) - this is not configurable.
-
No Signature Parameter: The
signature parameter is not supported (TypeScript will prevent you from using it).
-
MFA Support: The signing process may require MFA authentication if enabled.
Error Handling
Common errors and how to handle them:
try {
// For embedded wallets
await wallet.signPsbt({
unsignedPsbtBase64: 'invalid-psbt',
});
} catch (error) {
// Handle invalid PSBT format
console.error('Invalid PSBT format:', error);
}
Best Practices
- Always Review Before Finalization: Parse and display the PSBT details to the user before finalization
- Validate PSBT Format: Check that the PSBT is valid before attempting to sign
- Handle Errors Gracefully: Wrap
signPsbt calls in try-catch blocks
- Use Simplified Interface: Only provide
unsignedPsbtBase64 - no need for allowedSighash or signature
- Don’t Finalize Automatically: Let the user review and finalize the PSBT manually
- Store Signed PSBTs Securely: Signed PSBTs contain sensitive information - handle them securely
PSBT Workflow
The typical PSBT workflow is:
- Build PSBT: Create an unsigned PSBT with transaction details
- Sign PSBT: Use
signPsbt to sign inputs belonging to the user
- Review PSBT: Parse and display transaction details for user review
- Combine Signatures: If multi-party, combine with other signatures
- Finalize PSBT: Convert the PSBT to a finalized transaction
- Broadcast Transaction: Send the finalized transaction to the Bitcoin network
Additional Resources