diff --git a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts index b885331f39..c6bb2bc5b6 100644 --- a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts +++ b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts @@ -28,6 +28,8 @@ import { PresignTransactionOptions as BasePresignTransactionOptions, Recipient, SignTransactionOptions as BaseSignTransactionOptions, + TxIntentMismatchError, + TxIntentMismatchRecipientError, TransactionParams, TransactionPrebuild as BaseTransactionPrebuild, TransactionRecipient, @@ -2767,9 +2769,16 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { * @param {TransactionPrebuild} params.txPrebuild - prebuild object returned by server * @param {Wallet} params.wallet - Wallet object to obtain keys to verify against * @returns {boolean} + * @throws {TxIntentMismatchRecipientError} if transaction recipients don't match user intent */ async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise { 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); + }; + if ( !txParams?.recipients && !( @@ -2777,13 +2786,13 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { (txParams.type && ['acceleration', 'fillNonce', 'transferToken', 'tokenApproval'].includes(txParams.type)) ) ) { - throw new Error(`missing txParams`); + throw new Error('missing txParams'); } if (!wallet || !txPrebuild) { - throw new Error(`missing params`); + throw new Error('missing params'); } if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) { - throw new Error(`tx cannot be both a batch and hop transaction`); + throw new Error('tx cannot be both a batch and hop transaction'); } if (txParams.type && ['transfer'].includes(txParams.type)) { @@ -2798,10 +2807,14 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { const txJson = tx.toJson(); if (txJson.data === '0x') { if (expectedAmount !== txJson.value) { - throw new Error('the transaction amount in txPrebuild does not match the value given by client'); + 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()) { - throw new Error('destination address does not match with the recipient address'); + throwRecipientMismatch('destination address does not match with the recipient address', [ + { address: txJson.to, amount: txJson.value }, + ]); } } else if (txJson.data.startsWith('0xa9059cbb')) { const [recipientAddress, amount] = getRawDecoded( @@ -2809,10 +2822,14 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { getBufferedByteCode('0xa9059cbb', txJson.data) ); if (expectedAmount !== amount.toString()) { - throw new Error('the transaction amount in txPrebuild does not match the value given by client'); + throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [ + { address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() }, + ]); } if (expectedDestination.toLowerCase() !== addHexPrefix(recipientAddress.toString()).toLowerCase()) { - throw new Error('destination address does not match with the recipient address'); + throwRecipientMismatch('destination address does not match with the recipient address', [ + { address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() }, + ]); } } } @@ -2829,6 +2846,8 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { * @param {TransactionPrebuild} params.txPrebuild - prebuild object returned by server * @param {Wallet} params.wallet - Wallet object to obtain keys to verify against * @returns {boolean} + * @throws {TxIntentMismatchError} if transaction validation fails + * @throws {TxIntentMismatchRecipientError} if transaction recipients don't match user intent */ async verifyTransaction(params: VerifyEthTransactionOptions): Promise { const ethNetwork = this.getNetwork(); @@ -2838,11 +2857,19 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { return this.verifyTssTransaction(params); } + // Helper to throw TxIntentMismatchRecipientError with recipient details + const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => { + throw new TxIntentMismatchRecipientError(message, undefined, [txParams], txPrebuild?.txHex, mismatchedRecipients); + }; + if (!txParams?.recipients || !txPrebuild?.recipients || !wallet) { - throw new Error(`missing params`); + throw new Error('missing params'); } - if (txParams.hop && txParams.recipients.length > 1) { - throw new Error(`tx cannot be both a batch and hop transaction`); + + const recipients = txParams.recipients!; + + if (txParams.hop && recipients.length > 1) { + throw new Error('tx cannot be both a batch and hop transaction'); } if (txPrebuild.recipients.length > 1) { throw new Error( @@ -2851,8 +2878,8 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { } if (txParams.hop && txPrebuild.hopTransaction) { // Check recipient amount for hop transaction - if (txParams.recipients.length !== 1) { - throw new Error(`hop transaction only supports 1 recipient but ${txParams.recipients.length} found`); + if (recipients.length !== 1) { + throw new Error(`hop transaction only supports 1 recipient but ${recipients.length} found`); } // Check tx sends to hop address @@ -2862,11 +2889,13 @@ 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()) { - throw new Error('recipient address of txPrebuild does not match hop address'); + throwRecipientMismatch('recipient address of txPrebuild does not match hop address', [ + { address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() }, + ]); } // Convert TransactionRecipient array to Recipient array - const recipients: Recipient[] = txParams.recipients.map((r) => { + const hopRecipients: Recipient[] = recipients.map((r) => { return { address: r.address, amount: typeof r.amount === 'number' ? r.amount.toString() : r.amount, @@ -2874,22 +2903,25 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { }); // Check destination address and amount - await this.validateHopPrebuild(wallet, txPrebuild.hopTransaction, { recipients }); - } else if (txParams.recipients.length > 1) { + await this.validateHopPrebuild(wallet, txPrebuild.hopTransaction, { recipients: hopRecipients }); + } else if (recipients.length > 1) { // Check total amount for batch transaction if (txParams.tokenName) { const expectedTotalAmount = new BigNumber(0); if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) { - throw new Error('batch token transaction amount in txPrebuild should be zero for token transfers'); + 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 < txParams.recipients.length; i++) { - expectedTotalAmount = expectedTotalAmount.plus(txParams.recipients[i].amount); + for (let i = 0; i < recipients.length; i++) { + expectedTotalAmount = expectedTotalAmount.plus(recipients[i].amount); } if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) { - throw new Error( - 'batch transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client' + 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() }] ); } } @@ -2900,29 +2932,37 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { !batcherContractAddress || batcherContractAddress.toLowerCase() !== txPrebuild.recipients[0].address.toLowerCase() ) { - throw new Error('recipient address of txPrebuild does not match batcher address'); + throwRecipientMismatch('recipient address of txPrebuild does not match batcher address', [ + { address: txPrebuild.recipients[0].address, amount: txPrebuild.recipients[0].amount.toString() }, + ]); } } else { // Check recipient address and amount for normal transaction - if (txParams.recipients.length !== 1) { - throw new Error(`normal transaction only supports 1 recipient but ${txParams.recipients.length} found`); + if (recipients.length !== 1) { + throw new Error(`normal transaction only supports 1 recipient but ${recipients.length} found`); } - const expectedAmount = new BigNumber(txParams.recipients[0].amount); + const expectedAmount = new BigNumber(recipients[0].amount); if (!expectedAmount.isEqualTo(txPrebuild.recipients[0].amount)) { - throw new Error( - 'normal transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client' + 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(txParams.recipients[0].address) && - txParams.recipients[0].address !== txPrebuild.recipients[0].address - ) { - throw new Error('destination address in normal txPrebuild does not match that in txParams supplied by client'); + if (this.isETHAddress(recipients[0].address) && recipients[0].address !== txPrebuild.recipients[0].address) { + 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)) { - throw new Error(`coin in txPrebuild did not match that in txParams supplied by client`); + throw new TxIntentMismatchError( + 'coin in txPrebuild did not match that in txParams supplied by client', + undefined, + [txParams], + txPrebuild?.txHex + ); } return true; } diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index c153a6d318..00f67ab928 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -22,6 +22,7 @@ import { IWallet, KeychainsTriplet, KeyIndices, + MismatchedRecipient, MultisigType, multisigTypes, P2shP2wshUnsupportedError, @@ -39,6 +40,7 @@ import { TransactionParams as BaseTransactionParams, TransactionPrebuild as BaseTransactionPrebuild, Triple, + TxIntentMismatchRecipientError, UnexpectedAddressError, UnsupportedAddressTypeError, VerificationOptions, @@ -72,6 +74,11 @@ import { parseTransaction, verifyTransaction, } from './transaction'; +import { + AggregateValidationError, + ErrorMissingOutputs, + ErrorImplicitExternalOutputs, +} from './transaction/descriptor/verifyTransaction'; import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptorWallet } from './descriptor'; import { getChainFromNetwork, getFamilyFromNetwork, getFullNameFromNetwork } from './names'; import { CustomChangeOptions } from './transaction/fixedScript'; @@ -113,6 +120,52 @@ const { getExternalChainCode, isChainCode, scriptTypeForChain, outputScripts } = type Unspent = bitgo.Unspent; +/** + * Convert ValidationError to TxIntentMismatchRecipientError with structured data + * + * This preserves the structured error information from the original ValidationError + * by extracting the mismatched outputs and converting them to the standardized format. + * The original error is preserved as the `cause` for debugging purposes. + */ +function convertValidationErrorToTxIntentMismatch( + error: AggregateValidationError, + reqId: string | IRequestTracer | undefined, + txParams: BaseTransactionParams, + txHex: string | undefined +): TxIntentMismatchRecipientError { + const mismatchedRecipients: MismatchedRecipient[] = []; + + for (const err of error.errors) { + if (err instanceof ErrorMissingOutputs) { + mismatchedRecipients.push( + ...err.missingOutputs.map((output) => ({ + address: output.address, + amount: output.amount.toString(), + })) + ); + } else if (err instanceof ErrorImplicitExternalOutputs) { + mismatchedRecipients.push( + ...err.implicitExternalOutputs.map((output) => ({ + address: output.address, + amount: output.amount.toString(), + })) + ); + } + } + + const txIntentError = new TxIntentMismatchRecipientError( + error.message, + reqId, + [txParams], + txHex, + mismatchedRecipients + ); + // 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 + (txIntentError as Error & { cause?: Error }).cause = error; + return txIntentError; +} + export type DecodedTransaction = | utxolib.bitgo.UtxoTransaction | utxolib.bitgo.UtxoPsbt; @@ -631,12 +684,21 @@ export abstract class AbstractUtxoCoin extends BaseCoin { * @param params.verification.disableNetworking Disallow fetching any data from the internet for verification purposes * @param params.verification.keychains Pass keychains manually rather than fetching them by id * @param params.verification.addresses Address details to pass in for out-of-band verification - * @returns {boolean} + * @returns {boolean} True if verification passes + * @throws {TxIntentMismatchError} if transaction validation fails + * @throws {TxIntentMismatchRecipientError} if transaction recipients don't match user intent */ async verifyTransaction( params: VerifyTransactionOptions ): Promise { - return verifyTransaction(this, this.bitgo, params); + try { + return await verifyTransaction(this, this.bitgo, params); + } catch (error) { + if (error instanceof AggregateValidationError) { + throw convertValidationErrorToTxIntentMismatch(error, params.reqId, params.txParams, params.txPrebuild.txHex); + } + throw error; + } } /** diff --git a/modules/abstract-utxo/src/transaction/descriptor/verifyTransaction.ts b/modules/abstract-utxo/src/transaction/descriptor/verifyTransaction.ts index 8b2d6dbab4..939accb62d 100644 --- a/modules/abstract-utxo/src/transaction/descriptor/verifyTransaction.ts +++ b/modules/abstract-utxo/src/transaction/descriptor/verifyTransaction.ts @@ -1,8 +1,13 @@ import * as utxolib from '@bitgo/utxo-lib'; -import { ITransactionRecipient, VerifyTransactionOptions } from '@bitgo/sdk-core'; +import { ITransactionRecipient, TxIntentMismatchError } from '@bitgo/sdk-core'; import { DescriptorMap } from '@bitgo/utxo-core/descriptor'; -import { AbstractUtxoCoin, BaseOutput, BaseParsedTransactionOutputs } from '../../abstractUtxoCoin'; +import { + AbstractUtxoCoin, + BaseOutput, + BaseParsedTransactionOutputs, + VerifyTransactionOptions, +} from '../../abstractUtxoCoin'; import { toBaseParsedTransactionOutputsFromPsbt } from './parse'; @@ -66,16 +71,25 @@ export function assertValidTransaction( * @param coin * @param params * @param descriptorMap + * @returns {boolean} True if verification passes + * @throws {TxIntentMismatchError} if transaction validation fails */ -export async function verifyTransaction( +export async function verifyTransaction( coin: AbstractUtxoCoin, - params: VerifyTransactionOptions, + params: VerifyTransactionOptions, descriptorMap: DescriptorMap ): Promise { const tx = coin.decodeTransactionFromPrebuild(params.txPrebuild); if (!(tx instanceof utxolib.bitgo.UtxoPsbt)) { - throw new Error('unexpected transaction type'); + throw new TxIntentMismatchError( + 'unexpected transaction type', + params.reqId, + [params.txParams], + params.txPrebuild.txHex + ); } + assertValidTransaction(tx, descriptorMap, params.txParams.recipients ?? [], tx.network); + return true; } diff --git a/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts index 0bf0fe31ea..4e54d51cd9 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts @@ -1,7 +1,7 @@ import buildDebug from 'debug'; import _ from 'lodash'; import BigNumber from 'bignumber.js'; -import { BitGoBase } from '@bitgo/sdk-core'; +import { BitGoBase, TxIntentMismatchError } from '@bitgo/sdk-core'; import * as utxolib from '@bitgo/utxo-lib'; import { AbstractUtxoCoin, Output, ParsedTransaction, VerifyTransactionOptions } from '../../abstractUtxoCoin'; @@ -25,6 +25,23 @@ function getPayGoLimit(allowPaygoOutput?: boolean): number { return 0.015; } +/** + * Verify that a transaction prebuild complies with the original intention for fixed-script wallets + * + * This implementation handles transaction verification for traditional UTXO coins using fixed scripts + * (non-descriptor wallets). It validates keychains, signatures, outputs, and amounts. + * + * @param coin - The UTXO coin instance + * @param bitgo - BitGo API instance for network calls + * @param params - Verification parameters + * @param params.txParams - Transaction parameters passed to send + * @param params.txPrebuild - Prebuild object returned by server + * @param params.wallet - Wallet object to obtain keys to verify against + * @param params.verification - Verification options (disableNetworking, keychains, addresses) + * @param params.reqId - Optional request ID for logging + * @returns {boolean} True if verification passes + * @throws {TxIntentMismatchError} if transaction validation fails + */ export async function verifyTransaction( coin: AbstractUtxoCoin, bitgo: BitGoBase, @@ -32,8 +49,13 @@ export async function verifyTransaction( ): Promise { const { txParams, txPrebuild, wallet, verification = {}, reqId } = params; + // Helper to throw TxIntentMismatchError with consistent context + const throwTxMismatch = (message: string): never => { + throw new TxIntentMismatchError(message, reqId, [txParams], txPrebuild.txHex); + }; + if (!_.isUndefined(verification.disableNetworking) && !_.isBoolean(verification.disableNetworking)) { - throw new Error('verification.disableNetworking must be a boolean'); + throw new TypeError('verification.disableNetworking must be a boolean'); } const isPsbt = txPrebuild.txHex && utxolib.bitgo.isPsbt(txPrebuild.txHex); if (isPsbt && txPrebuild.txInfo?.unspents) { @@ -64,7 +86,7 @@ export async function verifyTransaction( if (!_.isEmpty(keySignatures)) { const verify = (key, pub) => { if (!keychains.user || !keychains.user.pub) { - throw new Error('missing user keychain'); + throwTxMismatch('missing user keychain'); } return verifyKeySignature({ userKeychain: keychains.user as { pub: string }, @@ -100,7 +122,7 @@ export async function verifyTransaction( const missingOutputs = parsedTransaction.missingOutputs; if (missingOutputs.length !== 0) { // there are some outputs in the recipients list that have not made it into the actual transaction - throw new Error('expected outputs missing in transaction prebuild'); + throwTxMismatch('expected outputs missing in transaction prebuild'); } const intendedExternalSpend = parsedTransaction.explicitExternalSpendAmount; @@ -140,7 +162,7 @@ export async function verifyTransaction( } else { // the additional external outputs can only be BitGo's pay-as-you-go fee, but we cannot verify the wallet address // there are some addresses that are outside the scope of intended recipients that are not change addresses - throw new Error('prebuild attempts to spend to unintended external recipients'); + throwTxMismatch('prebuild attempts to spend to unintended external recipients'); } } diff --git a/modules/sdk-core/src/bitgo/errors.ts b/modules/sdk-core/src/bitgo/errors.ts index 862a01c5ab..08c87c7aab 100644 --- a/modules/sdk-core/src/bitgo/errors.ts +++ b/modules/sdk-core/src/bitgo/errors.ts @@ -253,24 +253,29 @@ export interface ContractDataPayload { * * @class TxIntentMismatchError * @extends {BitGoJsError} - * @property {string | IRequestTracer} id - Transaction ID or request tracer for tracking + * @property {string | IRequestTracer | undefined} id - Transaction ID or request tracer for tracking * @property {TransactionParams[]} txParams - Array of transaction parameters that were analyzed - * @property {string} txHex - The raw transaction in hexadecimal format + * @property {string | undefined} txHex - The raw transaction in hexadecimal format */ export class TxIntentMismatchError extends BitGoJsError { - public readonly id: string | IRequestTracer; + public readonly id: string | IRequestTracer | undefined; public readonly txParams: TransactionParams[]; - public readonly txHex: string; + public readonly txHex: string | undefined; /** * Creates an instance of TxIntentMismatchError * * @param {string} message - Error message describing the intent mismatch - * @param {string | IRequestTracer} id - Transaction ID or request tracer + * @param {string | IRequestTracer | undefined} id - Transaction ID or request tracer * @param {TransactionParams[]} txParams - Transaction parameters that were analyzed - * @param {string} txHex - Raw transaction hex string + * @param {string | undefined} txHex - Raw transaction hex string */ - public constructor(message: string, id: string | IRequestTracer, txParams: TransactionParams[], txHex: string) { + public constructor( + message: string, + id: string | IRequestTracer | undefined, + txParams: TransactionParams[], + txHex: string | undefined + ) { super(message); this.id = id; this.txParams = txParams; @@ -295,16 +300,16 @@ export class TxIntentMismatchRecipientError extends TxIntentMismatchError { * Creates an instance of TxIntentMismatchRecipientError * * @param {string} message - Error message describing the recipient intent mismatch - * @param {string | IRequestTracer} id - Transaction ID or request tracer + * @param {string | IRequestTracer | undefined} id - Transaction ID or request tracer * @param {TransactionParams[]} txParams - Transaction parameters that were analyzed - * @param {string} txHex - Raw transaction hex string + * @param {string | undefined} txHex - Raw transaction hex string * @param {MismatchedRecipient[]} mismatchedRecipients - Array of recipients that don't match user intent */ public constructor( message: string, - id: string | IRequestTracer, + id: string | IRequestTracer | undefined, txParams: TransactionParams[], - txHex: string, + txHex: string | undefined, mismatchedRecipients: MismatchedRecipient[] ) { super(message, id, txParams, txHex); @@ -329,16 +334,16 @@ export class TxIntentMismatchContractError extends TxIntentMismatchError { * Creates an instance of TxIntentMismatchContractError * * @param {string} message - Error message describing the contract intent mismatch - * @param {string | IRequestTracer} id - Transaction ID or request tracer + * @param {string | IRequestTracer | undefined} id - Transaction ID or request tracer * @param {TransactionParams[]} txParams - Transaction parameters that were analyzed - * @param {string} txHex - Raw transaction hex string + * @param {string | undefined} txHex - Raw transaction hex string * @param {ContractDataPayload} mismatchedDataPayload - The contract interaction data that doesn't match user intent */ public constructor( message: string, - id: string | IRequestTracer, + id: string | IRequestTracer | undefined, txParams: TransactionParams[], - txHex: string, + txHex: string | undefined, mismatchedDataPayload: ContractDataPayload ) { super(message, id, txParams, txHex); @@ -363,16 +368,16 @@ export class TxIntentMismatchApprovalError extends TxIntentMismatchError { * Creates an instance of TxIntentMismatchApprovalError * * @param {string} message - Error message describing the approval intent mismatch - * @param {string | IRequestTracer} id - Transaction ID or request tracer + * @param {string | IRequestTracer | undefined} id - Transaction ID or request tracer * @param {TransactionParams[]} txParams - Transaction parameters that were analyzed - * @param {string} txHex - Raw transaction hex string + * @param {string | undefined} txHex - Raw transaction hex string * @param {TokenApproval} tokenApproval - Details of the token approval that doesn't match user intent */ public constructor( message: string, - id: string | IRequestTracer, + id: string | IRequestTracer | undefined, txParams: TransactionParams[], - txHex: string, + txHex: string | undefined, tokenApproval: TokenApproval ) { super(message, id, txParams, txHex);