Skip to content

Commit

Permalink
feat: report TxBuilder as extra context in the TxBuildError when the …
Browse files Browse the repository at this point in the history
…BTC Builder APIs fail
  • Loading branch information
ShookLyngs committed Aug 8, 2024
1 parent f2a0b65 commit a9a787d
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .changeset/tiny-beds-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rgbpp-sdk/btc': minor
---

Report TxBuilder as extra context in the TxBuildError when the BTC Builder APIs fail
60 changes: 35 additions & 25 deletions packages/btc/src/api/sendUtxos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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<bitcoin.Psbt> {
Expand Down
19 changes: 16 additions & 3 deletions packages/btc/src/error.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TxBuilder } from './transaction/build';

export enum ErrorCodes {
UNKNOWN,

Expand Down Expand Up @@ -56,16 +58,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;
}
}
21 changes: 20 additions & 1 deletion packages/btc/tests/Transaction.test.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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()', () => {
Expand Down

0 comments on commit a9a787d

Please sign in to comment.