Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 70 additions & 21 deletions modules/abstract-eth/src/abstractEthLikeNewCoins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3031,6 +3031,34 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
return txPrebuild.coin === nativeCoin;
}

/**
* Generate transaction explanation for error reporting
* @param txPrebuild - Transaction prebuild containing txHex and fee info
* @returns Stringified JSON explanation
*/
private async getTxExplanation(txPrebuild?: TransactionPrebuild): Promise<string | undefined> {
if (!txPrebuild?.txHex || !txPrebuild?.gasPrice) {
return undefined;
}

try {
const explanation = await this.explainTransaction({
txHex: txPrebuild.txHex,
feeInfo: {
fee: txPrebuild.gasPrice.toString(),
},
});
return JSON.stringify(explanation, null, 2);
} catch (e) {
const errorDetails = {
error: 'Failed to parse transaction explanation',
txHex: txPrebuild.txHex,
details: e instanceof Error ? e.message : String(e),
};
return JSON.stringify(errorDetails, null, 2);
}
}

/**
* Verify if a tss transaction is valid
*
Expand All @@ -3045,8 +3073,16 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
const { txParams, txPrebuild, wallet } = params;

// Helper to throw TxIntentMismatchRecipientError with recipient details
const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => {
throw new TxIntentMismatchRecipientError(message, undefined, [txParams], txPrebuild?.txHex, mismatchedRecipients);
const throwRecipientMismatch = async (message: string, mismatchedRecipients: Recipient[]): Promise<never> => {
const txExplanation = await this.getTxExplanation(txPrebuild);
throw new TxIntentMismatchRecipientError(
message,
undefined,
[txParams],
txPrebuild?.txHex,
mismatchedRecipients,
txExplanation
);
};

if (
Expand Down Expand Up @@ -3077,12 +3113,13 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
const txJson = tx.toJson();
if (txJson.data === '0x') {
if (expectedAmount !== txJson.value) {
throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [
{ address: txJson.to, amount: txJson.value },
]);
await throwRecipientMismatch(
'the transaction amount in txPrebuild does not match the value given by client',
[{ address: txJson.to, amount: txJson.value }]
);
}
if (expectedDestination.toLowerCase() !== txJson.to.toLowerCase()) {
throwRecipientMismatch('destination address does not match with the recipient address', [
await throwRecipientMismatch('destination address does not match with the recipient address', [
{ address: txJson.to, amount: txJson.value },
]);
}
Expand Down Expand Up @@ -3112,13 +3149,14 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
}

if (expectedTokenAmount !== amount.toString()) {
throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [
{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() },
]);
await throwRecipientMismatch(
'the transaction amount in txPrebuild does not match the value given by client',
[{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() }]
);
}

if (expectedRecipientAddress !== addHexPrefix(recipientAddress.toString()).toLowerCase()) {
throwRecipientMismatch('destination address does not match with the recipient address', [
await throwRecipientMismatch('destination address does not match with the recipient address', [
{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() },
]);
}
Expand Down Expand Up @@ -3149,8 +3187,16 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
}

// Helper to throw TxIntentMismatchRecipientError with recipient details
const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => {
throw new TxIntentMismatchRecipientError(message, undefined, [txParams], txPrebuild?.txHex, mismatchedRecipients);
const throwRecipientMismatch = async (message: string, mismatchedRecipients: Recipient[]): Promise<never> => {
const txExplanation = await this.getTxExplanation(txPrebuild);
throw new TxIntentMismatchRecipientError(
message,
undefined,
[txParams],
txPrebuild?.txHex,
mismatchedRecipients,
txExplanation
);
};

if (!txParams?.recipients || !txPrebuild?.recipients || !wallet) {
Expand Down Expand Up @@ -3180,7 +3226,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
const expectedHopAddress = optionalDeps.ethUtil.stripHexPrefix(decodedHopTx.getSenderAddress().toString());
const actualHopAddress = optionalDeps.ethUtil.stripHexPrefix(txPrebuild.recipients[0].address);
if (expectedHopAddress.toLowerCase() !== actualHopAddress.toLowerCase()) {
throwRecipientMismatch('recipient address of txPrebuild does not match hop address', [
await throwRecipientMismatch('recipient address of txPrebuild does not match hop address', [
{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() },
]);
}
Expand All @@ -3200,17 +3246,18 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
if (txParams.tokenName) {
const expectedTotalAmount = new BigNumber(0);
if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
throwRecipientMismatch('batch token transaction amount in txPrebuild should be zero for token transfers', [
{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() },
]);
await throwRecipientMismatch(
'batch token transaction amount in txPrebuild should be zero for token transfers',
[{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() }]
);
}
} else {
let expectedTotalAmount = new BigNumber(0);
for (let i = 0; i < recipients.length; i++) {
expectedTotalAmount = expectedTotalAmount.plus(recipients[i].amount);
}
if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
throwRecipientMismatch(
await throwRecipientMismatch(
'batch transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client',
[{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() }]
);
Expand All @@ -3223,7 +3270,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
!batcherContractAddress ||
batcherContractAddress.toLowerCase() !== txPrebuild.recipients[0].address.toLowerCase()
) {
throwRecipientMismatch('recipient address of txPrebuild does not match batcher address', [
await throwRecipientMismatch('recipient address of txPrebuild does not match batcher address', [
{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() },
]);
}
Expand All @@ -3234,25 +3281,27 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
}
const expectedAmount = new BigNumber(recipients[0].amount);
if (!expectedAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
throwRecipientMismatch(
await throwRecipientMismatch(
'normal transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client',
[{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() }]
);
}
if (this.isETHAddress(recipients[0].address) && recipients[0].address !== txPrebuild.recipients[0].address) {
throwRecipientMismatch(
await throwRecipientMismatch(
'destination address in normal txPrebuild does not match that in txParams supplied by client',
[{ address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() }]
);
}
}
// Check coin is correct for all transaction types
if (!this.verifyCoin(txPrebuild)) {
const txExplanation = await this.getTxExplanation(txPrebuild);
throw new TxIntentMismatchError(
'coin in txPrebuild did not match that in txParams supplied by client',
undefined,
[txParams],
txPrebuild?.txHex
txPrebuild?.txHex,
txExplanation
);
}
return true;
Expand Down
19 changes: 16 additions & 3 deletions modules/abstract-utxo/src/abstractUtxoCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
PresignTransactionOptions,
RequestTracer,
SignedTransaction,
TxIntentMismatchError,
SignTransactionOptions as BaseSignTransactionOptions,
SupplementGenerateWalletOptions,
TransactionParams as BaseTransactionParams,
Expand Down Expand Up @@ -143,7 +144,8 @@ function convertValidationErrorToTxIntentMismatch(
error: AggregateValidationError,
reqId: string | IRequestTracer | undefined,
txParams: BaseTransactionParams,
txHex: string | undefined
txHex: string | undefined,
txExplanation?: unknown
): TxIntentMismatchRecipientError {
const mismatchedRecipients: MismatchedRecipient[] = [];

Expand All @@ -170,7 +172,8 @@ function convertValidationErrorToTxIntentMismatch(
reqId,
[txParams],
txHex,
mismatchedRecipients
mismatchedRecipients,
txExplanation
);
// Preserve the original structured error as the cause for debugging
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
Expand Down Expand Up @@ -620,7 +623,17 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
return await verifyTransaction(this, this.bitgo, params);
} catch (error) {
if (error instanceof AggregateValidationError) {
throw convertValidationErrorToTxIntentMismatch(error, params.reqId, params.txParams, params.txPrebuild.txHex);
const txExplanation = await TxIntentMismatchError.tryGetTxExplanation(
this as unknown as IBaseCoin,
params.txPrebuild
);
throw convertValidationErrorToTxIntentMismatch(
error,
params.reqId,
params.txParams,
params.txPrebuild.txHex,
txExplanation
);
}
throw error;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as utxolib from '@bitgo/utxo-lib';
import { ITransactionRecipient, TxIntentMismatchError } from '@bitgo/sdk-core';
import { ITransactionRecipient, TxIntentMismatchError, IBaseCoin } from '@bitgo/sdk-core';
import { DescriptorMap } from '@bitgo/utxo-core/descriptor';

import { AbstractUtxoCoin, VerifyTransactionOptions } from '../../abstractUtxoCoin';
Expand Down Expand Up @@ -77,11 +77,16 @@ export async function verifyTransaction<TNumber extends number | bigint>(
): Promise<boolean> {
const tx = coin.decodeTransactionFromPrebuild(params.txPrebuild);
if (!(tx instanceof utxolib.bitgo.UtxoPsbt)) {
const txExplanation = await TxIntentMismatchError.tryGetTxExplanation(
coin as unknown as IBaseCoin,
params.txPrebuild
);
throw new TxIntentMismatchError(
'unexpected transaction type',
params.reqId,
[params.txParams],
params.txPrebuild.txHex
params.txPrebuild.txHex,
txExplanation
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import buildDebug from 'debug';
import _ from 'lodash';
import BigNumber from 'bignumber.js';
import { BitGoBase, TxIntentMismatchError } from '@bitgo/sdk-core';
import { BitGoBase, TxIntentMismatchError, IBaseCoin } from '@bitgo/sdk-core';
import * as utxolib from '@bitgo/utxo-lib';

import { AbstractUtxoCoin, VerifyTransactionOptions } from '../../abstractUtxoCoin';
Expand Down Expand Up @@ -50,9 +50,11 @@ export async function verifyTransaction<TNumber extends bigint | number>(
): Promise<boolean> {
const { txParams, txPrebuild, wallet, verification = {}, reqId } = params;

const txExplanation = await TxIntentMismatchError.tryGetTxExplanation(coin as unknown as IBaseCoin, txPrebuild);

// Helper to throw TxIntentMismatchError with consistent context
const throwTxMismatch = (message: string): never => {
throw new TxIntentMismatchError(message, reqId, [txParams], txPrebuild.txHex);
throw new TxIntentMismatchError(message, reqId, [txParams], txPrebuild.txHex, txExplanation);
};

if (!_.isUndefined(verification.disableNetworking) && !_.isBoolean(verification.disableNetworking)) {
Expand Down
Loading