Skip to content

Commit 418878d

Browse files
committed
feat: add tx explanation for TxIntentMismatch errors
Ticket: WP-6608
1 parent 3b715f8 commit 418878d

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
@@ -2798,6 +2798,34 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
27982798
return txPrebuild.coin === nativeCoin;
27992799
}
28002800

2801+
/**
2802+
* Generate transaction explanation for error reporting
2803+
* @param txPrebuild - Transaction prebuild containing txHex and fee info
2804+
* @returns Stringified JSON explanation
2805+
*/
2806+
private async getTxExplanation(txPrebuild?: TransactionPrebuild): Promise<string | undefined> {
2807+
if (!txPrebuild?.txHex || !txPrebuild?.gasPrice) {
2808+
return undefined;
2809+
}
2810+
2811+
try {
2812+
const explanation = await this.explainTransaction({
2813+
txHex: txPrebuild.txHex,
2814+
feeInfo: {
2815+
fee: txPrebuild.gasPrice.toString(),
2816+
},
2817+
});
2818+
return JSON.stringify(explanation, null, 2);
2819+
} catch (e) {
2820+
const errorDetails = {
2821+
error: 'Failed to parse transaction explanation',
2822+
txHex: txPrebuild.txHex,
2823+
details: e instanceof Error ? e.message : String(e),
2824+
};
2825+
return JSON.stringify(errorDetails, null, 2);
2826+
}
2827+
}
2828+
28012829
/**
28022830
* Verify if a tss transaction is valid
28032831
*
@@ -2811,9 +2839,18 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
28112839
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
28122840
const { txParams, txPrebuild, wallet } = params;
28132841

2842+
const txExplanation = await this.getTxExplanation(txPrebuild);
2843+
28142844
// Helper to throw TxIntentMismatchRecipientError with recipient details
28152845
const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => {
2816-
throw new TxIntentMismatchRecipientError(message, undefined, [txParams], txPrebuild?.txHex, mismatchedRecipients);
2846+
throw new TxIntentMismatchRecipientError(
2847+
message,
2848+
undefined,
2849+
[txParams],
2850+
txPrebuild?.txHex,
2851+
mismatchedRecipients,
2852+
txExplanation
2853+
);
28172854
};
28182855

28192856
if (
@@ -2915,9 +2952,18 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
29152952
return this.verifyTssTransaction(params);
29162953
}
29172954

2955+
const txExplanation = await this.getTxExplanation(txPrebuild);
2956+
29182957
// Helper to throw TxIntentMismatchRecipientError with recipient details
29192958
const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => {
2920-
throw new TxIntentMismatchRecipientError(message, undefined, [txParams], txPrebuild?.txHex, mismatchedRecipients);
2959+
throw new TxIntentMismatchRecipientError(
2960+
message,
2961+
undefined,
2962+
[txParams],
2963+
txPrebuild?.txHex,
2964+
mismatchedRecipients,
2965+
txExplanation
2966+
);
29212967
};
29222968

29232969
if (!txParams?.recipients || !txPrebuild?.recipients || !wallet) {
@@ -3019,7 +3065,8 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
30193065
'coin in txPrebuild did not match that in txParams supplied by client',
30203066
undefined,
30213067
[txParams],
3022-
txPrebuild?.txHex
3068+
txPrebuild?.txHex,
3069+
txExplanation
30233070
);
30243071
}
30253072
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
@@ -615,11 +618,19 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
615618
async verifyTransaction<TNumber extends number | bigint = number>(
616619
params: VerifyTransactionOptions<TNumber>
617620
): Promise<boolean> {
621+
const txExplanation = await getTxExplanation(this, params.txPrebuild);
622+
618623
try {
619624
return await verifyTransaction(this, this.bitgo, params);
620625
} catch (error) {
621626
if (error instanceof AggregateValidationError) {
622-
throw convertValidationErrorToTxIntentMismatch(error, params.reqId, params.txParams, params.txPrebuild.txHex);
627+
throw convertValidationErrorToTxIntentMismatch(
628+
error,
629+
params.reqId,
630+
params.txParams,
631+
params.txPrebuild.txHex,
632+
txExplanation
633+
);
623634
}
624635
throw error;
625636
}

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)