diff --git a/.changeset/tiny-beds-protect.md b/.changeset/tiny-beds-protect.md new file mode 100644 index 00000000..928d10b7 --- /dev/null +++ b/.changeset/tiny-beds-protect.md @@ -0,0 +1,5 @@ +--- +'@rgbpp-sdk/btc': minor +--- + +Report TxBuilder as extra context in the TxBuildError when the BTC Builder APIs fail diff --git a/packages/btc/README.md b/packages/btc/README.md index 788c0206..33d662ba 100644 --- a/packages/btc/README.md +++ b/packages/btc/README.md @@ -17,7 +17,7 @@ $ yarn add @rgbpp-sdk/btc $ pnpm add @rgbpp-sdk/btc ``` -## Transaction +## Transactions ### Transfer BTC from a `P2WPKH` address @@ -249,6 +249,22 @@ const psbt = await sendRbf({ }); ``` +## Errors + +### Visit context of the TxBuildError + +When you catch a `TxBuildError` error after calling the BTC Builder APIs (`sendBtc`, `sendUtxos`, etc), you can access the `e.context` object for error tracing, where it should contain a `tx` property that is a `TxBuilder` object: + +```typescript +try { + await sendBtc({ ... }); +} catch (e) { + if (e instanceof TxBuildError) { + console.log(e.context.tx); // TxBuilder object for error tracing + } +} +``` + ## Types ### Transaction diff --git a/packages/btc/src/api/sendUtxos.ts b/packages/btc/src/api/sendUtxos.ts index 4e225d93..a6a87600 100644 --- a/packages/btc/src/api/sendUtxos.ts +++ b/packages/btc/src/api/sendUtxos.ts @@ -3,6 +3,7 @@ import { DataSource } from '../query/source'; import { TxBuilder, InitOutput } from '../transaction/build'; import { BaseOutput, Utxo, prepareUtxoInputs } from '../transaction/utxo'; import { AddressToPubkeyMap, addAddressToPubkeyMap } from '../address'; +import { TxBuildError } from '../error'; export interface SendUtxosProps { inputs: Utxo[]; @@ -34,34 +35,43 @@ export async function createSendUtxosBuilder(props: SendUtxosProps): Promise<{ onlyConfirmedUtxos: props.onlyConfirmedUtxos, }); - // Prepare the UTXO inputs: - // 1. Fill pubkey for each P2TR UTXO, and throw if the corresponding pubkey is not found - // 2. Throw if unconfirmed UTXOs are found (if onlyConfirmedUtxos == true && skipInputsValidation == false) - const pubkeyMap = addAddressToPubkeyMap(props.pubkeyMap ?? {}, props.from, props.fromPubkey); - const inputs = await prepareUtxoInputs({ - utxos: props.inputs, - source: props.source, - requireConfirmed: props.onlyConfirmedUtxos && !props.skipInputsValidation, - requirePubkey: true, - pubkeyMap, - }); + try { + // Prepare the UTXO inputs: + // 1. Fill pubkey for each P2TR UTXO, and throw if the corresponding pubkey is not found + // 2. Throw if unconfirmed UTXOs are found (if onlyConfirmedUtxos == true && skipInputsValidation == false) + const pubkeyMap = addAddressToPubkeyMap(props.pubkeyMap ?? {}, props.from, props.fromPubkey); + const inputs = await prepareUtxoInputs({ + utxos: props.inputs, + source: props.source, + requireConfirmed: props.onlyConfirmedUtxos && !props.skipInputsValidation, + requirePubkey: true, + pubkeyMap, + }); - tx.addInputs(inputs); - tx.addOutputs(props.outputs); + tx.addInputs(inputs); + tx.addOutputs(props.outputs); - const paid = await tx.payFee({ - address: props.from, - publicKey: pubkeyMap[props.from], - changeAddress: props.changeAddress, - excludeUtxos: props.excludeUtxos, - }); + const paid = await tx.payFee({ + address: props.from, + publicKey: pubkeyMap[props.from], + changeAddress: props.changeAddress, + excludeUtxos: props.excludeUtxos, + }); + + return { + builder: tx, + fee: paid.fee, + feeRate: paid.feeRate, + changeIndex: paid.changeIndex, + }; + } catch (e) { + // When caught TxBuildError, add TxBuilder as the context + if (e instanceof TxBuildError) { + e.setContext({ tx }); + } - return { - builder: tx, - fee: paid.fee, - feeRate: paid.feeRate, - changeIndex: paid.changeIndex, - }; + throw e; + } } export async function sendUtxos(props: SendUtxosProps): Promise { diff --git a/packages/btc/src/error.ts b/packages/btc/src/error.ts index 8562ac09..51e86e7a 100644 --- a/packages/btc/src/error.ts +++ b/packages/btc/src/error.ts @@ -1,3 +1,5 @@ +import { TxBuilder } from './transaction/build'; + export enum ErrorCodes { UNKNOWN, @@ -30,7 +32,8 @@ export enum ErrorCodes { export const ErrorMessages = { [ErrorCodes.UNKNOWN]: 'Unknown error', - [ErrorCodes.MISSING_PUBKEY]: 'Missing a pubkey that pairs with the address', + [ErrorCodes.MISSING_PUBKEY]: + 'Missing a pubkey that pairs with the address, it is required for the P2TR UTXO included in the transaction', [ErrorCodes.CANNOT_FIND_UTXO]: 'Cannot find the UTXO, it may not exist or is not live', [ErrorCodes.UNCONFIRMED_UTXO]: 'Unconfirmed UTXO', [ErrorCodes.INSUFFICIENT_UTXO]: 'Insufficient UTXO to construct the transaction', @@ -56,16 +59,27 @@ export const ErrorMessages = { [ErrorCodes.MEMPOOL_API_RESPONSE_ERROR]: 'Mempool.space API returned an error', }; +export interface TxBuildErrorContext { + tx?: TxBuilder; +} + export class TxBuildError extends Error { public code = ErrorCodes.UNKNOWN; - constructor(code: ErrorCodes, message = ErrorMessages[code] || 'Unknown error') { + public context?: TxBuildErrorContext; + + constructor(code: ErrorCodes, message = ErrorMessages[code] || 'Unknown error', context?: TxBuildErrorContext) { super(message); this.code = code; + this.context = context; Object.setPrototypeOf(this, TxBuildError.prototype); } - static withComment(code: ErrorCodes, comment?: string): TxBuildError { + static withComment(code: ErrorCodes, comment?: string, context?: TxBuildErrorContext): TxBuildError { const message: string | undefined = ErrorMessages[code]; - return new TxBuildError(code, comment ? `${message}: ${comment}` : message); + return new TxBuildError(code, comment ? `${message}: ${comment}` : message, context); + } + + setContext(context: TxBuildErrorContext) { + this.context = context; } } diff --git a/packages/btc/tests/Transaction.test.ts b/packages/btc/tests/Transaction.test.ts index b4fcd4c1..a5c9ed8a 100644 --- a/packages/btc/tests/Transaction.test.ts +++ b/packages/btc/tests/Transaction.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { accounts, config, network, service, source } from './shared/env'; import { expectPsbtFeeInRange, signAndBroadcastPsbt, waitFor } from './shared/utils'; -import { bitcoin, ErrorMessages, ErrorCodes, AddressType } from '../src'; +import { bitcoin, ErrorMessages, ErrorCodes, AddressType, TxBuilder, TxBuildError } from '../src'; import { createSendUtxosBuilder, createSendBtcBuilder, sendBtc, sendUtxos, sendRbf, tweakSigner } from '../src'; const STATIC_FEE_RATE = 1; @@ -149,6 +149,25 @@ describe('Transaction', () => { // const res = await service.sendBtcTransaction(tx.toHex()); // console.log(`explorer: https://mempool.space/testnet/tx/${res.txid}`); }); + it('Try insufficient-balance transfer, and check error.context', async () => { + try { + await createSendBtcBuilder({ + from: accounts.charlie.p2wpkh.address, + tos: [ + { + address: accounts.charlie.p2wpkh.address, + value: 1_0000_0000_0000, + }, + ], + feeRate: STATIC_FEE_RATE, + source, + }); + } catch (e) { + expect(e).toBeInstanceOf(TxBuildError); + expect(e.context).toBeDefined(); + expect(e.context.tx).toBeInstanceOf(TxBuilder); + } + }); }); describe('sendUtxos()', () => {