Skip to content

Commit

Permalink
Merge pull request #272 from ckb-cell/feat/tx-build-error-context
Browse files Browse the repository at this point in the history
feat(btc): report extra context in the TxBuildError
  • Loading branch information
Flouse authored Aug 8, 2024
2 parents f2a0b65 + ab41f6f commit e24c526
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 31 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
18 changes: 17 additions & 1 deletion packages/btc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ $ yarn add @rgbpp-sdk/btc
$ pnpm add @rgbpp-sdk/btc
```

## Transaction
## Transactions

### Transfer BTC from a `P2WPKH` address

Expand Down Expand Up @@ -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
Expand Down
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

1 comment on commit e24c526

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New snapshot version of the rgbpp-sdk packages have been released:

Name Version
@rgbpp-sdk/btc 0.0.0-snap-20240808073209
@rgbpp-sdk/ckb 0.0.0-snap-20240808073209
rgbpp 0.0.0-snap-20240808073209
@rgbpp-sdk/service 0.0.0-snap-20240808073209

Please sign in to comment.