Skip to content

Commit f7a1cba

Browse files
committed
refactor: update txExplanation for TxIntentMismatchError
Ticket: WP-6608
1 parent 418878d commit f7a1cba

File tree

8 files changed

+150
-113
lines changed

8 files changed

+150
-113
lines changed

modules/abstract-eth/src/abstractEthLikeNewCoins.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2839,10 +2839,9 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
28392839
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
28402840
const { txParams, txPrebuild, wallet } = params;
28412841

2842-
const txExplanation = await this.getTxExplanation(txPrebuild);
2843-
28442842
// Helper to throw TxIntentMismatchRecipientError with recipient details
2845-
const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => {
2843+
const throwRecipientMismatch = async (message: string, mismatchedRecipients: Recipient[]): Promise<never> => {
2844+
const txExplanation = await this.getTxExplanation(txPrebuild);
28462845
throw new TxIntentMismatchRecipientError(
28472846
message,
28482847
undefined,
@@ -2881,12 +2880,13 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
28812880
const txJson = tx.toJson();
28822881
if (txJson.data === '0x') {
28832882
if (expectedAmount !== txJson.value) {
2884-
throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [
2885-
{ address: txJson.to, amount: txJson.value },
2886-
]);
2883+
await throwRecipientMismatch(
2884+
'the transaction amount in txPrebuild does not match the value given by client',
2885+
[{ address: txJson.to, amount: txJson.value }]
2886+
);
28872887
}
28882888
if (expectedDestination.toLowerCase() !== txJson.to.toLowerCase()) {
2889-
throwRecipientMismatch('destination address does not match with the recipient address', [
2889+
await throwRecipientMismatch('destination address does not match with the recipient address', [
28902890
{ address: txJson.to, amount: txJson.value },
28912891
]);
28922892
}
@@ -2916,13 +2916,14 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
29162916
}
29172917

29182918
if (expectedTokenAmount !== amount.toString()) {
2919-
throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [
2920-
{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() },
2921-
]);
2919+
await throwRecipientMismatch(
2920+
'the transaction amount in txPrebuild does not match the value given by client',
2921+
[{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() }]
2922+
);
29222923
}
29232924

29242925
if (expectedRecipientAddress !== addHexPrefix(recipientAddress.toString()).toLowerCase()) {
2925-
throwRecipientMismatch('destination address does not match with the recipient address', [
2926+
await throwRecipientMismatch('destination address does not match with the recipient address', [
29262927
{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() },
29272928
]);
29282929
}
@@ -2952,10 +2953,9 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
29522953
return this.verifyTssTransaction(params);
29532954
}
29542955

2955-
const txExplanation = await this.getTxExplanation(txPrebuild);
2956-
29572956
// Helper to throw TxIntentMismatchRecipientError with recipient details
2958-
const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => {
2957+
const throwRecipientMismatch = async (message: string, mismatchedRecipients: Recipient[]): Promise<never> => {
2958+
const txExplanation = await this.getTxExplanation(txPrebuild);
29592959
throw new TxIntentMismatchRecipientError(
29602960
message,
29612961
undefined,
@@ -2993,7 +2993,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
29932993
const expectedHopAddress = optionalDeps.ethUtil.stripHexPrefix(decodedHopTx.getSenderAddress().toString());
29942994
const actualHopAddress = optionalDeps.ethUtil.stripHexPrefix(txPrebuild.recipients[0].address);
29952995
if (expectedHopAddress.toLowerCase() !== actualHopAddress.toLowerCase()) {
2996-
throwRecipientMismatch('recipient address of txPrebuild does not match hop address', [
2996+
await throwRecipientMismatch('recipient address of txPrebuild does not match hop address', [
29972997
{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() },
29982998
]);
29992999
}
@@ -3013,17 +3013,18 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
30133013
if (txParams.tokenName) {
30143014
const expectedTotalAmount = new BigNumber(0);
30153015
if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
3016-
throwRecipientMismatch('batch token transaction amount in txPrebuild should be zero for token transfers', [
3017-
{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() },
3018-
]);
3016+
await throwRecipientMismatch(
3017+
'batch token transaction amount in txPrebuild should be zero for token transfers',
3018+
[{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() }]
3019+
);
30193020
}
30203021
} else {
30213022
let expectedTotalAmount = new BigNumber(0);
30223023
for (let i = 0; i < recipients.length; i++) {
30233024
expectedTotalAmount = expectedTotalAmount.plus(recipients[i].amount);
30243025
}
30253026
if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
3026-
throwRecipientMismatch(
3027+
await throwRecipientMismatch(
30273028
'batch transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client',
30283029
[{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() }]
30293030
);
@@ -3036,7 +3037,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
30363037
!batcherContractAddress ||
30373038
batcherContractAddress.toLowerCase() !== txPrebuild.recipients[0].address.toLowerCase()
30383039
) {
3039-
throwRecipientMismatch('recipient address of txPrebuild does not match batcher address', [
3040+
await throwRecipientMismatch('recipient address of txPrebuild does not match batcher address', [
30403041
{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() },
30413042
]);
30423043
}
@@ -3047,20 +3048,21 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
30473048
}
30483049
const expectedAmount = new BigNumber(recipients[0].amount);
30493050
if (!expectedAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
3050-
throwRecipientMismatch(
3051+
await throwRecipientMismatch(
30513052
'normal transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client',
30523053
[{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() }]
30533054
);
30543055
}
30553056
if (this.isETHAddress(recipients[0].address) && recipients[0].address !== txPrebuild.recipients[0].address) {
3056-
throwRecipientMismatch(
3057+
await throwRecipientMismatch(
30573058
'destination address in normal txPrebuild does not match that in txParams supplied by client',
30583059
[{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() }]
30593060
);
30603061
}
30613062
}
30623063
// Check coin is correct for all transaction types
30633064
if (!this.verifyCoin(txPrebuild)) {
3065+
const txExplanation = await this.getTxExplanation(txPrebuild);
30643066
throw new TxIntentMismatchError(
30653067
'coin in txPrebuild did not match that in txParams supplied by client',
30663068
undefined,

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
PresignTransactionOptions,
2929
RequestTracer,
3030
SignedTransaction,
31+
TxIntentMismatchError,
3132
SignTransactionOptions as BaseSignTransactionOptions,
3233
SupplementGenerateWalletOptions,
3334
TransactionParams as BaseTransactionParams,
@@ -61,7 +62,6 @@ import {
6162
assertValidTransactionRecipient,
6263
explainTx,
6364
fromExtendedAddressFormat,
64-
getTxExplanation,
6565
isScriptRecipient,
6666
parseTransaction,
6767
verifyTransaction,
@@ -145,7 +145,7 @@ function convertValidationErrorToTxIntentMismatch(
145145
reqId: string | IRequestTracer | undefined,
146146
txParams: BaseTransactionParams,
147147
txHex: string | undefined,
148-
txExplanation?: string
148+
txExplanation?: unknown
149149
): TxIntentMismatchRecipientError {
150150
const mismatchedRecipients: MismatchedRecipient[] = [];
151151

@@ -618,12 +618,14 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
618618
async verifyTransaction<TNumber extends number | bigint = number>(
619619
params: VerifyTransactionOptions<TNumber>
620620
): Promise<boolean> {
621-
const txExplanation = await getTxExplanation(this, params.txPrebuild);
622-
623621
try {
624622
return await verifyTransaction(this, this.bitgo, params);
625623
} catch (error) {
626624
if (error instanceof AggregateValidationError) {
625+
const txExplanation = await TxIntentMismatchError.tryGetTxExplanation(
626+
this as unknown as IBaseCoin,
627+
params.txPrebuild
628+
);
627629
throw convertValidationErrorToTxIntentMismatch(
628630
error,
629631
params.reqId,

modules/abstract-utxo/src/transaction/descriptor/verifyTransaction.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import * as utxolib from '@bitgo/utxo-lib';
2-
import { ITransactionRecipient, TxIntentMismatchError } from '@bitgo/sdk-core';
2+
import { ITransactionRecipient, TxIntentMismatchError, IBaseCoin } from '@bitgo/sdk-core';
33
import { DescriptorMap } from '@bitgo/utxo-core/descriptor';
44

55
import { AbstractUtxoCoin, VerifyTransactionOptions } from '../../abstractUtxoCoin';
66
import { BaseOutput, BaseParsedTransactionOutputs } from '../types';
7-
import { getTxExplanation } from '../txExplanation';
87

98
import { toBaseParsedTransactionOutputsFromPsbt } from './parse';
109

@@ -76,10 +75,12 @@ export async function verifyTransaction<TNumber extends number | bigint>(
7675
params: VerifyTransactionOptions<TNumber>,
7776
descriptorMap: DescriptorMap
7877
): Promise<boolean> {
79-
const txExplanation = await getTxExplanation(coin, params.txPrebuild);
80-
8178
const tx = coin.decodeTransactionFromPrebuild(params.txPrebuild);
8279
if (!(tx instanceof utxolib.bitgo.UtxoPsbt)) {
80+
const txExplanation = await TxIntentMismatchError.tryGetTxExplanation(
81+
coin as unknown as IBaseCoin,
82+
params.txPrebuild
83+
);
8384
throw new TxIntentMismatchError(
8485
'unexpected transaction type',
8586
params.reqId,

modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import buildDebug from 'debug';
22
import _ from 'lodash';
33
import BigNumber from 'bignumber.js';
4-
import { BitGoBase, TxIntentMismatchError } from '@bitgo/sdk-core';
4+
import { BitGoBase, TxIntentMismatchError, IBaseCoin } from '@bitgo/sdk-core';
55
import * as utxolib from '@bitgo/utxo-lib';
66

77
import { AbstractUtxoCoin, VerifyTransactionOptions } from '../../abstractUtxoCoin';
88
import { Output, ParsedTransaction } from '../types';
99
import { verifyCustomChangeKeySignatures, verifyKeySignature, verifyUserPublicKey } from '../../verifyKey';
1010
import { getPsbtTxInputs, getTxInputs } from '../fetchInputs';
11-
import { getTxExplanation } from '../txExplanation';
1211

1312
const debug = buildDebug('bitgo:abstract-utxo:verifyTransaction');
1413

@@ -51,7 +50,7 @@ export async function verifyTransaction<TNumber extends bigint | number>(
5150
): Promise<boolean> {
5251
const { txParams, txPrebuild, wallet, verification = {}, reqId } = params;
5352

54-
const txExplanation = await getTxExplanation(coin, txPrebuild);
53+
const txExplanation = await TxIntentMismatchError.tryGetTxExplanation(coin as unknown as IBaseCoin, txPrebuild);
5554

5655
// Helper to throw TxIntentMismatchError with consistent context
5756
const throwTxMismatch = (message: string): never => {

modules/abstract-utxo/src/transaction/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,5 @@ export * from './recipient';
33
export { explainTx } from './explainTransaction';
44
export { parseTransaction } from './parseTransaction';
55
export { verifyTransaction } from './verifyTransaction';
6-
export { getTxExplanation } from './txExplanation';
76
export * from './fetchInputs';
87
export * as bip322 from './bip322';

modules/abstract-utxo/src/transaction/txExplanation.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

modules/sdk-core/src/bitgo/errors.ts

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { BitGoJsError } from '../bitgojsError';
44
import { IRequestTracer } from '../api/types';
55
import { TransactionParams } from './baseCoin';
6+
import { IBaseCoin } from './baseCoin/iBaseCoin';
67
import { SendManyOptions } from './wallet';
78

89
// re-export for backwards compat
@@ -271,20 +272,58 @@ export class TxIntentMismatchError extends BitGoJsError {
271272
* @param {string | IRequestTracer | undefined} id - Transaction ID or request tracer
272273
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
273274
* @param {string | undefined} txHex - Raw transaction hex string
274-
* @param {string | undefined} txExplanation - Stringified transaction explanation
275+
* @param {unknown} txExplanation - Transaction explanation
275276
*/
276277
public constructor(
277278
message: string,
278279
id: string | IRequestTracer | undefined,
279280
txParams: TransactionParams[],
280281
txHex: string | undefined,
281-
txExplanation?: string
282+
txExplanation?: unknown
282283
) {
283284
super(message);
284285
this.id = id;
285286
this.txParams = txParams;
286287
this.txHex = txHex;
287-
this.txExplanation = txExplanation;
288+
this.txExplanation = txExplanation ? this.safeStringify(txExplanation) : undefined;
289+
}
290+
291+
/**
292+
* Safely stringify a value with BigInt support
293+
* @param value - Value to stringify
294+
* @returns JSON string with BigInts converted to strings
295+
*/
296+
private safeStringify(value: unknown): string {
297+
return JSON.stringify(value, (_, v) => (typeof v === 'bigint' ? v.toString() : v), 2);
298+
}
299+
300+
/**
301+
* Try to get transaction explanation from a coin's explainTransaction method.
302+
*
303+
* @param coin - Coin instance implementing IBaseCoin
304+
* @param txPrebuild - Transaction prebuild containing txHex and txInfo
305+
* @returns Transaction explanation object or undefined
306+
*/
307+
static async tryGetTxExplanation(
308+
coin: IBaseCoin,
309+
txPrebuild: { txHex?: string; txInfo?: unknown }
310+
): Promise<unknown> {
311+
if (!txPrebuild.txHex) {
312+
return undefined;
313+
}
314+
315+
try {
316+
return await coin.explainTransaction({
317+
txHex: txPrebuild.txHex,
318+
txInfo: txPrebuild.txInfo,
319+
});
320+
} catch (e) {
321+
return {
322+
error: 'Failed to parse transaction explanation',
323+
txHex: txPrebuild.txHex,
324+
details: e instanceof Error ? e.message : String(e),
325+
};
326+
}
288327
}
289328
}
290329

@@ -309,15 +348,15 @@ export class TxIntentMismatchRecipientError extends TxIntentMismatchError {
309348
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
310349
* @param {string | undefined} txHex - Raw transaction hex string
311350
* @param {MismatchedRecipient[]} mismatchedRecipients - Array of recipients that don't match user intent
312-
* @param {string | undefined} txExplanation - Stringified transaction explanation
351+
* @param {unknown} txExplanation - Transaction explanation
313352
*/
314353
public constructor(
315354
message: string,
316355
id: string | IRequestTracer | undefined,
317356
txParams: TransactionParams[],
318357
txHex: string | undefined,
319358
mismatchedRecipients: MismatchedRecipient[],
320-
txExplanation?: string
359+
txExplanation?: unknown
321360
) {
322361
super(message, id, txParams, txHex, txExplanation);
323362
this.mismatchedRecipients = mismatchedRecipients;
@@ -345,15 +384,15 @@ export class TxIntentMismatchContractError extends TxIntentMismatchError {
345384
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
346385
* @param {string | undefined} txHex - Raw transaction hex string
347386
* @param {ContractDataPayload} mismatchedDataPayload - The contract interaction data that doesn't match user intent
348-
* @param {string | undefined} txExplanation - Stringified transaction explanation
387+
* @param {unknown} txExplanation - Transaction explanation
349388
*/
350389
public constructor(
351390
message: string,
352391
id: string | IRequestTracer | undefined,
353392
txParams: TransactionParams[],
354393
txHex: string | undefined,
355394
mismatchedDataPayload: ContractDataPayload,
356-
txExplanation?: string
395+
txExplanation?: unknown
357396
) {
358397
super(message, id, txParams, txHex, txExplanation);
359398
this.mismatchedDataPayload = mismatchedDataPayload;
@@ -381,15 +420,15 @@ export class TxIntentMismatchApprovalError extends TxIntentMismatchError {
381420
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
382421
* @param {string | undefined} txHex - Raw transaction hex string
383422
* @param {TokenApproval} tokenApproval - Details of the token approval that doesn't match user intent
384-
* @param {string | undefined} txExplanation - Stringified transaction explanation
423+
* @param {unknown} txExplanation - Transaction explanation
385424
*/
386425
public constructor(
387426
message: string,
388427
id: string | IRequestTracer | undefined,
389428
txParams: TransactionParams[],
390429
txHex: string | undefined,
391430
tokenApproval: TokenApproval,
392-
txExplanation?: string
431+
txExplanation?: unknown
393432
) {
394433
super(message, id, txParams, txHex, txExplanation);
395434
this.tokenApproval = tokenApproval;

0 commit comments

Comments
 (0)