Skip to content

Commit a3513a3

Browse files
committed
feat: refactoring error handling for transaction verification
Ticket: WP-6189
1 parent 3a9d500 commit a3513a3

File tree

2 files changed

+73
-68
lines changed

2 files changed

+73
-68
lines changed

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
IWallet,
2323
KeychainsTriplet,
2424
KeyIndices,
25+
MismatchedRecipient,
2526
MultisigType,
2627
multisigTypes,
2728
P2shP2wshUnsupportedError,
@@ -39,6 +40,7 @@ import {
3940
TransactionParams as BaseTransactionParams,
4041
TransactionPrebuild as BaseTransactionPrebuild,
4142
Triple,
43+
TxIntentMismatchRecipientError,
4244
UnexpectedAddressError,
4345
UnsupportedAddressTypeError,
4446
VerificationOptions,
@@ -72,6 +74,11 @@ import {
7274
parseTransaction,
7375
verifyTransaction,
7476
} from './transaction';
77+
import {
78+
AggregateValidationError,
79+
ErrorMissingOutputs,
80+
ErrorImplicitExternalOutputs,
81+
} from './transaction/descriptor/verifyTransaction';
7582
import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptorWallet } from './descriptor';
7683
import { getChainFromNetwork, getFamilyFromNetwork, getFullNameFromNetwork } from './names';
7784
import { CustomChangeOptions } from './transaction/fixedScript';
@@ -113,6 +120,52 @@ const { getExternalChainCode, isChainCode, scriptTypeForChain, outputScripts } =
113120

114121
type Unspent<TNumber extends number | bigint = number> = bitgo.Unspent<TNumber>;
115122

123+
/**
124+
* Convert ValidationError to TxIntentMismatchRecipientError with structured data
125+
*
126+
* This preserves the structured error information from the original ValidationError
127+
* by extracting the mismatched outputs and converting them to the standardized format.
128+
* The original error is preserved as the `cause` for debugging purposes.
129+
*/
130+
function convertValidationErrorToTxIntentMismatch(
131+
error: AggregateValidationError,
132+
reqId: string | IRequestTracer | undefined,
133+
txParams: BaseTransactionParams,
134+
txHex: string | undefined
135+
): TxIntentMismatchRecipientError {
136+
const mismatchedRecipients: MismatchedRecipient[] = [];
137+
138+
for (const err of error.errors) {
139+
if (err instanceof ErrorMissingOutputs) {
140+
mismatchedRecipients.push(
141+
...err.missingOutputs.map((output) => ({
142+
address: output.address,
143+
amount: output.amount.toString(),
144+
}))
145+
);
146+
} else if (err instanceof ErrorImplicitExternalOutputs) {
147+
mismatchedRecipients.push(
148+
...err.implicitExternalOutputs.map((output) => ({
149+
address: output.address,
150+
amount: output.amount.toString(),
151+
}))
152+
);
153+
}
154+
}
155+
156+
const txIntentError = new TxIntentMismatchRecipientError(
157+
error.message,
158+
reqId,
159+
[txParams],
160+
txHex,
161+
mismatchedRecipients
162+
);
163+
// Preserve the original structured error as the cause for debugging
164+
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
165+
(txIntentError as Error & { cause?: Error }).cause = error;
166+
return txIntentError;
167+
}
168+
116169
export type DecodedTransaction<TNumber extends number | bigint> =
117170
| utxolib.bitgo.UtxoTransaction<TNumber>
118171
| utxolib.bitgo.UtxoPsbt;
@@ -631,13 +684,21 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
631684
* @param params.verification.disableNetworking Disallow fetching any data from the internet for verification purposes
632685
* @param params.verification.keychains Pass keychains manually rather than fetching them by id
633686
* @param params.verification.addresses Address details to pass in for out-of-band verification
634-
* @returns {boolean}
687+
* @returns {boolean} True if verification passes
635688
* @throws {TxIntentMismatchError} if transaction validation fails
689+
* @throws {TxIntentMismatchRecipientError} if transaction recipients don't match user intent
636690
*/
637691
async verifyTransaction<TNumber extends number | bigint = number>(
638692
params: VerifyTransactionOptions<TNumber>
639693
): Promise<boolean> {
640-
return verifyTransaction(this, this.bitgo, params);
694+
try {
695+
return await verifyTransaction(this, this.bitgo, params);
696+
} catch (error) {
697+
if (error instanceof AggregateValidationError) {
698+
throw convertValidationErrorToTxIntentMismatch(error, params.reqId, params.txParams, params.txPrebuild.txHex);
699+
}
700+
throw error;
701+
}
641702
}
642703

643704
/**
Lines changed: 10 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import * as utxolib from '@bitgo/utxo-lib';
2-
import {
3-
IRequestTracer,
4-
ITransactionRecipient,
5-
MismatchedRecipient,
6-
TxIntentMismatchError,
7-
TxIntentMismatchRecipientError,
8-
VerifyTransactionOptions,
9-
} from '@bitgo/sdk-core';
2+
import { ITransactionRecipient, TxIntentMismatchError } from '@bitgo/sdk-core';
103
import { DescriptorMap } from '@bitgo/utxo-core/descriptor';
114

12-
import { AbstractUtxoCoin, BaseOutput, BaseParsedTransactionOutputs } from '../../abstractUtxoCoin';
5+
import {
6+
AbstractUtxoCoin,
7+
BaseOutput,
8+
BaseParsedTransactionOutputs,
9+
VerifyTransactionOptions,
10+
} from '../../abstractUtxoCoin';
1311

1412
import { toBaseParsedTransactionOutputsFromPsbt } from './parse';
1513

@@ -64,52 +62,6 @@ export function assertValidTransaction(
6462
assertExpectedOutputDifference(toBaseParsedTransactionOutputsFromPsbt(psbt, descriptors, recipients, network));
6563
}
6664

67-
/**
68-
* Convert ValidationError to TxIntentMismatchRecipientError with structured data
69-
*
70-
* This preserves the structured error information from the original ValidationError
71-
* by extracting the mismatched outputs and converting them to the standardized format.
72-
* The original error is preserved as the `cause` for debugging purposes.
73-
*/
74-
function convertValidationErrorToTxIntentMismatch(
75-
error: AggregateValidationError,
76-
reqId: string | IRequestTracer | undefined,
77-
txParams: VerifyTransactionOptions['txParams'],
78-
txHex: string | undefined
79-
): TxIntentMismatchRecipientError {
80-
const mismatchedRecipients: MismatchedRecipient[] = [];
81-
82-
for (const err of error.errors) {
83-
if (err instanceof ErrorMissingOutputs) {
84-
mismatchedRecipients.push(
85-
...err.missingOutputs.map((output) => ({
86-
address: output.address,
87-
amount: output.amount.toString(),
88-
}))
89-
);
90-
} else if (err instanceof ErrorImplicitExternalOutputs) {
91-
mismatchedRecipients.push(
92-
...err.implicitExternalOutputs.map((output) => ({
93-
address: output.address,
94-
amount: output.amount.toString(),
95-
}))
96-
);
97-
}
98-
}
99-
100-
const txIntentError = new TxIntentMismatchRecipientError(
101-
error.message,
102-
reqId,
103-
[txParams],
104-
txHex,
105-
mismatchedRecipients
106-
);
107-
// Preserve the original structured error as the cause for debugging
108-
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
109-
(txIntentError as Error & { cause?: Error }).cause = error;
110-
return txIntentError;
111-
}
112-
11365
/**
11466
* Wrapper around assertValidTransaction that returns a boolean instead of throwing.
11567
*
@@ -121,11 +73,10 @@ function convertValidationErrorToTxIntentMismatch(
12173
* @param descriptorMap
12274
* @returns {boolean} True if verification passes
12375
* @throws {TxIntentMismatchError} if transaction validation fails
124-
* @throws {TxIntentMismatchRecipientError} if transaction recipients don't match user intent
12576
*/
126-
export async function verifyTransaction(
77+
export async function verifyTransaction<TNumber extends number | bigint>(
12778
coin: AbstractUtxoCoin,
128-
params: VerifyTransactionOptions,
79+
params: VerifyTransactionOptions<TNumber>,
12980
descriptorMap: DescriptorMap
13081
): Promise<boolean> {
13182
const tx = coin.decodeTransactionFromPrebuild(params.txPrebuild);
@@ -138,14 +89,7 @@ export async function verifyTransaction(
13889
);
13990
}
14091

141-
try {
142-
assertValidTransaction(tx, descriptorMap, params.txParams.recipients ?? [], tx.network);
143-
} catch (error) {
144-
if (error instanceof AggregateValidationError) {
145-
throw convertValidationErrorToTxIntentMismatch(error, params.reqId, params.txParams, params.txPrebuild.txHex);
146-
}
147-
throw error;
148-
}
92+
assertValidTransaction(tx, descriptorMap, params.txParams.recipients ?? [], tx.network);
14993

15094
return true;
15195
}

0 commit comments

Comments
 (0)