diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 8dd5af53046..46839bcf9a4 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Export `hasTransactionType` helper for checking a transaction's type against the top-level `TransactionMeta` and any nested transactions ([#9570](https://github.com/MetaMask/core/pull/9570)) + ## [69.1.0] ### Changed diff --git a/packages/transaction-controller/src/index.ts b/packages/transaction-controller/src/index.ts index 9dbc08c9841..67493bc3dce 100644 --- a/packages/transaction-controller/src/index.ts +++ b/packages/transaction-controller/src/index.ts @@ -137,6 +137,7 @@ export { isEIP1559Transaction, normalizeTransactionParams, } from './utils/utils'; +export { hasTransactionType } from './utils/transaction-type'; export { CHAIN_IDS } from './constants'; export { HARDFORK } from './utils/prepare'; export { getAccountAddressRelationship } from './api/accounts-api'; diff --git a/packages/transaction-controller/src/utils/transaction-type.test.ts b/packages/transaction-controller/src/utils/transaction-type.test.ts index 2b6b57bee3e..7e4284e76ab 100644 --- a/packages/transaction-controller/src/utils/transaction-type.test.ts +++ b/packages/transaction-controller/src/utils/transaction-type.test.ts @@ -9,12 +9,14 @@ import { import type { NetworkClientId } from '@metamask/network-controller'; import type { TransactionControllerMessenger } from '../TransactionController'; +import type { TransactionMeta } from '../types'; import { TransactionType } from '../types'; import { DELEGATION_PREFIX } from './eip7702'; import { rpcRequest } from './provider'; import { decodeTransactionData, determineTransactionType, + hasTransactionType, } from './transaction-type'; jest.mock('./provider', () => ({ @@ -396,3 +398,70 @@ describe('decodeTransactionData', () => { expect(result?.args[1].toString()).toBe(amount); }); }); + +describe('hasTransactionType', () => { + const MATCH = TransactionType.perpsWithdraw; + const OTHER = TransactionType.simpleSend; + + it('returns true when the top-level transaction type matches', () => { + const transaction = { type: MATCH } as TransactionMeta; + + expect(hasTransactionType(transaction, [MATCH])).toBe(true); + }); + + it('returns true when a nested transaction type matches', () => { + const transaction = { + nestedTransactions: [{ type: MATCH }], + } as TransactionMeta; + + expect(hasTransactionType(transaction, [MATCH])).toBe(true); + }); + + it('returns true when one of multiple nested transaction types matches', () => { + const transaction = { + nestedTransactions: [{ type: OTHER }, { type: MATCH }], + } as TransactionMeta; + + expect(hasTransactionType(transaction, [MATCH])).toBe(true); + }); + + it('returns true when the top-level type matches any of multiple candidates', () => { + const transaction = { type: MATCH } as TransactionMeta; + + expect( + hasTransactionType(transaction, [TransactionType.predictWithdraw, MATCH]), + ).toBe(true); + }); + + it('returns false when nested transactions have different types', () => { + const transaction = { + nestedTransactions: [{ type: OTHER }], + } as TransactionMeta; + + expect(hasTransactionType(transaction, [MATCH])).toBe(false); + }); + + it('returns false when nestedTransactions is undefined', () => { + const transaction = {} as TransactionMeta; + + expect(hasTransactionType(transaction, [MATCH])).toBe(false); + }); + + it('returns false when nestedTransactions is empty', () => { + const transaction = { + nestedTransactions: [] as { type: TransactionType }[], + } as TransactionMeta; + + expect(hasTransactionType(transaction, [MATCH])).toBe(false); + }); + + it('returns false when the transaction is undefined', () => { + expect(hasTransactionType(undefined, [MATCH])).toBe(false); + }); + + it('returns false when the types array is empty', () => { + const transaction = { type: MATCH } as TransactionMeta; + + expect(hasTransactionType(transaction, [])).toBe(false); + }); +}); diff --git a/packages/transaction-controller/src/utils/transaction-type.ts b/packages/transaction-controller/src/utils/transaction-type.ts index 85ccad46cc6..abb2d0d0fe1 100644 --- a/packages/transaction-controller/src/utils/transaction-type.ts +++ b/packages/transaction-controller/src/utils/transaction-type.ts @@ -9,7 +9,11 @@ import { import type { NetworkClientId } from '@metamask/network-controller'; import type { TransactionControllerMessenger } from '../TransactionController'; -import type { InferTransactionTypeResult, TransactionParams } from '../types'; +import type { + InferTransactionTypeResult, + TransactionMeta, + TransactionParams, +} from '../types'; import { TransactionType } from '../types'; import { DELEGATION_PREFIX } from './eip7702'; import { rpcRequest } from './provider'; @@ -189,3 +193,28 @@ async function readAddressAsContract( : false; return { contractCode, isContractAddress }; } + +/** + * Check whether a transaction (or any of its nested transactions) has one of + * the given types. + * + * @param transactionMeta - Transaction metadata. + * @param types - Transaction types to match against. + * @returns `true` when the transaction or a nested transaction has one of the given types. + */ +export function hasTransactionType( + transactionMeta: TransactionMeta | undefined, + types: readonly TransactionType[], +): boolean { + const { nestedTransactions, type } = transactionMeta ?? {}; + + if (types.includes(type as TransactionType)) { + return true; + } + + return ( + nestedTransactions?.some((tx) => + types.includes(tx.type as TransactionType), + ) ?? false + ); +} diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index dae8fe35853..7d4f4f6415e 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Consume `hasTransactionType` helper from `@metamask/transaction-controller` to derive relevant transaction type against the top-level `TransactionMeta` ([#9570](https://github.com/MetaMask/core/pull/9570)) - Bump `@metamask/transaction-controller` from `^69.0.0` to `^69.1.0` ([#9568](https://github.com/MetaMask/core/pull/9568)) ## [25.0.0] diff --git a/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.ts b/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.ts index 0c942dd7d03..18a821f8544 100644 --- a/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.ts +++ b/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.ts @@ -1,4 +1,7 @@ -import { TransactionType } from '@metamask/transaction-controller'; +import { + TransactionType, + hasTransactionType, +} from '@metamask/transaction-controller'; import type { PayStrategy, @@ -8,7 +11,6 @@ import type { TransactionPayQuote, } from '../../types'; import { getPayStrategiesConfig } from '../../utils/feature-flags'; -import { isPredictWithdrawTransaction } from '../../utils/transaction'; import { getAcrossDestination } from './across-actions'; import { getAcrossQuotes } from './across-quotes'; import { submitAcrossQuotes } from './across-submit'; @@ -65,7 +67,9 @@ export class AcrossStrategy implements PayStrategy { return actionableRequests.every((singleRequest) => { if (singleRequest.isPostQuote) { - return isPredictWithdrawTransaction(request.transaction); + return hasTransactionType(request.transaction, [ + TransactionType.predictWithdraw, + ]); } try { @@ -91,7 +95,11 @@ export class AcrossStrategy implements PayStrategy { return true; } - if (!isPredictWithdrawTransaction(request.transaction)) { + if ( + !hasTransactionType(request.transaction, [ + TransactionType.predictWithdraw, + ]) + ) { return false; } diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts index bd5eec27cb2..8bee323d22f 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -1,4 +1,8 @@ import { successfulFetch, toHex } from '@metamask/controller-utils'; +import { + TransactionType, + hasTransactionType, +} from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; @@ -28,7 +32,6 @@ import { getTokenBalance, getTokenFiatRate, } from '../../utils/token'; -import { isPredictWithdrawTransaction } from '../../utils/transaction'; import type { AcrossDestination } from './across-actions'; import { getAcrossDestination } from './across-actions'; import { hasUnsupportedTransactionAuthorizationList } from './authorization-list'; @@ -190,7 +193,9 @@ async function getQuoteWithGasStationHandling( const requiresSourceGasReservation = request.isPostQuote === true && - isPredictWithdrawTransaction(fullRequest.transaction); + hasTransactionType(fullRequest.transaction, [ + TransactionType.predictWithdraw, + ]); const adjustedSourceAmount = new BigNumber(request.sourceTokenAmount) .minus(phase1Quote.fees.sourceNetwork.max.raw) @@ -567,7 +572,8 @@ async function calculateSourceNetworkCost( const { swapTx } = quote; const swapChainId = toHex(swapTx.chainId); const isPredictWithdraw = - request.isPostQuote === true && isPredictWithdrawTransaction(transaction); + request.isPostQuote === true && + hasTransactionType(transaction, [TransactionType.predictWithdraw]); const relaxPrefundedSourceEstimate = isPredictWithdraw && new BigNumber(request.sourceTokenAmount).gt(request.sourceBalanceRaw); diff --git a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts index 02619938fa5..c4e24e94733 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-submit.ts @@ -3,7 +3,10 @@ import { successfulFetch, toHex, } from '@metamask/controller-utils'; -import { TransactionType } from '@metamask/transaction-controller'; +import { + TransactionType, + hasTransactionType, +} from '@metamask/transaction-controller'; import type { BatchTransactionParams, TransactionMeta, @@ -26,7 +29,6 @@ import { collectTransactionIds, getTransaction, updateTransaction, - isPredictWithdrawTransaction, waitForTransactionConfirmed, } from '../../utils/transaction'; import { @@ -426,7 +428,7 @@ function shouldEstimate7702SubmitBatch( quote: TransactionPayQuote, ): boolean { return ( - isPredictWithdrawTransaction(parentTransaction) && + hasTransactionType(parentTransaction, [TransactionType.predictWithdraw]) && quote.request.isPostQuote === true && quote.fees.isSourceGasFeeToken === true ); @@ -543,7 +545,7 @@ function buildOriginalTransaction( function getOriginalTransactionType( transaction: TransactionMeta, ): TransactionMeta['type'] { - if (isPredictWithdrawTransaction(transaction)) { + if (hasTransactionType(transaction, [TransactionType.predictWithdraw])) { return TransactionType.predictWithdraw; } @@ -567,7 +569,7 @@ function hasOriginalTransactionGas(transaction: TransactionMeta): boolean { * @returns Across-specific transaction type for known flows, or the original type. */ function getAcrossDepositType(transaction: TransactionMeta): TransactionType { - if (isPredictWithdrawTransaction(transaction)) { + if (hasTransactionType(transaction, [TransactionType.predictWithdraw])) { return TransactionType.predictAcrossWithdraw; } diff --git a/packages/transaction-pay-controller/src/strategy/across/authorization-list.ts b/packages/transaction-pay-controller/src/strategy/across/authorization-list.ts index c1570984585..6432448b6aa 100644 --- a/packages/transaction-pay-controller/src/strategy/across/authorization-list.ts +++ b/packages/transaction-pay-controller/src/strategy/across/authorization-list.ts @@ -1,7 +1,10 @@ +import { + TransactionType, + hasTransactionType, +} from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; import type { QuoteRequest } from '../../types'; -import { isPredictWithdrawTransaction } from '../../utils/transaction'; /** * Check whether an authorization list on the original transaction is unsupported by Across. @@ -24,7 +27,7 @@ export function hasUnsupportedTransactionAuthorizationList( } return ( - !isPredictWithdrawTransaction(transaction) || + !hasTransactionType(transaction, [TransactionType.predictWithdraw]) || requests.some((request) => request.isPostQuote !== true) ); } diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 470b0fd2d76..969ba352c3c 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -2,6 +2,10 @@ import { Interface } from '@ethersproject/abi'; import { toHex } from '@metamask/controller-utils'; +import { + TransactionType, + hasTransactionType, +} from '@metamask/transaction-controller'; import type { AuthorizationList, TransactionMeta, @@ -55,7 +59,6 @@ import { normalizeTokenAddress, TokenAddressTarget, } from '../../utils/token'; -import { isPredictWithdrawTransaction } from '../../utils/transaction'; import { TOKEN_TRANSFER_FOUR_BYTE } from './constants'; import { applyHyperliquidActivationFee } from './hyperliquid-activation'; import { applyPolymarketDepositWalletOverrides } from './polymarket/withdraw'; @@ -829,7 +832,8 @@ async function calculateSourceNetworkCost( relayParams[0]; const isPredictWithdraw = - request.isPostQuote && isPredictWithdrawTransaction(transaction); + request.isPostQuote && + hasTransactionType(transaction, [TransactionType.predictWithdraw]); // `fromOverride = Safe proxy` is only valid for deposit-style Relay routes // where the deposit contract reads the user's source-token balance directly. diff --git a/packages/transaction-pay-controller/src/utils/transaction.test.ts b/packages/transaction-pay-controller/src/utils/transaction.test.ts index 741f40f7f4c..2010ea67b29 100644 --- a/packages/transaction-pay-controller/src/utils/transaction.test.ts +++ b/packages/transaction-pay-controller/src/utils/transaction.test.ts @@ -23,7 +23,6 @@ import { collectTransactionIds, getTransaction, getTransferredAmountFromTxHash, - isPredictWithdrawTransaction, subscribeAssetChanges, subscribeTransactionChanges, updateTransaction, @@ -687,64 +686,6 @@ describe('Transaction Utils', () => { expect(mockCallback).toHaveBeenCalledWith('tx1'); }); }); - - describe('isPredictWithdrawTransaction', () => { - it('returns true when the transaction type is predictWithdraw', () => { - const transaction = { - ...TRANSACTION_META_MOCK, - type: TransactionType.predictWithdraw, - } as TransactionMeta; - - expect(isPredictWithdrawTransaction(transaction)).toBe(true); - }); - - it('returns true when a nested transaction has type predictWithdraw', () => { - const transaction = { - ...TRANSACTION_META_MOCK, - nestedTransactions: [{ type: TransactionType.predictWithdraw }], - } as TransactionMeta; - - expect(isPredictWithdrawTransaction(transaction)).toBe(true); - }); - - it('returns true when one of multiple nested transactions has type predictWithdraw', () => { - const transaction = { - ...TRANSACTION_META_MOCK, - nestedTransactions: [ - { type: TransactionType.simpleSend }, - { type: TransactionType.predictWithdraw }, - ], - } as TransactionMeta; - - expect(isPredictWithdrawTransaction(transaction)).toBe(true); - }); - - it('returns false when nested transactions have different types', () => { - const transaction = { - ...TRANSACTION_META_MOCK, - nestedTransactions: [{ type: TransactionType.simpleSend }], - } as TransactionMeta; - - expect(isPredictWithdrawTransaction(transaction)).toBe(false); - }); - - it('returns false when nestedTransactions is undefined', () => { - const transaction = { - ...TRANSACTION_META_MOCK, - } as TransactionMeta; - - expect(isPredictWithdrawTransaction(transaction)).toBe(false); - }); - - it('returns false when nestedTransactions is empty', () => { - const transaction = { - ...TRANSACTION_META_MOCK, - nestedTransactions: [], - } as TransactionMeta; - - expect(isPredictWithdrawTransaction(transaction)).toBe(false); - }); - }); }); const TX_HASH_MOCK = '0xabc123'; diff --git a/packages/transaction-pay-controller/src/utils/transaction.ts b/packages/transaction-pay-controller/src/utils/transaction.ts index 75bf543b1f4..02cdb779c2e 100644 --- a/packages/transaction-pay-controller/src/utils/transaction.ts +++ b/packages/transaction-pay-controller/src/utils/transaction.ts @@ -1,9 +1,6 @@ import { Interface } from '@ethersproject/abi'; import { abiERC20 } from '@metamask/metamask-eth-abis'; -import { - TransactionStatus, - TransactionType, -} from '@metamask/transaction-controller'; +import { TransactionStatus } from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; @@ -312,27 +309,6 @@ export function collectTransactionIds( return { end }; } -/** - * Check whether a transaction is a Predict withdrawal. - * - * Returns `true` when the transaction's own type is `predictWithdraw`, or - * when any of its nested transactions has that type. - * - * @param transaction - Transaction metadata. - * @returns `true` when the transaction is a Predict withdrawal. - */ -export function isPredictWithdrawTransaction( - transaction: TransactionMeta, -): boolean { - return ( - transaction.type === TransactionType.predictWithdraw || - (transaction.nestedTransactions?.some( - (nt) => nt.type === TransactionType.predictWithdraw, - ) ?? - false) - ); -} - /** * Handle a transaction change by updating its associated data. *