Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(btc): report extra context in the TxBuildError #272

Merged
merged 3 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
22 changes: 18 additions & 4 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 @@ -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',
Expand All @@ -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;
}
}
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