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
4 changes: 4 additions & 0 deletions packages/transaction-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/transaction-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand Down Expand Up @@ -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);
});
});
31 changes: 30 additions & 1 deletion packages/transaction-controller/src/utils/transaction-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
);
}
1 change: 1 addition & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { TransactionType } from '@metamask/transaction-controller';
import {
TransactionType,
hasTransactionType,
} from '@metamask/transaction-controller';

import type {
PayStrategy,
Expand All @@ -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';
Expand Down Expand Up @@ -65,7 +67,9 @@ export class AcrossStrategy implements PayStrategy<AcrossQuote> {

return actionableRequests.every((singleRequest) => {
if (singleRequest.isPostQuote) {
return isPredictWithdrawTransaction(request.transaction);
return hasTransactionType(request.transaction, [
TransactionType.predictWithdraw,
]);
}

try {
Expand All @@ -91,7 +95,11 @@ export class AcrossStrategy implements PayStrategy<AcrossQuote> {
return true;
}

if (!isPredictWithdrawTransaction(request.transaction)) {
if (
!hasTransactionType(request.transaction, [
TransactionType.predictWithdraw,
])
) {
return false;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,7 +29,6 @@ import {
collectTransactionIds,
getTransaction,
updateTransaction,
isPredictWithdrawTransaction,
waitForTransactionConfirmed,
} from '../../utils/transaction';
import {
Expand Down Expand Up @@ -426,7 +428,7 @@ function shouldEstimate7702SubmitBatch(
quote: TransactionPayQuote<AcrossQuote>,
): boolean {
return (
isPredictWithdrawTransaction(parentTransaction) &&
hasTransactionType(parentTransaction, [TransactionType.predictWithdraw]) &&
quote.request.isPostQuote === true &&
quote.fees.isSourceGasFeeToken === true
);
Expand Down Expand Up @@ -543,7 +545,7 @@ function buildOriginalTransaction(
function getOriginalTransactionType(
transaction: TransactionMeta,
): TransactionMeta['type'] {
if (isPredictWithdrawTransaction(transaction)) {
if (hasTransactionType(transaction, [TransactionType.predictWithdraw])) {
return TransactionType.predictWithdraw;
}

Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -24,7 +27,7 @@ export function hasUnsupportedTransactionAuthorizationList(
}

return (
!isPredictWithdrawTransaction(transaction) ||
!hasTransactionType(transaction, [TransactionType.predictWithdraw]) ||
requests.some((request) => request.isPostQuote !== true)
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down
Loading