Skip to content

Commit b81a5e5

Browse files
committed
refactor: update txExplanation for TxIntentMismatchError
Ticket: WP-6608
1 parent feed271 commit b81a5e5

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
@@ -3072,10 +3072,9 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
30723072
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
30733073
const { txParams, txPrebuild, wallet } = params;
30743074

3075-
const txExplanation = await this.getTxExplanation(txPrebuild);
3076-
30773075
// Helper to throw TxIntentMismatchRecipientError with recipient details
3078-
const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => {
3076+
const throwRecipientMismatch = async (message: string, mismatchedRecipients: Recipient[]): Promise<never> => {
3077+
const txExplanation = await this.getTxExplanation(txPrebuild);
30793078
throw new TxIntentMismatchRecipientError(
30803079
message,
30813080
undefined,
@@ -3114,12 +3113,13 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
31143113
const txJson = tx.toJson();
31153114
if (txJson.data === '0x') {
31163115
if (expectedAmount !== txJson.value) {
3117-
throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [
3118-
{ address: txJson.to, amount: txJson.value },
3119-
]);
3116+
await throwRecipientMismatch(
3117+
'the transaction amount in txPrebuild does not match the value given by client',
3118+
[{ address: txJson.to, amount: txJson.value }]
3119+
);
31203120
}
31213121
if (expectedDestination.toLowerCase() !== txJson.to.toLowerCase()) {
3122-
throwRecipientMismatch('destination address does not match with the recipient address', [
3122+
await throwRecipientMismatch('destination address does not match with the recipient address', [
31233123
{ address: txJson.to, amount: txJson.value },
31243124
]);
31253125
}
@@ -3149,13 +3149,14 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
31493149
}
31503150

31513151
if (expectedTokenAmount !== amount.toString()) {
3152-
throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [
3153-
{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() },
3154-
]);
3152+
await throwRecipientMismatch(
3153+
'the transaction amount in txPrebuild does not match the value given by client',
3154+
[{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() }]
3155+
);
31553156
}
31563157

31573158
if (expectedRecipientAddress !== addHexPrefix(recipientAddress.toString()).toLowerCase()) {
3158-
throwRecipientMismatch('destination address does not match with the recipient address', [
3159+
await throwRecipientMismatch('destination address does not match with the recipient address', [
31593160
{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() },
31603161
]);
31613162
}
@@ -3185,10 +3186,9 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
31853186
return this.verifyTssTransaction(params);
31863187
}
31873188

3188-
const txExplanation = await this.getTxExplanation(txPrebuild);
3189-
31903189
// Helper to throw TxIntentMismatchRecipientError with recipient details
3191-
const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => {
3190+
const throwRecipientMismatch = async (message: string, mismatchedRecipients: Recipient[]): Promise<never> => {
3191+
const txExplanation = await this.getTxExplanation(txPrebuild);
31923192
throw new TxIntentMismatchRecipientError(
31933193
message,
31943194
undefined,
@@ -3226,7 +3226,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
32263226
const expectedHopAddress = optionalDeps.ethUtil.stripHexPrefix(decodedHopTx.getSenderAddress().toString());
32273227
const actualHopAddress = optionalDeps.ethUtil.stripHexPrefix(txPrebuild.recipients[0].address);
32283228
if (expectedHopAddress.toLowerCase() !== actualHopAddress.toLowerCase()) {
3229-
throwRecipientMismatch('recipient address of txPrebuild does not match hop address', [
3229+
await throwRecipientMismatch('recipient address of txPrebuild does not match hop address', [
32303230
{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() },
32313231
]);
32323232
}
@@ -3246,17 +3246,18 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
32463246
if (txParams.tokenName) {
32473247
const expectedTotalAmount = new BigNumber(0);
32483248
if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
3249-
throwRecipientMismatch('batch token transaction amount in txPrebuild should be zero for token transfers', [
3250-
{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() },
3251-
]);
3249+
await throwRecipientMismatch(
3250+
'batch token transaction amount in txPrebuild should be zero for token transfers',
3251+
[{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() }]
3252+
);
32523253
}
32533254
} else {
32543255
let expectedTotalAmount = new BigNumber(0);
32553256
for (let i = 0; i < recipients.length; i++) {
32563257
expectedTotalAmount = expectedTotalAmount.plus(recipients[i].amount);
32573258
}
32583259
if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
3259-
throwRecipientMismatch(
3260+
await throwRecipientMismatch(
32603261
'batch transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client',
32613262
[{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() }]
32623263
);
@@ -3269,7 +3270,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
32693270
!batcherContractAddress ||
32703271
batcherContractAddress.toLowerCase() !== txPrebuild.recipients[0].address.toLowerCase()
32713272
) {
3272-
throwRecipientMismatch('recipient address of txPrebuild does not match batcher address', [
3273+
await throwRecipientMismatch('recipient address of txPrebuild does not match batcher address', [
32733274
{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() },
32743275
]);
32753276
}
@@ -3280,20 +3281,21 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
32803281
}
32813282
const expectedAmount = new BigNumber(recipients[0].amount);
32823283
if (!expectedAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
3283-
throwRecipientMismatch(
3284+
await throwRecipientMismatch(
32843285
'normal transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client',
32853286
[{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() }]
32863287
);
32873288
}
32883289
if (this.isETHAddress(recipients[0].address) && recipients[0].address !== txPrebuild.recipients[0].address) {
3289-
throwRecipientMismatch(
3290+
await throwRecipientMismatch(
32903291
'destination address in normal txPrebuild does not match that in txParams supplied by client',
32913292
[{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() }]
32923293
);
32933294
}
32943295
}
32953296
// Check coin is correct for all transaction types
32963297
if (!this.verifyCoin(txPrebuild)) {
3298+
const txExplanation = await this.getTxExplanation(txPrebuild);
32973299
throw new TxIntentMismatchError(
32983300
'coin in txPrebuild did not match that in txParams supplied by client',
32993301
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

@@ -619,12 +619,14 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
619619
async verifyTransaction<TNumber extends number | bigint = number>(
620620
params: VerifyTransactionOptions<TNumber>
621621
): Promise<boolean> {
622-
const txExplanation = await getTxExplanation(this, params.txPrebuild);
623-
624622
try {
625623
return await verifyTransaction(this, this.bitgo, params);
626624
} catch (error) {
627625
if (error instanceof AggregateValidationError) {
626+
const txExplanation = await TxIntentMismatchError.tryGetTxExplanation(
627+
this as unknown as IBaseCoin,
628+
params.txPrebuild
629+
);
628630
throw convertValidationErrorToTxIntentMismatch(
629631
error,
630632
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)