Skip to content

Commit feed271

Browse files
committed
feat: add tx explanation for TxIntentMismatch errors
Ticket: WP-6608
1 parent 78e280a commit feed271

File tree

8 files changed

+284
-18
lines changed

8 files changed

+284
-18
lines changed

modules/abstract-eth/src/abstractEthLikeNewCoins.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3031,6 +3031,34 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
30313031
return txPrebuild.coin === nativeCoin;
30323032
}
30333033

3034+
/**
3035+
* Generate transaction explanation for error reporting
3036+
* @param txPrebuild - Transaction prebuild containing txHex and fee info
3037+
* @returns Stringified JSON explanation
3038+
*/
3039+
private async getTxExplanation(txPrebuild?: TransactionPrebuild): Promise<string | undefined> {
3040+
if (!txPrebuild?.txHex || !txPrebuild?.gasPrice) {
3041+
return undefined;
3042+
}
3043+
3044+
try {
3045+
const explanation = await this.explainTransaction({
3046+
txHex: txPrebuild.txHex,
3047+
feeInfo: {
3048+
fee: txPrebuild.gasPrice.toString(),
3049+
},
3050+
});
3051+
return JSON.stringify(explanation, null, 2);
3052+
} catch (e) {
3053+
const errorDetails = {
3054+
error: 'Failed to parse transaction explanation',
3055+
txHex: txPrebuild.txHex,
3056+
details: e instanceof Error ? e.message : String(e),
3057+
};
3058+
return JSON.stringify(errorDetails, null, 2);
3059+
}
3060+
}
3061+
30343062
/**
30353063
* Verify if a tss transaction is valid
30363064
*
@@ -3044,9 +3072,18 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
30443072
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
30453073
const { txParams, txPrebuild, wallet } = params;
30463074

3075+
const txExplanation = await this.getTxExplanation(txPrebuild);
3076+
30473077
// Helper to throw TxIntentMismatchRecipientError with recipient details
30483078
const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => {
3049-
throw new TxIntentMismatchRecipientError(message, undefined, [txParams], txPrebuild?.txHex, mismatchedRecipients);
3079+
throw new TxIntentMismatchRecipientError(
3080+
message,
3081+
undefined,
3082+
[txParams],
3083+
txPrebuild?.txHex,
3084+
mismatchedRecipients,
3085+
txExplanation
3086+
);
30503087
};
30513088

30523089
if (
@@ -3148,9 +3185,18 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
31483185
return this.verifyTssTransaction(params);
31493186
}
31503187

3188+
const txExplanation = await this.getTxExplanation(txPrebuild);
3189+
31513190
// Helper to throw TxIntentMismatchRecipientError with recipient details
31523191
const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => {
3153-
throw new TxIntentMismatchRecipientError(message, undefined, [txParams], txPrebuild?.txHex, mismatchedRecipients);
3192+
throw new TxIntentMismatchRecipientError(
3193+
message,
3194+
undefined,
3195+
[txParams],
3196+
txPrebuild?.txHex,
3197+
mismatchedRecipients,
3198+
txExplanation
3199+
);
31543200
};
31553201

31563202
if (!txParams?.recipients || !txPrebuild?.recipients || !wallet) {
@@ -3252,7 +3298,8 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
32523298
'coin in txPrebuild did not match that in txParams supplied by client',
32533299
undefined,
32543300
[txParams],
3255-
txPrebuild?.txHex
3301+
txPrebuild?.txHex,
3302+
txExplanation
32563303
);
32573304
}
32583305
return true;

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
assertValidTransactionRecipient,
6262
explainTx,
6363
fromExtendedAddressFormat,
64+
getTxExplanation,
6465
isScriptRecipient,
6566
parseTransaction,
6667
verifyTransaction,
@@ -143,7 +144,8 @@ function convertValidationErrorToTxIntentMismatch(
143144
error: AggregateValidationError,
144145
reqId: string | IRequestTracer | undefined,
145146
txParams: BaseTransactionParams,
146-
txHex: string | undefined
147+
txHex: string | undefined,
148+
txExplanation?: string
147149
): TxIntentMismatchRecipientError {
148150
const mismatchedRecipients: MismatchedRecipient[] = [];
149151

@@ -170,7 +172,8 @@ function convertValidationErrorToTxIntentMismatch(
170172
reqId,
171173
[txParams],
172174
txHex,
173-
mismatchedRecipients
175+
mismatchedRecipients,
176+
txExplanation
174177
);
175178
// Preserve the original structured error as the cause for debugging
176179
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
@@ -616,11 +619,19 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
616619
async verifyTransaction<TNumber extends number | bigint = number>(
617620
params: VerifyTransactionOptions<TNumber>
618621
): Promise<boolean> {
622+
const txExplanation = await getTxExplanation(this, params.txPrebuild);
623+
619624
try {
620625
return await verifyTransaction(this, this.bitgo, params);
621626
} catch (error) {
622627
if (error instanceof AggregateValidationError) {
623-
throw convertValidationErrorToTxIntentMismatch(error, params.reqId, params.txParams, params.txPrebuild.txHex);
628+
throw convertValidationErrorToTxIntentMismatch(
629+
error,
630+
params.reqId,
631+
params.txParams,
632+
params.txPrebuild.txHex,
633+
txExplanation
634+
);
624635
}
625636
throw error;
626637
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ 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';
78

89
import { toBaseParsedTransactionOutputsFromPsbt } from './parse';
910

@@ -75,13 +76,16 @@ export async function verifyTransaction<TNumber extends number | bigint>(
7576
params: VerifyTransactionOptions<TNumber>,
7677
descriptorMap: DescriptorMap
7778
): Promise<boolean> {
79+
const txExplanation = await getTxExplanation(coin, params.txPrebuild);
80+
7881
const tx = coin.decodeTransactionFromPrebuild(params.txPrebuild);
7982
if (!(tx instanceof utxolib.bitgo.UtxoPsbt)) {
8083
throw new TxIntentMismatchError(
8184
'unexpected transaction type',
8285
params.reqId,
8386
[params.txParams],
84-
params.txPrebuild.txHex
87+
params.txPrebuild.txHex,
88+
txExplanation
8589
);
8690
}
8791

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { AbstractUtxoCoin, VerifyTransactionOptions } from '../../abstractUtxoCo
88
import { Output, ParsedTransaction } from '../types';
99
import { verifyCustomChangeKeySignatures, verifyKeySignature, verifyUserPublicKey } from '../../verifyKey';
1010
import { getPsbtTxInputs, getTxInputs } from '../fetchInputs';
11+
import { getTxExplanation } from '../txExplanation';
1112

1213
const debug = buildDebug('bitgo:abstract-utxo:verifyTransaction');
1314

@@ -50,9 +51,11 @@ export async function verifyTransaction<TNumber extends bigint | number>(
5051
): Promise<boolean> {
5152
const { txParams, txPrebuild, wallet, verification = {}, reqId } = params;
5253

54+
const txExplanation = await getTxExplanation(coin, txPrebuild);
55+
5356
// Helper to throw TxIntentMismatchError with consistent context
5457
const throwTxMismatch = (message: string): never => {
55-
throw new TxIntentMismatchError(message, reqId, [txParams], txPrebuild.txHex);
58+
throw new TxIntentMismatchError(message, reqId, [txParams], txPrebuild.txHex, txExplanation);
5659
};
5760

5861
if (!_.isUndefined(verification.disableNetworking) && !_.isBoolean(verification.disableNetworking)) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ export * from './recipient';
33
export { explainTx } from './explainTransaction';
44
export { parseTransaction } from './parseTransaction';
55
export { verifyTransaction } from './verifyTransaction';
6+
export { getTxExplanation } from './txExplanation';
67
export * from './fetchInputs';
78
export * as bip322 from './bip322';
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { AbstractUtxoCoin, TransactionPrebuild } from '../abstractUtxoCoin';
2+
3+
/**
4+
* Generate a stringified transaction explanation for error reporting
5+
* @param coin - The UTXO coin instance
6+
* @param txPrebuild - Transaction prebuild containing txHex and txInfo
7+
* @returns Stringified JSON explanation
8+
*/
9+
export async function getTxExplanation<TNumber extends number | bigint>(
10+
coin: AbstractUtxoCoin,
11+
txPrebuild: TransactionPrebuild<TNumber>
12+
): Promise<string | undefined> {
13+
if (!txPrebuild.txHex) {
14+
return undefined;
15+
}
16+
17+
try {
18+
const explanation = await coin.explainTransaction({
19+
txHex: txPrebuild.txHex,
20+
txInfo: txPrebuild.txInfo,
21+
});
22+
return JSON.stringify(explanation, null, 2);
23+
} catch (e) {
24+
const errorDetails = {
25+
error: 'Failed to parse transaction explanation',
26+
txHex: txPrebuild.txHex,
27+
details: e instanceof Error ? e.message : String(e),
28+
};
29+
return JSON.stringify(errorDetails, null, 2);
30+
}
31+
}

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,13 @@ export interface ContractDataPayload {
256256
* @property {string | IRequestTracer | undefined} id - Transaction ID or request tracer for tracking
257257
* @property {TransactionParams[]} txParams - Array of transaction parameters that were analyzed
258258
* @property {string | undefined} txHex - The raw transaction in hexadecimal format
259+
* @property {string | undefined} txExplanation - Stringified transaction explanation
259260
*/
260261
export class TxIntentMismatchError extends BitGoJsError {
261262
public readonly id: string | IRequestTracer | undefined;
262263
public readonly txParams: TransactionParams[];
263264
public readonly txHex: string | undefined;
265+
public readonly txExplanation: string | undefined;
264266

265267
/**
266268
* Creates an instance of TxIntentMismatchError
@@ -269,17 +271,20 @@ export class TxIntentMismatchError extends BitGoJsError {
269271
* @param {string | IRequestTracer | undefined} id - Transaction ID or request tracer
270272
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
271273
* @param {string | undefined} txHex - Raw transaction hex string
274+
* @param {string | undefined} txExplanation - Stringified transaction explanation
272275
*/
273276
public constructor(
274277
message: string,
275278
id: string | IRequestTracer | undefined,
276279
txParams: TransactionParams[],
277-
txHex: string | undefined
280+
txHex: string | undefined,
281+
txExplanation?: string
278282
) {
279283
super(message);
280284
this.id = id;
281285
this.txParams = txParams;
282286
this.txHex = txHex;
287+
this.txExplanation = txExplanation;
283288
}
284289
}
285290

@@ -304,15 +309,17 @@ export class TxIntentMismatchRecipientError extends TxIntentMismatchError {
304309
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
305310
* @param {string | undefined} txHex - Raw transaction hex string
306311
* @param {MismatchedRecipient[]} mismatchedRecipients - Array of recipients that don't match user intent
312+
* @param {string | undefined} txExplanation - Stringified transaction explanation
307313
*/
308314
public constructor(
309315
message: string,
310316
id: string | IRequestTracer | undefined,
311317
txParams: TransactionParams[],
312318
txHex: string | undefined,
313-
mismatchedRecipients: MismatchedRecipient[]
319+
mismatchedRecipients: MismatchedRecipient[],
320+
txExplanation?: string
314321
) {
315-
super(message, id, txParams, txHex);
322+
super(message, id, txParams, txHex, txExplanation);
316323
this.mismatchedRecipients = mismatchedRecipients;
317324
}
318325
}
@@ -338,15 +345,17 @@ export class TxIntentMismatchContractError extends TxIntentMismatchError {
338345
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
339346
* @param {string | undefined} txHex - Raw transaction hex string
340347
* @param {ContractDataPayload} mismatchedDataPayload - The contract interaction data that doesn't match user intent
348+
* @param {string | undefined} txExplanation - Stringified transaction explanation
341349
*/
342350
public constructor(
343351
message: string,
344352
id: string | IRequestTracer | undefined,
345353
txParams: TransactionParams[],
346354
txHex: string | undefined,
347-
mismatchedDataPayload: ContractDataPayload
355+
mismatchedDataPayload: ContractDataPayload,
356+
txExplanation?: string
348357
) {
349-
super(message, id, txParams, txHex);
358+
super(message, id, txParams, txHex, txExplanation);
350359
this.mismatchedDataPayload = mismatchedDataPayload;
351360
}
352361
}
@@ -372,15 +381,17 @@ export class TxIntentMismatchApprovalError extends TxIntentMismatchError {
372381
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
373382
* @param {string | undefined} txHex - Raw transaction hex string
374383
* @param {TokenApproval} tokenApproval - Details of the token approval that doesn't match user intent
384+
* @param {string | undefined} txExplanation - Stringified transaction explanation
375385
*/
376386
public constructor(
377387
message: string,
378388
id: string | IRequestTracer | undefined,
379389
txParams: TransactionParams[],
380390
txHex: string | undefined,
381-
tokenApproval: TokenApproval
391+
tokenApproval: TokenApproval,
392+
txExplanation?: string
382393
) {
383-
super(message, id, txParams, txHex);
394+
super(message, id, txParams, txHex, txExplanation);
384395
this.tokenApproval = tokenApproval;
385396
}
386397
}

0 commit comments

Comments
 (0)