From d3781fedd2d730663d95a89caa710825177f02d6 Mon Sep 17 00:00:00 2001 From: Alexey Kureev Date: Fri, 17 Jul 2026 15:11:55 +0200 Subject: [PATCH 1/2] Memoize formatStateForTransactionPay on input identity --- .../formatStateForTransactionPay.test.ts | 89 ++++++++++++++++ .../src/utils/formatStateForTransactionPay.ts | 100 ++++++++++++++++-- 2 files changed, 180 insertions(+), 9 deletions(-) diff --git a/packages/assets-controller/src/utils/formatStateForTransactionPay.test.ts b/packages/assets-controller/src/utils/formatStateForTransactionPay.test.ts index d906273061e..0ace55b4ee9 100644 --- a/packages/assets-controller/src/utils/formatStateForTransactionPay.test.ts +++ b/packages/assets-controller/src/utils/formatStateForTransactionPay.test.ts @@ -2,6 +2,7 @@ import type { AssetBalance, AssetMetadata, FungibleAssetPrice } from '../types'; import { formatStateForTransactionPay, AccountForLegacyFormat, + FormatStateForTransactionPayParams, } from './formatStateForTransactionPay'; function price( @@ -382,4 +383,92 @@ describe('formatStateForTransactionPay', () => { result.allTokens['0x1'][''].find((token) => token.symbol === 'X'), ).toBeUndefined(); }); + + describe('memoization', () => { + const makeParams = (): FormatStateForTransactionPayParams => ({ + accounts: [{ ...ACCOUNT_1 }], + assetsBalance: { + [ACCOUNT_1.id]: { + [USDC_ASSET_ID]: { amount: '1000000' } as AssetBalance, + }, + }, + assetsInfo: {}, + assetsPrice: {}, + selectedCurrency: 'usd', + nativeAssetIdentifiers: {}, + }); + + it('returns the cached result when called again with identical inputs', () => { + const params = makeParams(); + + const first = formatStateForTransactionPay(params); + const second = formatStateForTransactionPay({ + ...params, + accounts: [{ ...ACCOUNT_1 }], + nativeAssetIdentifiers: {}, + }); + + expect(second).toBe(first); + }); + + it('recomputes when a state slice reference changes', () => { + const params = makeParams(); + + const first = formatStateForTransactionPay(params); + const second = formatStateForTransactionPay({ + ...params, + assetsBalance: { + [ACCOUNT_1.id]: { + [USDC_ASSET_ID]: { amount: '2000000' } as AssetBalance, + }, + }, + }); + + expect(second).not.toBe(first); + const accountLower = ACCOUNT_1.address.toLowerCase(); + expect(second.tokenBalances[accountLower]['0x1'][USDC_ADDRESS]).toBe( + '0x1e8480', + ); + }); + + it('recomputes when the accounts change', () => { + const params = makeParams(); + + const first = formatStateForTransactionPay(params); + const second = formatStateForTransactionPay({ + ...params, + accounts: [ + { + id: 'account-2', + address: '0x0Ac1dF02185025F65202660F8167210A80dD5086', + }, + ], + }); + + expect(second).not.toBe(first); + expect(second.tokenBalances).toStrictEqual({}); + }); + + it('recomputes when the selected currency changes', () => { + const params = makeParams(); + + const first = formatStateForTransactionPay(params); + const second = formatStateForTransactionPay({ + ...params, + selectedCurrency: 'eur', + }); + + expect(second).not.toBe(first); + expect(second.currentCurrency).toBe('eur'); + }); + + it('produces the same output whether served from cache or recomputed', () => { + const params = makeParams(); + + const cached = formatStateForTransactionPay(params); + const recomputed = formatStateForTransactionPay(makeParams()); + + expect(recomputed).toStrictEqual(cached); + }); + }); }); diff --git a/packages/assets-controller/src/utils/formatStateForTransactionPay.ts b/packages/assets-controller/src/utils/formatStateForTransactionPay.ts index eeeba30c893..b66b03afa9c 100644 --- a/packages/assets-controller/src/utils/formatStateForTransactionPay.ts +++ b/packages/assets-controller/src/utils/formatStateForTransactionPay.ts @@ -49,22 +49,79 @@ export type TransactionPayLegacyFormat = { currentCurrency: string; }; +/** Parameters accepted by {@link formatStateForTransactionPay}. */ +export type FormatStateForTransactionPayParams = { + assetsBalance: Record>; + assetsInfo: Record; + assetsPrice: Record; + selectedCurrency: string; + accounts: AccountForLegacyFormat[]; + nativeAssetIdentifiers: Record; + networkConfigurationsByChainId?: Record; +}; + function amountToHex(amount: string): `0x${string}` { const hexString = BigInt(amount).toString(16); return `0x${hexString}`; } +function areAccountsEqual( + a: AccountForLegacyFormat[], + b: AccountForLegacyFormat[], +): boolean { + if (a === b) { + return true; + } + if (a.length !== b.length) { + return false; + } + return a.every( + (account, index) => + account.id === b[index].id && account.address === b[index].address, + ); +} + +function areRecordsShallowEqual( + a: Record | undefined, + b: Record | undefined, +): boolean { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + if (aKeys.length !== bKeys.length) { + return false; + } + return aKeys.every((key) => a[key] === b[key]); +} + function getAmountFromBalance(balance: AssetBalance): string { return typeof balance === 'object' && balance !== null && 'amount' in balance ? String((balance as { amount: string }).amount) : '0'; } +let lastCall: { + params: FormatStateForTransactionPayParams; + result: TransactionPayLegacyFormat; +} | null = null; + /** * Converts AssetsController state into the legacy format consumed by * transaction-pay-controller (TokenBalancesController, AccountTrackerController, * TokensController, TokenRatesController, CurrencyRateController shapes). * + * The last result is memoized on input identity: this function is invoked (via + * `AssetsController:getStateForTransactionPay`) on every + * `TransactionController:stateChange`, but its inputs only change when the + * assets pipeline updates. Recomputing runs keccak256 (`toChecksumAddress`) + * per asset per call, which dominates CPU profiles during transaction + * approval. + * * @param params - Conversion parameters. * @param params.assetsBalance - Per-account balances by asset ID. * @param params.assetsInfo - Metadata by asset ID. @@ -75,15 +132,40 @@ function getAmountFromBalance(balance: AssetBalance): string { * @param params.networkConfigurationsByChainId - Optional chain ID to network config (for native symbol). * @returns Legacy-compatible state for transaction-pay-controller. */ -export function formatStateForTransactionPay(params: { - assetsBalance: Record>; - assetsInfo: Record; - assetsPrice: Record; - selectedCurrency: string; - accounts: AccountForLegacyFormat[]; - nativeAssetIdentifiers: Record; - networkConfigurationsByChainId?: Record; -}): TransactionPayLegacyFormat { +export function formatStateForTransactionPay( + params: FormatStateForTransactionPayParams, +): TransactionPayLegacyFormat { + if ( + lastCall?.params.assetsBalance === params.assetsBalance && + lastCall.params.assetsInfo === params.assetsInfo && + lastCall.params.assetsPrice === params.assetsPrice && + lastCall.params.selectedCurrency === params.selectedCurrency && + lastCall.params.networkConfigurationsByChainId === + params.networkConfigurationsByChainId && + areAccountsEqual(lastCall.params.accounts, params.accounts) && + areRecordsShallowEqual( + lastCall.params.nativeAssetIdentifiers, + params.nativeAssetIdentifiers, + ) + ) { + return lastCall.result; + } + + const result = computeStateForTransactionPay(params); + lastCall = { params, result }; + return result; +} + +/** + * Performs the actual state conversion for + * {@link formatStateForTransactionPay}. + * + * @param params - Conversion parameters. + * @returns Legacy-compatible state for transaction-pay-controller. + */ +function computeStateForTransactionPay( + params: FormatStateForTransactionPayParams, +): TransactionPayLegacyFormat { const { assetsBalance, assetsInfo, From ed31a9a3be34cc5fe4aaecc84cc54f6326d7261d Mon Sep 17 00:00:00 2001 From: Alexey Kureev Date: Fri, 17 Jul 2026 15:13:34 +0200 Subject: [PATCH 2/2] Add changelog entry and export params type --- packages/assets-controller/CHANGELOG.md | 1 + packages/assets-controller/src/index.ts | 1 + packages/assets-controller/src/utils/index.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index d52bcc6eae6..3f86d9ccfd6 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Memoize `formatStateForTransactionPay` on input identity so repeated `AssetsController:getStateForTransactionPay` calls with unchanged state (one per `TransactionController:stateChange`) no longer re-run per-asset address checksumming (keccak256); also exports the new `FormatStateForTransactionPayParams` type ([#9546](https://github.com/MetaMask/core/pull/9546)) - `TokenDataSource` EVM spam filtering now uses per-chain floors from Token API `GET /v1/suggestedOccurrenceFloors` (`queryApiClient.token.fetchV1SuggestedOccurrenceFloors`) instead of a hardcoded minimum of 3 occurrences. Chains missing from the response (or a failed floors fetch) still fall back to 3 ([#9537](https://github.com/MetaMask/core/pull/9537)) - `TokensApiClient` (used by `RpcDataSource` / `TokenDetector`) now sets the token-list `occurrenceFloor` query param from the same Token API `GET /v1/suggestedOccurrenceFloors` endpoint (cached 1h), replacing the hardcoded Linea/MegaETH/Tempo special cases. Missing chains or failed fetches fall back to 3 ([#9537](https://github.com/MetaMask/core/pull/9537)) - Bump `@metamask/network-enablement-controller` from `^5.5.0` to `^5.6.0` ([#9520](https://github.com/MetaMask/core/pull/9520)) diff --git a/packages/assets-controller/src/index.ts b/packages/assets-controller/src/index.ts index 4f69c989517..72e617594d0 100644 --- a/packages/assets-controller/src/index.ts +++ b/packages/assets-controller/src/index.ts @@ -178,6 +178,7 @@ export { export type { AccountForLegacyFormat, BridgeExchangeRatesFormat, + FormatStateForTransactionPayParams, LegacyToken, TransactionPayLegacyFormat, } from './utils'; diff --git a/packages/assets-controller/src/utils/index.ts b/packages/assets-controller/src/utils/index.ts index 408f745261d..d484dcd0be2 100644 --- a/packages/assets-controller/src/utils/index.ts +++ b/packages/assets-controller/src/utils/index.ts @@ -6,6 +6,7 @@ export { formatStateForTransactionPay } from './formatStateForTransactionPay'; export type { BridgeExchangeRatesFormat } from './formatExchangeRatesForBridge'; export type { AccountForLegacyFormat, + FormatStateForTransactionPayParams, LegacyToken, TransactionPayLegacyFormat, } from './formatStateForTransactionPay';