From 24ed98ad5d1ad4bf66e0718eec19ffb91599da3c Mon Sep 17 00:00:00 2001 From: ffmcgee <51971598+ffmcgee725@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:50:11 +0100 Subject: [PATCH 01/10] fix: add logic to compute the correct non-EVM network image source using `getNetworkImageSource` (#23089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Incorrect Network (Solana) is displayed on dapp browsing when I have Bitcoin/ or Tron selected in my wallet, because current logic either renders EVM selected network, or defaults to Solana when something other than EVM is selected. This PR proposes a fix to handle non EVM networks accordingly. ## **Changelog** CHANGELOG entry: Add logic to compute the correct non-EVM network image source using `getNetworkImageSource` ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/22787 ## **Manual testing steps** ```gherkin Feature: Non EVM Image for AccountRightButton Component Scenario: user selects non evm network Given he does so in In App Browser When user selects non evm network via In App Browser AccountRightButton Then the correct network image should be displayed ``` ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/e2d114cb-2a99-497b-9417-ee7877e98fc7 ### **After** https://github.com/user-attachments/assets/5ca5dc99-e94a-41e3-a48c-371762a7320c ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Computes the proper non-EVM network image using getNetworkImageSource and replaces the hardcoded Solana fallback in AccountRightButton. > > - **UI**: > - **AccountRightButton (`app/components/UI/AccountRightButton/index.tsx`)**: > - Compute non-EVM network image via `useMemo` and `getNetworkImageSource` using `selectedNonEvmNetworkChainId`. > - Replace hardcoded Solana fallback with computed `imageSource`; EVM logic unchanged. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2851891dd74c2ca35f59db0ebea87e8cf1738833. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../UI/AccountRightButton/index.tsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/app/components/UI/AccountRightButton/index.tsx b/app/components/UI/AccountRightButton/index.tsx index ba656a961ce6..b8fb4a038f07 100644 --- a/app/components/UI/AccountRightButton/index.tsx +++ b/app/components/UI/AccountRightButton/index.tsx @@ -1,4 +1,10 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { useSelector } from 'react-redux'; import { TouchableOpacity, @@ -9,7 +15,6 @@ import { EmitterSubscription, } from 'react-native'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; -import images from 'images/image-icons'; import Device from '../../../util/device'; import AvatarAccount from '../../../component-library/components/Avatars/Avatar/variants/AvatarAccount'; import { AccountRightButtonProps } from './AccountRightButton.types'; @@ -17,7 +22,10 @@ import Avatar, { AvatarVariant, AvatarSize, } from '../../../component-library/components/Avatars/Avatar'; -import { getDecimalChainId } from '../../../util/networks'; +import { + getDecimalChainId, + getNetworkImageSource, +} from '../../../util/networks'; import Routes from '../../../constants/navigation/Routes'; import { MetaMetricsEvents } from '../../../core/Analytics'; import { AccountOverviewSelectorsIDs } from '../../../../e2e/selectors/Browser/AccountOverview.selectors'; @@ -156,6 +164,12 @@ const AccountRightButton = ({ const { networkName, networkImageSource } = useNetworkInfo(hostname); + const nonEvmNetworkImageSource = useMemo(() => { + if (!isEvmSelected && selectedNonEvmNetworkChainId) { + return getNetworkImageSource({ chainId: selectedNonEvmNetworkChainId }); + } + }, [isEvmSelected, selectedNonEvmNetworkChainId]); + const renderAvatarAccount = () => ( ); @@ -179,7 +193,9 @@ const AccountRightButton = ({ : nonEvmNetworkConfigurations?.[selectedNonEvmNetworkChainId] ?.name } - imageSource={isEvmSelected ? networkImageSource : images.SOLANA} + imageSource={ + isEvmSelected ? networkImageSource : nonEvmNetworkImageSource + } /> )} From 0251731de5980d0b4f743eaae44303fc7e3b61f5 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 21 Nov 2025 13:18:10 +0000 Subject: [PATCH 02/10] perf: cp-7.60.0 reduce loading time of metamask pay confirmations (#23064) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Reduce loading time when starting Perps and Predict deposit confirmations. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: [#6162](https://github.com/MetaMask/MetaMask-planning/issues/6162) [#6165](https://github.com/MetaMask/MetaMask-planning/issues/6165) ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Speeds up confirmation flows with memoization and streamlined pay-token logic, adds safer Perps/Predict transaction options, updates alerts/UI, and bumps transaction-controller. > > - **Confirmations/Pay UX & Performance**: > - Memoize calculations in `useGasFeeToken`, `useTokenAmount`, and `pay-token-amount` with skeleton fallback; reduce recomputations and unnecessary renders. > - Refactor `useAutomaticTransactionPayToken` to use `useTransactionPayAvailableTokens`, simplify selection logic, and remove count-return path; update related tests. > - Update `useTransactionPayMetrics` to derive payment token list size from available tokens and drop dependency on automatic selector. > - Improve `useTransactionPayToken` by making gas estimate fetch non-blocking and flushing Engine state immediately. > - Tighten recipient detection in `useTransferRecipient` to explicit transfer types only. > - **Perps/Predict Transactions**: > - Perps deposit: add `skipInitialGasEstimate: true` when calling `TransactionController.addTransaction`; test updated. > - Predict deposit: add `disableUpgrade: true` and `skipInitialGasEstimate: true` to `addTransactionBatch` options; tests updated. > - Safe utils: mark generated proxy/allowance/claim transactions as `TransactionType.contractInteraction`. > - **Alerts & Localization**: > - `useInsufficientPayTokenBalanceAlert`: handle zero/negative target amounts with alternate copy; add `insufficient_pay_token_balance_fees_no_target` locale string. > - **Engine**: > - `EngineService`: flush Redux updates immediately for `ApprovalController` and expose `flushState()`. > - **Dependencies**: > - Bump `@metamask/transaction-controller` to `62.1.0` (package.json and yarn.lock). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f7c039c41135cfafe54118a156f20f548e39fdfd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../Perps/controllers/PerpsController.test.ts | 1 + .../UI/Perps/controllers/PerpsController.ts | 1 + .../controllers/PredictController.test.ts | 2 + .../Predict/controllers/PredictController.ts | 2 + .../providers/polymarket/safe/utils.ts | 2 + .../pay-token-amount/pay-token-amount.tsx | 33 +-- ...seInsufficientPayTokenBalanceAlert.test.ts | 33 +++ .../useInsufficientPayTokenBalanceAlert.ts | 17 +- .../confirmations/hooks/gas/useGasFeeToken.ts | 83 ++++--- .../useAutomaticTransactionPayToken.test.ts | 234 +++--------------- .../pay/useAutomaticTransactionPayToken.ts | 187 +++++++------- .../pay/useTransactionPayMetrics.test.ts | 20 +- .../hooks/pay/useTransactionPayMetrics.ts | 14 +- .../hooks/pay/useTransactionPayToken.ts | 10 +- .../transactions/useTransferRecipient.ts | 13 +- .../confirmations/hooks/useTokenAmount.ts | 8 +- app/core/EngineService/EngineService.ts | 12 + locales/languages/en.json | 3 + package.json | 4 +- yarn.lock | 18 +- 20 files changed, 310 insertions(+), 387 deletions(-) diff --git a/app/components/UI/Perps/controllers/PerpsController.test.ts b/app/components/UI/Perps/controllers/PerpsController.test.ts index 26b06e7bff56..9d80caa108ce 100644 --- a/app/components/UI/Perps/controllers/PerpsController.test.ts +++ b/app/components/UI/Perps/controllers/PerpsController.test.ts @@ -2166,6 +2166,7 @@ describe('PerpsController', () => { networkClientId: mockNetworkClientId, origin: 'metamask', type: 'perpsDeposit', + skipInitialGasEstimate: true, }); }); diff --git a/app/components/UI/Perps/controllers/PerpsController.ts b/app/components/UI/Perps/controllers/PerpsController.ts index cb978580726f..0bbf5f1c6f08 100644 --- a/app/components/UI/Perps/controllers/PerpsController.ts +++ b/app/components/UI/Perps/controllers/PerpsController.ts @@ -1279,6 +1279,7 @@ export class PerpsController extends BaseController< networkClientId, origin: 'metamask', type: TransactionType.perpsDeposit, + skipInitialGasEstimate: true, }); // Store the transaction ID and try to get amount from transaction diff --git a/app/components/UI/Predict/controllers/PredictController.test.ts b/app/components/UI/Predict/controllers/PredictController.test.ts index d635084a50e2..3f30178f3f25 100644 --- a/app/components/UI/Predict/controllers/PredictController.test.ts +++ b/app/components/UI/Predict/controllers/PredictController.test.ts @@ -2605,6 +2605,8 @@ describe('PredictController', () => { networkClientId: 'mainnet', disableHook: true, disableSequential: true, + disableUpgrade: true, + skipInitialGasEstimate: true, transactions: mockTransactions, }); }); diff --git a/app/components/UI/Predict/controllers/PredictController.ts b/app/components/UI/Predict/controllers/PredictController.ts index b6aff6f6e064..ca3612fe6912 100644 --- a/app/components/UI/Predict/controllers/PredictController.ts +++ b/app/components/UI/Predict/controllers/PredictController.ts @@ -1760,6 +1760,8 @@ export class PredictController extends BaseController< networkClientId, disableHook: true, disableSequential: true, + disableUpgrade: true, + skipInitialGasEstimate: true, transactions, }); diff --git a/app/components/UI/Predict/providers/polymarket/safe/utils.ts b/app/components/UI/Predict/providers/polymarket/safe/utils.ts index 511258898e96..dde34579b94f 100644 --- a/app/components/UI/Predict/providers/polymarket/safe/utils.ts +++ b/app/components/UI/Predict/providers/polymarket/safe/utils.ts @@ -389,6 +389,7 @@ export const getDeployProxyWalletTransaction = async ({ to: SAFE_FACTORY_ADDRESS as Hex, data: calldata, }, + type: TransactionType.contractInteraction, }; } catch (error) { console.error('Error creating proxy wallet', error); @@ -580,6 +581,7 @@ export const getProxyWalletAllowancesTransaction = async ({ to: safeAddress as Hex, data: callData as Hex, }, + type: TransactionType.contractInteraction, }; }; diff --git a/app/components/Views/confirmations/components/pay-token-amount/pay-token-amount.tsx b/app/components/Views/confirmations/components/pay-token-amount/pay-token-amount.tsx index d310c2e9b006..2f2324ac8e5e 100644 --- a/app/components/Views/confirmations/components/pay-token-amount/pay-token-amount.tsx +++ b/app/components/Views/confirmations/components/pay-token-amount/pay-token-amount.tsx @@ -44,8 +44,24 @@ export function PayTokenAmount({ amountHuman, disabled }: PayTokenAmountProps) { const fiatRates = useTokenFiatRates(fiatRequests); - const payTokenFiatRate = fiatRates[0]; - const assetFiatRate = fiatRates[1]; + const formattedAmount = useMemo(() => { + const payTokenFiatRate = fiatRates[0]; + const assetFiatRate = fiatRates[1]; + + if (disabled || !payToken || !payTokenFiatRate || !assetFiatRate) { + return undefined; + } + + const assetToPayTokenRate = new BigNumber(assetFiatRate).dividedBy( + payTokenFiatRate, + ); + + const payTokenAmount = new BigNumber(amountHuman || '0').multipliedBy( + assetToPayTokenRate, + ); + + return formatAmount(I18n.locale, payTokenAmount); + }, [amountHuman, disabled, payToken, fiatRates]); if (disabled) { return ( @@ -55,18 +71,7 @@ export function PayTokenAmount({ amountHuman, disabled }: PayTokenAmountProps) { ); } - if (!payToken || !payTokenFiatRate || !assetFiatRate) - return ; - - const assetToPayTokenRate = new BigNumber(assetFiatRate).dividedBy( - payTokenFiatRate, - ); - - const payTokenAmount = new BigNumber(amountHuman || '0').multipliedBy( - assetToPayTokenRate, - ); - - const formattedAmount = formatAmount(I18n.locale, payTokenAmount); + if (!formattedAmount) return ; return ( diff --git a/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.test.ts b/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.test.ts index 1182a79a70b0..320ddc6b5009 100644 --- a/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.test.ts +++ b/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.test.ts @@ -159,6 +159,39 @@ describe('useInsufficientPayTokenBalanceAlert', () => { ]); }); + it('returns alert if pay token balance shortfall is equal to total amount', () => { + useTransactionPayTokenMock.mockReturnValue({ + payToken: { + ...PAY_TOKEN_MOCK, + balanceRaw: '999', + }, + setPayToken: jest.fn(), + }); + + useTransactionPayRequiredTokensMock.mockReturnValue([ + { + ...REQUIRED_TOKEN_MOCK, + amountUsd: '0.02', + }, + ]); + + const { result } = runHook(); + + expect(result.current).toStrictEqual([ + { + key: AlertKeys.InsufficientPayTokenFees, + field: RowAlertKey.Amount, + isBlocking: true, + title: strings('alert_system.insufficient_pay_token_balance.message'), + message: strings( + 'alert_system.insufficient_pay_token_balance_fees_no_target.message', + { amount: '$1.21' }, + ), + severity: Severity.Danger, + }, + ]); + }); + it('returns alert if pay token balance is less than source amount plus source network', () => { useTransactionPayTokenMock.mockReturnValue({ payToken: { diff --git a/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.ts b/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.ts index 3d5ec92493fa..e700dc4ae465 100644 --- a/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.ts +++ b/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.ts @@ -83,7 +83,10 @@ export function useInsufficientPayTokenBalanceAlert({ const targetAmountUsd = useMemo(() => { const shortfall = totalSourceAmountUsd.minus(balanceUsd ?? '0'); - return formatFiat(totalAmountUsd.minus(shortfall)); + const targetUsdValue = totalAmountUsd.minus(shortfall); + const targetUsd = formatFiat(targetUsdValue); + + return targetUsdValue.isLessThanOrEqualTo(0) ? undefined : targetUsd; }, [balanceUsd, formatFiat, totalAmountUsd, totalSourceAmountUsd]); const totalSourceNetworkFeeRaw = useMemo( @@ -141,10 +144,14 @@ export function useInsufficientPayTokenBalanceAlert({ ...baseAlert, key: AlertKeys.InsufficientPayTokenFees, title: strings('alert_system.insufficient_pay_token_balance.message'), - message: strings( - 'alert_system.insufficient_pay_token_balance_fees.message', - { amount: targetAmountUsd }, - ), + message: targetAmountUsd + ? strings( + 'alert_system.insufficient_pay_token_balance_fees.message', + { amount: targetAmountUsd }, + ) + : strings( + 'alert_system.insufficient_pay_token_balance_fees_no_target.message', + ), }, ]; } diff --git a/app/components/Views/confirmations/hooks/gas/useGasFeeToken.ts b/app/components/Views/confirmations/hooks/gas/useGasFeeToken.ts index 65a5336325f2..da10ad725f63 100644 --- a/app/components/Views/confirmations/hooks/gas/useGasFeeToken.ts +++ b/app/components/Views/confirmations/hooks/gas/useGasFeeToken.ts @@ -17,6 +17,7 @@ import { selectNetworkConfigurationByChainId } from '../../../../../selectors/ne import { RootState } from '../../../../../reducers'; import { useEthFiatAmount } from '../useEthFiatAmount'; import { useAccountNativeBalance } from '../useAccountNativeBalance'; +import { useMemo } from 'react'; export const RATE_WEI_NATIVE = '0xDE0B6B3A7640000'; // 1x10^18 @@ -44,9 +45,9 @@ export function useGasFeeToken({ tokenAddress }: { tokenAddress?: Hex }) { decimals: 0, }; - const amountFormatted = formatAmount( - locale, - new BigNumber(amount).shiftedBy(-decimals), + const amountFormatted = useMemo( + () => formatAmount(locale, new BigNumber(amount).shiftedBy(-decimals)), + [amount, decimals, locale], ); const amountFiat = useFiatTokenValue( @@ -61,20 +62,34 @@ export function useGasFeeToken({ tokenAddress }: { tokenAddress?: Hex }) { ); const metamaskFeeFiat = useFiatTokenValue(gasFeeToken, metaMaskFee, chainId); - const transferTransaction = - tokenAddress === NATIVE_TOKEN_ADDRESS - ? getNativeTransferTransaction(gasFeeToken) - : getTokenTransferTransaction(gasFeeToken); + const transferTransaction = useMemo( + () => + tokenAddress === NATIVE_TOKEN_ADDRESS + ? getNativeTransferTransaction(gasFeeToken) + : getTokenTransferTransaction(gasFeeToken), + [gasFeeToken, tokenAddress], + ); - return { - ...gasFeeToken, - amountFormatted, - amountFiat, - balanceFiat, - metaMaskFee, - metamaskFeeFiat, - transferTransaction, - }; + return useMemo( + () => ({ + ...gasFeeToken, + amountFormatted, + amountFiat, + balanceFiat, + metaMaskFee, + metamaskFeeFiat, + transferTransaction, + }), + [ + gasFeeToken, + amountFormatted, + amountFiat, + balanceFiat, + metaMaskFee, + metamaskFeeFiat, + transferTransaction, + ], + ); } export function useSelectedGasFeeToken() { @@ -109,19 +124,29 @@ function useNativeGasFeeToken(): GasFeeToken { const { nativeCurrency } = networkConfiguration ?? {}; const { gas, maxFeePerGas, maxPriorityFeePerGas } = txParams ?? {}; - return { - amount: (estimatedFeeNativeHex as Hex) ?? '0x0', - balance, - decimals: 18, - gas: gas as Hex, - gasTransfer: '0x0', - maxFeePerGas: maxFeePerGas as Hex, - maxPriorityFeePerGas: maxPriorityFeePerGas as Hex, - rateWei: RATE_WEI_NATIVE, - recipient: NATIVE_TOKEN_ADDRESS, - symbol: nativeCurrency, - tokenAddress: NATIVE_TOKEN_ADDRESS, - }; + return useMemo( + () => ({ + amount: (estimatedFeeNativeHex as Hex) ?? '0x0', + balance, + decimals: 18, + gas: gas as Hex, + gasTransfer: '0x0', + maxFeePerGas: maxFeePerGas as Hex, + maxPriorityFeePerGas: maxPriorityFeePerGas as Hex, + rateWei: RATE_WEI_NATIVE, + recipient: NATIVE_TOKEN_ADDRESS, + symbol: nativeCurrency, + tokenAddress: NATIVE_TOKEN_ADDRESS, + }), + [ + estimatedFeeNativeHex, + balance, + gas, + maxFeePerGas, + maxPriorityFeePerGas, + nativeCurrency, + ], + ); } function useFiatTokenValue( diff --git a/app/components/Views/confirmations/hooks/pay/useAutomaticTransactionPayToken.test.ts b/app/components/Views/confirmations/hooks/pay/useAutomaticTransactionPayToken.test.ts index 15ad960a94d7..753717f2bc1a 100644 --- a/app/components/Views/confirmations/hooks/pay/useAutomaticTransactionPayToken.test.ts +++ b/app/components/Views/confirmations/hooks/pay/useAutomaticTransactionPayToken.test.ts @@ -1,33 +1,26 @@ import { merge } from 'lodash'; import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider'; -import { useTokensWithBalance } from '../../../../UI/Bridge/hooks/useTokensWithBalance'; import { useAutomaticTransactionPayToken } from './useAutomaticTransactionPayToken'; import { useTransactionPayToken } from './useTransactionPayToken'; import { simpleSendTransactionControllerMock } from '../../__mocks__/controllers/transaction-controller-mock'; import { transactionApprovalControllerMock } from '../../__mocks__/controllers/approval-controller-mock'; -import { selectEnabledSourceChains } from '../../../../../core/redux/slices/bridge'; -import { NATIVE_TOKEN_ADDRESS } from '../../constants/tokens'; import { isHardwareAccount } from '../../../../../util/address'; import { TransactionType } from '@metamask/transaction-controller'; import { TransactionPayRequiredToken } from '@metamask/transaction-pay-controller'; import { Hex } from '@metamask/utils'; import { useTransactionPayRequiredTokens } from './useTransactionPayData'; +import { useTransactionPayAvailableTokens } from './useTransactionPayAvailableTokens'; +import { AssetType } from '../../types/token'; jest.mock('./useTransactionPayToken'); -jest.mock('../../../../UI/Bridge/hooks/useTokensWithBalance'); jest.mock('../../../../../util/address'); jest.mock('../../../../../selectors/transactionPayController'); jest.mock('./useTransactionPayData'); - -jest.mock('../../../../../core/redux/slices/bridge', () => ({ - ...jest.requireActual('../../../../../core/redux/slices/bridge'), - selectEnabledSourceChains: jest.fn(), -})); +jest.mock('./useTransactionPayAvailableTokens'); const TOKEN_ADDRESS_1_MOCK = '0x1234567890abcdef1234567890abcdef12345678'; const TOKEN_ADDRESS_2_MOCK = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; const TOKEN_ADDRESS_3_MOCK = '0xabc1234567890abcdef1234567890abcdef12345678'; -const REQUIRED_BALANCE_MOCK = 10; const CHAIN_ID_1_MOCK = '0x1'; const CHAIN_ID_2_MOCK = '0x2'; @@ -61,8 +54,9 @@ function runHook({ disable = false } = {}) { describe('useAutomaticTransactionPayToken', () => { const useTransactionPayTokenMock = jest.mocked(useTransactionPayToken); - const useTokensWithBalanceMock = jest.mocked(useTokensWithBalance); - const selectEnabledSourceChainsMock = jest.mocked(selectEnabledSourceChains); + const useTransactionPayAvailableTokensMock = jest.mocked( + useTransactionPayAvailableTokens, + ); const isHardwareAccountMock = jest.mocked(isHardwareAccount); const useTransactionPayRequiredTokensMock = jest.mocked( useTransactionPayRequiredTokens, @@ -75,8 +69,6 @@ describe('useAutomaticTransactionPayToken', () => { beforeEach(() => { jest.resetAllMocks(); - selectEnabledSourceChainsMock.mockReturnValue([]); - useTransactionPayTokenMock.mockReturnValue({ payToken: undefined, setPayToken: setPayTokenMock, @@ -85,6 +77,7 @@ describe('useAutomaticTransactionPayToken', () => { useTransactionPayRequiredTokensMock.mockReturnValue([ { address: TOKEN_ADDRESS_1_MOCK as Hex, + chainId: CHAIN_ID_1_MOCK as Hex, } as TransactionPayRequiredToken, ]); @@ -92,23 +85,20 @@ describe('useAutomaticTransactionPayToken', () => { }); it('selects target token if has balance', () => { - useTokensWithBalanceMock.mockReturnValue([ - { - address: TOKEN_ADDRESS_1_MOCK, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK, - }, + useTransactionPayAvailableTokensMock.mockReturnValue([ { address: TOKEN_ADDRESS_2_MOCK, chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK + 10, }, { address: TOKEN_ADDRESS_3_MOCK, chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK + 20, }, - ] as unknown as ReturnType); + { + address: TOKEN_ADDRESS_1_MOCK, + chainId: CHAIN_ID_1_MOCK, + }, + ] as AssetType[]); runHook(); @@ -119,33 +109,20 @@ describe('useAutomaticTransactionPayToken', () => { }); it('selects token with highest balance on same chain if insufficient balance on target token', () => { - useTokensWithBalanceMock.mockReturnValue([ - { - address: TOKEN_ADDRESS_1_MOCK, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: 0, - }, - { - address: TOKEN_ADDRESS_2_MOCK, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK + 5, - }, + useTransactionPayAvailableTokensMock.mockReturnValue([ { address: TOKEN_ADDRESS_3_MOCK, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK + 10, + chainId: CHAIN_ID_2_MOCK, }, { - address: TOKEN_ADDRESS_3_MOCK, + address: TOKEN_ADDRESS_2_MOCK, chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK + 20, }, { - address: NATIVE_TOKEN_ADDRESS, + address: TOKEN_ADDRESS_3_MOCK, chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: 1, }, - ] as unknown as ReturnType); + ] as AssetType[]); runHook(); @@ -156,38 +133,16 @@ describe('useAutomaticTransactionPayToken', () => { }); it('selects token with highest balance on alternate chain if insufficient balance on same chain', () => { - useTokensWithBalanceMock.mockReturnValue([ - { - address: TOKEN_ADDRESS_1_MOCK, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: 0, - }, - { - address: TOKEN_ADDRESS_2_MOCK, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: 0, - }, - { - address: TOKEN_ADDRESS_1_MOCK, - chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK + 10, - }, + useTransactionPayAvailableTokensMock.mockReturnValue([ { address: TOKEN_ADDRESS_3_MOCK, chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK + 20, - }, - { - address: NATIVE_TOKEN_ADDRESS, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: 0, }, { - address: NATIVE_TOKEN_ADDRESS, + address: TOKEN_ADDRESS_2_MOCK, chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: 1, }, - ] as unknown as ReturnType); + ] as AssetType[]); runHook(); @@ -198,28 +153,7 @@ describe('useAutomaticTransactionPayToken', () => { }); it('selects target token if insufficient balance on all chains', () => { - useTokensWithBalanceMock.mockReturnValue([ - { - address: TOKEN_ADDRESS_1_MOCK, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK - 1, - }, - { - address: TOKEN_ADDRESS_2_MOCK, - chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK - 1, - }, - { - address: NATIVE_TOKEN_ADDRESS, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: 1, - }, - { - address: NATIVE_TOKEN_ADDRESS, - chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: 1, - }, - ] as unknown as ReturnType); + useTransactionPayAvailableTokensMock.mockReturnValue([]); runHook(); @@ -230,19 +164,7 @@ describe('useAutomaticTransactionPayToken', () => { }); it('does nothing if no required tokens', () => { - useTokensWithBalanceMock.mockReturnValue([ - { - address: TOKEN_ADDRESS_1_MOCK, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK, - }, - { - address: TOKEN_ADDRESS_2_MOCK, - chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK, - }, - ] as unknown as ReturnType); - + useTransactionPayAvailableTokensMock.mockReturnValue([]); useTransactionPayRequiredTokensMock.mockReturnValue([]); runHook(); @@ -250,81 +172,21 @@ describe('useAutomaticTransactionPayToken', () => { expect(setPayTokenMock).not.toHaveBeenCalled(); }); - it('does not select token if no native balance on chain', () => { - useTokensWithBalanceMock.mockReturnValue([ - { - address: TOKEN_ADDRESS_1_MOCK, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK - 1, - }, - { - address: TOKEN_ADDRESS_2_MOCK, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK + 5, - }, - { - address: TOKEN_ADDRESS_3_MOCK, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK + 10, - }, - { - address: TOKEN_ADDRESS_3_MOCK, - chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK + 20, - }, - { - address: NATIVE_TOKEN_ADDRESS, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: 0, - }, - { - address: NATIVE_TOKEN_ADDRESS, - chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: 1, - }, - ] as unknown as ReturnType); - - runHook(); - - expect(setPayTokenMock).toHaveBeenCalledWith({ - address: TOKEN_ADDRESS_3_MOCK, - chainId: CHAIN_ID_2_MOCK, - }); - }); - - it('always selects target token if hardware wallet', () => { - useTokensWithBalanceMock.mockReturnValue([ - { - address: TOKEN_ADDRESS_1_MOCK, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK - 1, - }, + it('selects target token if hardware wallet', () => { + useTransactionPayAvailableTokensMock.mockReturnValue([ { address: TOKEN_ADDRESS_2_MOCK, chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK - 2, }, { address: TOKEN_ADDRESS_1_MOCK, chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK + 10, }, { address: TOKEN_ADDRESS_3_MOCK, chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK + 20, }, - { - address: NATIVE_TOKEN_ADDRESS, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: 1, - }, - { - address: NATIVE_TOKEN_ADDRESS, - chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: 1, - }, - ] as unknown as ReturnType); + ] as AssetType[]); isHardwareAccountMock.mockReturnValue(true); @@ -336,53 +198,13 @@ describe('useAutomaticTransactionPayToken', () => { }); }); - it('returns number of tokens with balance', () => { - useTokensWithBalanceMock.mockReturnValue([ - { - address: TOKEN_ADDRESS_1_MOCK, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK - 1, - }, - { - address: TOKEN_ADDRESS_2_MOCK, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK - 2, - }, - { - address: TOKEN_ADDRESS_1_MOCK, - chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK + 10, - }, - { - address: TOKEN_ADDRESS_3_MOCK, - chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK + 20, - }, - { - address: NATIVE_TOKEN_ADDRESS, - chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: 1, - }, - { - address: NATIVE_TOKEN_ADDRESS, - chainId: CHAIN_ID_2_MOCK, - tokenFiatAmount: 1, - }, - ] as unknown as ReturnType); - - const { result } = runHook(); - - expect(result.current.count).toBe(6); - }); - it('selected nothing if disabled', () => { - useTokensWithBalanceMock.mockReturnValue([ + useTransactionPayAvailableTokensMock.mockReturnValue([ { address: TOKEN_ADDRESS_1_MOCK, chainId: CHAIN_ID_1_MOCK, - tokenFiatAmount: REQUIRED_BALANCE_MOCK, }, - ] as unknown as ReturnType); + ] as AssetType[]); runHook({ disable: true }); diff --git a/app/components/Views/confirmations/hooks/pay/useAutomaticTransactionPayToken.ts b/app/components/Views/confirmations/hooks/pay/useAutomaticTransactionPayToken.ts index c8fed86f528d..b53456f0eba0 100644 --- a/app/components/Views/confirmations/hooks/pay/useAutomaticTransactionPayToken.ts +++ b/app/components/Views/confirmations/hooks/pay/useAutomaticTransactionPayToken.ts @@ -1,149 +1,136 @@ -import { useSelector } from 'react-redux'; -import { useTokensWithBalance } from '../../../../UI/Bridge/hooks/useTokensWithBalance'; -import { selectEnabledSourceChains } from '../../../../../core/redux/slices/bridge'; import { useTransactionMetadataRequest } from '../transactions/useTransactionMetadataRequest'; -import { orderBy } from 'lodash'; import { useEffect, useMemo, useRef } from 'react'; import { Hex } from 'viem'; import { createProjectLogger } from '@metamask/utils'; import { useTransactionPayToken } from './useTransactionPayToken'; -import { BridgeToken } from '../../../../UI/Bridge/types'; import { isHardwareAccount } from '../../../../../util/address'; import { TransactionMeta } from '@metamask/transaction-controller'; -import { getRequiredBalance } from '../../utils/transaction-pay'; -import { getNativeTokenAddress } from '../../utils/asset'; import { useTransactionPayRequiredTokens } from './useTransactionPayData'; +import { useTransactionPayAvailableTokens } from './useTransactionPayAvailableTokens'; +import { AssetType } from '../../types/token'; const log = createProjectLogger('transaction-pay'); -export interface BalanceOverride { - address: Hex; - balance: number; - chainId: Hex; -} - export function useAutomaticTransactionPayToken({ - countOnly = false, disable = false, }: { - countOnly?: boolean; disable?: boolean; } = {}) { const isUpdated = useRef(false); - const supportedChains = useSelector(selectEnabledSourceChains); const { setPayToken } = useTransactionPayToken(); const requiredTokens = useTransactionPayRequiredTokens(); + const tokens = useTransactionPayAvailableTokens(); + + const tokensWithBalance = useMemo( + () => tokens.filter((t) => !t.disabled), + [tokens], + ); const transactionMeta = useTransactionMetadataRequest() ?? ({ txParams: {} } as TransactionMeta); const { - chainId, txParams: { from }, } = transactionMeta; - const chainIds = useMemo( - () => (!isUpdated.current ? supportedChains.map((c) => c.chainId) : []), - [supportedChains], + const isHardwareWallet = useMemo( + () => isHardwareAccount(from ?? '') ?? false, + [from], ); - const tokens = useTokensWithBalance({ chainIds }); - const isHardwareWallet = isHardwareAccount(from ?? ''); - const requiredBalance = getRequiredBalance(transactionMeta); - - let automaticToken: { address: string; chainId?: string } | undefined; - let count = 0; - - const nativeTokenAddress = getNativeTokenAddress(chainId as Hex); - - if (!disable && (!isUpdated.current || countOnly)) { - const targetToken = - requiredTokens.find((token) => token.address !== nativeTokenAddress) ?? - requiredTokens[0]; - - const sufficientBalanceTokens = orderBy( - tokens.filter((token) => - isTokenSupported(token, tokens, requiredBalance), - ), - (token) => token?.tokenFiatAmount ?? 0, - 'desc', - ); - - count = sufficientBalanceTokens.length; - - const requiredToken = sufficientBalanceTokens.find( - (token) => - token.address === targetToken?.address && token.chainId === chainId, - ); - - const sameChainHighestBalanceToken = sufficientBalanceTokens?.find( - (token) => token.chainId === chainId, - ); - - const alternateChainHighestBalanceToken = sufficientBalanceTokens?.find( - (token) => token.chainId !== chainId, - ); - - const targetTokenFallback = targetToken - ? { - address: targetToken.address, - chainId, - } - : undefined; - - automaticToken = - requiredToken ?? - sameChainHighestBalanceToken ?? - alternateChainHighestBalanceToken ?? - targetTokenFallback; - - if (isHardwareWallet) { - automaticToken = targetTokenFallback; - } - } + const targetToken = useMemo( + () => requiredTokens.find((token) => !token.allowUnderMinimum), + [requiredTokens], + ); useEffect(() => { - if ( - isUpdated.current || - !automaticToken || - !requiredTokens?.length || - countOnly - ) { + if (disable || isUpdated.current) { + return; + } + + const automaticToken = getBestToken({ + isHardwareWallet, + targetToken, + tokens: tokensWithBalance, + }); + + if (!automaticToken) { + log('No automatic pay token found'); return; } setPayToken({ - address: automaticToken.address as Hex, - chainId: automaticToken.chainId as Hex, + address: automaticToken.address, + chainId: automaticToken.chainId, }); isUpdated.current = true; log('Automatically selected pay token', automaticToken); - }, [automaticToken, countOnly, isUpdated, requiredTokens, setPayToken]); - - return { count }; + }, [ + disable, + isHardwareWallet, + requiredTokens, + setPayToken, + targetToken, + tokensWithBalance, + ]); } -function isTokenSupported( - token: BridgeToken, - tokens: BridgeToken[], - requiredBalance: number | undefined, -): boolean { - const nativeTokenAddress = getNativeTokenAddress(token.chainId as Hex); +function getBestToken({ + isHardwareWallet, + targetToken, + tokens, +}: { + isHardwareWallet: boolean; + targetToken?: { address: Hex; chainId: Hex }; + tokens: AssetType[]; +}): { address: Hex; chainId: Hex } | undefined { + const targetTokenFallback = targetToken + ? { + address: targetToken.address, + chainId: targetToken.chainId, + } + : undefined; + + if (isHardwareWallet) { + return targetTokenFallback; + } - const nativeToken = tokens.find( - (t) => t.address === nativeTokenAddress && t.chainId === token.chainId, + const requiredToken = tokens.find( + (t) => + t.address.toLowerCase() === targetToken?.address.toLowerCase() && + t.chainId === targetToken?.chainId, ); - const tokenAmount = token?.tokenFiatAmount ?? 0; + if (requiredToken) { + return { + address: requiredToken.address as Hex, + chainId: requiredToken.chainId as Hex, + }; + } - const isTokenBalanceSufficient = - requiredBalance === undefined - ? tokenAmount > 0 - : tokenAmount >= requiredBalance; + const sameChainHighestBalanceToken = tokens.find( + (t) => t.chainId === targetToken?.chainId, + ); - const hasNativeBalance = (nativeToken?.tokenFiatAmount ?? 0) > 0; + if (sameChainHighestBalanceToken) { + return { + address: sameChainHighestBalanceToken.address as Hex, + chainId: sameChainHighestBalanceToken.chainId as Hex, + }; + } + + const alternateChainHighestBalanceToken = tokens.find( + (t) => t.chainId !== targetToken?.chainId, + ); + + if (alternateChainHighestBalanceToken) { + return { + address: alternateChainHighestBalanceToken.address as Hex, + chainId: alternateChainHighestBalanceToken.chainId as Hex, + }; + } - return isTokenBalanceSufficient && hasNativeBalance; + return targetTokenFallback; } diff --git a/app/components/Views/confirmations/hooks/pay/useTransactionPayMetrics.test.ts b/app/components/Views/confirmations/hooks/pay/useTransactionPayMetrics.test.ts index 191da8fe91a8..dfe782e10c05 100644 --- a/app/components/Views/confirmations/hooks/pay/useTransactionPayMetrics.test.ts +++ b/app/components/Views/confirmations/hooks/pay/useTransactionPayMetrics.test.ts @@ -14,7 +14,6 @@ import { useTransactionPayToken } from './useTransactionPayToken'; import { act } from '@testing-library/react-native'; import { updateConfirmationMetric } from '../../../../../core/redux/slices/confirmationMetrics'; import { TransactionType } from '@metamask/transaction-controller'; -import { useAutomaticTransactionPayToken } from './useAutomaticTransactionPayToken'; import { TransactionPayQuote, TransactionPayRequiredToken, @@ -26,12 +25,14 @@ import { useTransactionPayRequiredTokens, useTransactionPayTotals, } from './useTransactionPayData'; +import { useTransactionPayAvailableTokens } from './useTransactionPayAvailableTokens'; +import { AssetType } from '../../types/token'; jest.mock('./useTransactionPayToken'); -jest.mock('./useAutomaticTransactionPayToken'); jest.mock('../useTokenAmount'); jest.mock('../../../../../selectors/transactionPayController'); jest.mock('../pay/useTransactionPayData'); +jest.mock('./useTransactionPayAvailableTokens'); jest.mock('../../../../../core/redux/slices/confirmationMetrics', () => ({ ...jest.requireActual('../../../../../core/redux/slices/confirmationMetrics'), @@ -76,12 +77,13 @@ describe('useTransactionPayMetrics', () => { const updateConfirmationMetricMock = jest.mocked(updateConfirmationMetric); const useTransactionPayQuotesMock = jest.mocked(useTransactionPayQuotes); const useTransactionPayTotalsMock = jest.mocked(useTransactionPayTotals); + const useTransactionPayRequiredTokensMock = jest.mocked( useTransactionPayRequiredTokens, ); - const useAutomaticTransactionPayTokenMock = jest.mocked( - useAutomaticTransactionPayToken, + const useTransactionPayAvailableTokensMock = jest.mocked( + useTransactionPayAvailableTokens, ); beforeEach(() => { @@ -104,9 +106,13 @@ describe('useTransactionPayMetrics', () => { useTransactionPayQuotesMock.mockReturnValue([]); - useAutomaticTransactionPayTokenMock.mockReturnValue({ - count: 5, - }); + useTransactionPayAvailableTokensMock.mockReturnValue([ + {}, + {}, + {}, + {}, + {}, + ] as AssetType[]); }); it('does not update metrics if no pay token selected', async () => { diff --git a/app/components/Views/confirmations/hooks/pay/useTransactionPayMetrics.ts b/app/components/Views/confirmations/hooks/pay/useTransactionPayMetrics.ts index 3f7ecf2e34d5..e1e2ef0c33f6 100644 --- a/app/components/Views/confirmations/hooks/pay/useTransactionPayMetrics.ts +++ b/app/components/Views/confirmations/hooks/pay/useTransactionPayMetrics.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { updateConfirmationMetric } from '../../../../../core/redux/slices/confirmationMetrics'; import { useTransactionMetadataRequest } from '../transactions/useTransactionMetadataRequest'; @@ -7,7 +7,6 @@ import { Hex, Json } from '@metamask/utils'; import { TransactionType } from '@metamask/transaction-controller'; import { useTransactionPayToken } from './useTransactionPayToken'; import { BridgeToken } from '../../../../UI/Bridge/types'; -import { useAutomaticTransactionPayToken } from './useAutomaticTransactionPayToken'; import { getNativeTokenAddress } from '../../utils/asset'; import { hasTransactionType } from '../../utils/transaction'; import { @@ -17,6 +16,7 @@ import { } from './useTransactionPayData'; import { TransactionPayStrategy } from '@metamask/transaction-pay-controller'; import { BigNumber } from 'bignumber.js'; +import { useTransactionPayAvailableTokens } from './useTransactionPayAvailableTokens'; export function useTransactionPayMetrics() { const dispatch = useDispatch(); @@ -26,10 +26,12 @@ export function useTransactionPayMetrics() { const automaticPayToken = useRef(); const quotes = useTransactionPayQuotes(); const totals = useTransactionPayTotals(); + const tokens = useTransactionPayAvailableTokens(); - const { count: availableTokenCount } = useAutomaticTransactionPayToken({ - countOnly: true, - }); + const availableTokens = useMemo( + () => tokens.filter((t) => !t.disabled), + [tokens], + ); const transactionId = transactionMeta?.id ?? ''; const { chainId, type } = transactionMeta ?? {}; @@ -58,7 +60,7 @@ export function useTransactionPayMetrics() { properties.mm_pay_chain_presented = automaticPayToken.current?.chainId ?? null; - properties.mm_pay_payment_token_list_size = availableTokenCount; + properties.mm_pay_payment_token_list_size = availableTokens.length; } if (payToken && type === TransactionType.perpsDeposit) { diff --git a/app/components/Views/confirmations/hooks/pay/useTransactionPayToken.ts b/app/components/Views/confirmations/hooks/pay/useTransactionPayToken.ts index 605da0ba0ba8..a5bb24ec2462 100644 --- a/app/components/Views/confirmations/hooks/pay/useTransactionPayToken.ts +++ b/app/components/Views/confirmations/hooks/pay/useTransactionPayToken.ts @@ -5,6 +5,8 @@ import { RootState } from '../../../../../reducers'; import Engine from '../../../../../core/Engine'; import { selectTransactionPaymentTokenByTransactionId } from '../../../../../selectors/transactionPayController'; import { Hex } from '@metamask/utils'; +import { noop } from 'lodash'; +import EngineService from '../../../../../core/EngineService'; export function useTransactionPayToken() { const { id: transactionId } = useTransactionMetadataRequest() || { id: '' }; @@ -14,7 +16,7 @@ export function useTransactionPayToken() { ); const setPayToken = useCallback( - async (newPayToken: { address: Hex; chainId: Hex }) => { + (newPayToken: { address: Hex; chainId: Hex }) => { const { GasFeeController, NetworkController, TransactionPayController } = Engine.context; @@ -22,9 +24,9 @@ export function useTransactionPayToken() { newPayToken.chainId, ); - await GasFeeController.fetchGasFeeEstimates({ + GasFeeController.fetchGasFeeEstimates({ networkClientId, - }); + }).catch(noop); try { TransactionPayController.updatePaymentToken({ @@ -32,6 +34,8 @@ export function useTransactionPayToken() { tokenAddress: newPayToken.address, chainId: newPayToken.chainId, }); + + EngineService.flushState(); } catch (e) { console.error('Error updating payment token', e); } diff --git a/app/components/Views/confirmations/hooks/transactions/useTransferRecipient.ts b/app/components/Views/confirmations/hooks/transactions/useTransferRecipient.ts index 03b428ab63be..bf95906dce85 100644 --- a/app/components/Views/confirmations/hooks/transactions/useTransferRecipient.ts +++ b/app/components/Views/confirmations/hooks/transactions/useTransferRecipient.ts @@ -51,10 +51,15 @@ function getRecipientByType( data: string, transactionTo: string, ): string | undefined { - const dataRecipient = getTransactionDataRecipient(data); - const paramsRecipient = transactionTo; - - return type === TransactionType.simpleSend ? paramsRecipient : dataRecipient; + switch (type) { + case TransactionType.simpleSend: + return transactionTo; + case TransactionType.tokenMethodTransfer: + case TransactionType.tokenMethodTransferFrom: + return getTransactionDataRecipient(data); + default: + return undefined; + } } function getTransactionDataRecipient(data: string): string | undefined { diff --git a/app/components/Views/confirmations/hooks/useTokenAmount.ts b/app/components/Views/confirmations/hooks/useTokenAmount.ts index 1787ad5eda1e..7f8de624582f 100644 --- a/app/components/Views/confirmations/hooks/useTokenAmount.ts +++ b/app/components/Views/confirmations/hooks/useTokenAmount.ts @@ -32,7 +32,7 @@ import { ERC20_DEFAULT_DECIMALS, fetchErc20Decimals } from '../utils/token'; import { parseStandardTokenTransactionData } from '../utils/transaction'; import { useTransactionMetadataOrThrow } from './transactions/useTransactionMetadataRequest'; import useNetworkInfo from './useNetworkInfo'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { updateEditableParams } from '../../../../util/transaction-controller'; import { selectTokensByChainIdAndAddress } from '../../../../selectors/tokensController'; import { getTokenTransferData } from '../utils/transaction-pay'; @@ -130,7 +130,11 @@ export const useTokenAmount = ({ networkClientId, ); - const transactionData = parseStandardTokenTransactionData(tokenData?.data); + const transactionData = useMemo( + () => parseStandardTokenTransactionData(tokenData?.data), + [tokenData?.data], + ); + const recipient = transactionData?.args?._to; const updateTokenAmount = useCallback( diff --git a/app/core/EngineService/EngineService.ts b/app/core/EngineService/EngineService.ts index a602f55abd9f..feca935df658 100644 --- a/app/core/EngineService/EngineService.ts +++ b/app/core/EngineService/EngineService.ts @@ -83,6 +83,10 @@ export class EngineService { Logger.log('keyringController vault missing for UPDATE_BG_STATE_KEY'); } this.updateBatcher.add(controllerName); + + if (controllerName === 'ApprovalController') { + this.updateBatcher.flush(); + } }; BACKGROUND_STATE_CHANGE_EVENT_NAMES.forEach((eventName) => { @@ -177,6 +181,14 @@ export class EngineService { endTrace({ name: TraceName.EngineInitialization }); }; + /** + * Flush any pending controller state updates. + * Only necessary in rare cases where immediate state consistency is required. + */ + flushState() { + this.updateBatcher.flush(); + } + /** * Sets up persistence subscriptions for all engine controllers. * diff --git a/locales/languages/en.json b/locales/languages/en.json index b7dcfafe4bfb..90eb35bdccb3 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -86,6 +86,9 @@ "message": "Add less than {{amount}} or use a different token.", "title": "Insufficient funds" }, + "insufficient_pay_token_balance_fees_no_target": { + "message": "Add less or use a different token." + }, "insufficient_pay_token_native": { "message": "Not enough {{ticker}} to cover fees. Use a token on another network or add more {{ticker}} to continue.", "title": "Insufficient funds" diff --git a/package.json b/package.json index 0627f3f7ac66..5b780b15a9a5 100644 --- a/package.json +++ b/package.json @@ -176,7 +176,7 @@ "@scure/bip32": "1.7.0", "@metamask/snaps-sdk": "^10.0.0", "react-native@0.76.9": "patch:react-native@npm%3A0.76.9#./.yarn/patches/react-native-npm-0.76.9-1c25352097.patch", - "@metamask/transaction-controller@npm:^62.0.0": "patch:@metamask/transaction-controller@npm%3A62.0.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch" + "@metamask/transaction-controller@npm:^62.1.0": "patch:@metamask/transaction-controller@npm%3A62.1.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch" }, "dependencies": { "@config-plugins/detox": "^9.0.0", @@ -286,7 +286,7 @@ "@metamask/swappable-obj-proxy": "^2.1.0", "@metamask/swaps-controller": "^15.0.0", "@metamask/token-search-discovery-controller": "^4.0.0", - "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A62.0.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch", + "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A62.1.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch", "@metamask/transaction-pay-controller": "^10.0.0", "@metamask/tron-wallet-snap": "^1.9.1", "@metamask/utils": "^11.8.1", diff --git a/yarn.lock b/yarn.lock index 9eab6b6d2ba2..cc91332ab567 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9227,9 +9227,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:62.0.0": - version: 62.0.0 - resolution: "@metamask/transaction-controller@npm:62.0.0" +"@metamask/transaction-controller@npm:62.1.0": + version: 62.1.0 + resolution: "@metamask/transaction-controller@npm:62.1.0" dependencies: "@ethereumjs/common": "npm:^4.4.0" "@ethereumjs/tx": "npm:^5.4.0" @@ -9261,7 +9261,7 @@ __metadata: "@metamask/gas-fee-controller": ^26.0.0 "@metamask/network-controller": ^26.0.0 "@metamask/remote-feature-flag-controller": ^2.0.0 - checksum: 10/885217c920c29e953aec06c5d9e21ecd58847d5593e9e1f60a3e8e52f7de7869a087797cdf60c5022f12f0c87822b19c860c4ec7a9df1b2a0f140c7dfdaa25e3 + checksum: 10/2de5d4f36e2b5ddf5cee14a17484d0694fc7dd81bb568a51c712fde9eb0ab8395cae49efa66d5a9daefa86a2429e09561445e6dddc0281e9e645364a9aeeffed languageName: node linkType: hard @@ -9303,9 +9303,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@patch:@metamask/transaction-controller@npm%3A62.0.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch": - version: 62.0.0 - resolution: "@metamask/transaction-controller@patch:@metamask/transaction-controller@npm%3A62.0.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch::version=62.0.0&hash=1a3342" +"@metamask/transaction-controller@patch:@metamask/transaction-controller@npm%3A62.1.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch": + version: 62.1.0 + resolution: "@metamask/transaction-controller@patch:@metamask/transaction-controller@npm%3A62.1.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch::version=62.1.0&hash=1a3342" dependencies: "@ethereumjs/common": "npm:^4.4.0" "@ethereumjs/tx": "npm:^5.4.0" @@ -9337,7 +9337,7 @@ __metadata: "@metamask/gas-fee-controller": ^26.0.0 "@metamask/network-controller": ^26.0.0 "@metamask/remote-feature-flag-controller": ^2.0.0 - checksum: 10/9caf3dfa6d88dded658f7902e42c8c20b6916c21804a8c7f593cf37b88764732738e6379443a1faefed81ea0d58f4fbac269c85fc240fa98a61f7551ec7465c9 + checksum: 10/f21f02550da1230a7e0f51ebd5a828d7cbbdfbb5d5d036ecb3e5f7d903b8dff15fb5627b675fd477f158a52722ab1daa23a103bef47be08f4a96391a7fa4dcda languageName: node linkType: hard @@ -35392,7 +35392,7 @@ __metadata: "@metamask/test-dapp-multichain": "npm:^0.17.1" "@metamask/test-dapp-solana": "npm:^0.3.0" "@metamask/token-search-discovery-controller": "npm:^4.0.0" - "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A62.0.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch" + "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A62.1.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch" "@metamask/transaction-pay-controller": "npm:^10.0.0" "@metamask/tron-wallet-snap": "npm:^1.9.1" "@metamask/utils": "npm:^11.8.1" From 583e05ac1d9f477d2db1866761a99b9eca48efc6 Mon Sep 17 00:00:00 2001 From: Alejandro Garcia Anglada Date: Fri, 21 Nov 2025 15:04:35 +0100 Subject: [PATCH 03/10] fix: cp-7.60.0 bump tron 1.10 (#23106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Upgrading tron to `1.10.0` https://github.com/MetaMask/snap-tron-wallet/releases/tag/v1.10.0 ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/22889 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) is generating a summary for commit cfa573af4c5284b2e9def59aa281f5f38b601a3e. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 5b780b15a9a5..824d35d63ef8 100644 --- a/package.json +++ b/package.json @@ -288,7 +288,7 @@ "@metamask/token-search-discovery-controller": "^4.0.0", "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A62.1.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch", "@metamask/transaction-pay-controller": "^10.0.0", - "@metamask/tron-wallet-snap": "^1.9.1", + "@metamask/tron-wallet-snap": "^1.10.0", "@metamask/utils": "^11.8.1", "@ngraveio/bc-ur": "^1.1.6", "@nktkas/hyperliquid": "^0.25.9", diff --git a/yarn.lock b/yarn.lock index cc91332ab567..0f0d8c07e6a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9368,10 +9368,10 @@ __metadata: languageName: node linkType: hard -"@metamask/tron-wallet-snap@npm:^1.9.1": - version: 1.9.1 - resolution: "@metamask/tron-wallet-snap@npm:1.9.1" - checksum: 10/9880fce865211ed2d58b162f1ba0e41a7aa43c0180319d008b0a8a247a678284009592aeef50c25c8d4eb6393fdc9663d48a2261d7d03658e0850366052d6719 +"@metamask/tron-wallet-snap@npm:^1.10.0": + version: 1.10.0 + resolution: "@metamask/tron-wallet-snap@npm:1.10.0" + checksum: 10/c9b2f9cae0a2f9dcfd43934bd4321ed029e395122b61f1f3a8c3780dd4b44ac8669139b382da8b9230123639bf7a7554206223e3127d3e3317dfb802190cda46 languageName: node linkType: hard @@ -35394,7 +35394,7 @@ __metadata: "@metamask/token-search-discovery-controller": "npm:^4.0.0" "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A62.1.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch" "@metamask/transaction-pay-controller": "npm:^10.0.0" - "@metamask/tron-wallet-snap": "npm:^1.9.1" + "@metamask/tron-wallet-snap": "npm:^1.10.0" "@metamask/utils": "npm:^11.8.1" "@ngraveio/bc-ur": "npm:^1.1.6" "@nktkas/hyperliquid": "npm:^0.25.9" From 77989e4b817a6f7ba4999c7bf33f077d0925b38b Mon Sep 17 00:00:00 2001 From: abretonc7s <107169956+abretonc7s@users.noreply.github.com> Date: Fri, 21 Nov 2025 22:06:11 +0800 Subject: [PATCH 04/10] =?UTF-8?q?fix(perps):=20increase=20bottom=20padding?= =?UTF-8?q?=20in=20PerpsTabView=20for=20navigation=20cl=E2=80=A6=20(#23105?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fixed bottom padding in the Perps tab view where the "Start trading" button was overlapping with the bottom navigation's "+" action button, particularly noticeable on Android. Increased bottom padding from 12px to 20px for both the positions/orders view and empty state to provide adequate clearance. **Changes:** - Updated `tradeInfoContainer` style: paddingBottom from 12 to 30 - Added `emptyStateContainer` style with paddingBottom: 30 - Wrapped `PerpsEmptyState` component with styled View to apply consistent padding ## **Changelog** CHANGELOG entry: Fixed bottom padding in Perps tab to prevent overlap with bottom navigation button ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: Perps tab bottom padding Scenario: user views Perps tab with positions/orders Given user has open positions or orders in the Perps tab When user navigates to the Perps tab Then the "Start trading" button should have adequate clearance from the bottom navigation And the button should not overlap with the "+" action button Scenario: user views empty Perps tab Given user has no positions or orders When user navigates to the Perps tab Then the empty state should have adequate clearance from the bottom navigation And content should not overlap with the "+" action button ``` ## **Screenshots/Recordings** ### **Before** image image ### **After** image ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Increase bottom padding for trade info and empty states in `PerpsTabView` and wrap empty state in a padded container to avoid overlap with bottom navigation. > > - **UI (PerpsTabView)**: > - **Padding adjustments**: > - `tradeInfoContainer` `paddingBottom`: `12` -> `30` in `app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.styles.ts`. > - Add `emptyStateContainer` with `paddingBottom: 30`. > - **Empty state layout**: > - Wrap `PerpsEmptyState` in a `View` using `styles.emptyStateContainer` in `app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.tsx` to apply consistent bottom spacing. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f5dc7adcbc9acaf7146e338e965ae7e0c194a908. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../Perps/Views/PerpsTabView/PerpsTabView.styles.ts | 5 ++++- .../UI/Perps/Views/PerpsTabView/PerpsTabView.tsx | 12 +++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.styles.ts b/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.styles.ts index 8dd9f7eb6e1e..ee6b89faff00 100644 --- a/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.styles.ts +++ b/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.styles.ts @@ -7,7 +7,10 @@ const styleSheet = (params: { theme: Theme }) => { return StyleSheet.create({ tradeInfoContainer: { - paddingBottom: 12, + paddingBottom: 30, + }, + emptyStateContainer: { + paddingBottom: 30, }, wrapper: { flex: 1, diff --git a/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.tsx b/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.tsx index b2b4d868f11e..54296afcb6d9 100644 --- a/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.tsx +++ b/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.tsx @@ -255,11 +255,13 @@ const PerpsTabView: React.FC = () => { }} > {!isInitialLoading && hasNoPositionsOrOrders ? ( - + + + ) : ( {renderPositionsSection()} From f4e23e315275feecfe606b3c38f98f16ce24f40e Mon Sep 17 00:00:00 2001 From: George Weiler Date: Fri, 21 Nov 2025 07:13:19 -0700 Subject: [PATCH 05/10] feat(ramps): adds unsupported and error modals to ramp entrypoint (#23057) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adds error and unsupported routing handling to the `useRampNavigation` hook. The `goToBuy` function now checks the routing decision first and navigates to the appropriate modal when the routing decision is `ERROR` or `UNSUPPORTED`, before any other routing logic executes. **Changes:** - Added imports for `createEligibilityFailedModalNavigationDetails` and `createRampUnsupportedModalNavigationDetails` - Added early return checks in `goToBuy` to handle `UnifiedRampRoutingType.ERROR` and `UnifiedRampRoutingType.UNSUPPORTED` routing decisions - When `ERROR` is detected, navigates to the eligibility failed modal - When `UNSUPPORTED` is detected, navigates to the unsupported region modal This ensures users see appropriate error messages when ramp services are unavailable or unsupported in their region, rather than attempting to navigate to unavailable flows. ## **Changelog** CHANGELOG entry: Added error and unsupported region handling to ramp navigation flow ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: Ramp navigation error handling Scenario: user attempts to buy when routing decision is ERROR Given the app has a routing decision of ERROR When user triggers the buy flow Then the eligibility failed modal should be displayed Scenario: user attempts to buy when routing decision is UNSUPPORTED Given the app has a routing decision of UNSUPPORTED When user triggers the buy flow Then the unsupported region modal should be displayed ``` ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/6eb13363-2412-481b-b268-3ef1c3a58070 https://github.com/user-attachments/assets/5ffd32ef-6798-41a0-b868-dc258bb1b294 ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Adds early routing in `useRampNavigation.goToBuy` to show eligibility-failed or unsupported modals when unified V1 is enabled, with accompanying tests. > > - **Hook updates** > - In `app/components/UI/Ramp/hooks/useRampNavigation.ts`: > - `goToBuy` now checks `UnifiedRampRoutingType` first under unified V1. > - Routes to `createEligibilityFailedModalNavigationDetails()` on `ERROR` and to `createRampUnsupportedModalNavigationDetails()` on `UNSUPPORTED` (early return). > - Preserves existing token selection and smart routing for `DEPOSIT` vs `AGGREGATOR` when an `assetId` is present. > - **Tests** > - In `app/components/UI/Ramp/hooks/useRampNavigation.test.ts`: > - Adds test coverage for `ERROR` and `UNSUPPORTED` decisions navigating to the respective modals (with and without intent). > - Retains tests for token selection and smart routing behaviors. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7e686bbd218dbd4dbe019f1a37596f3773ac3837. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../UI/Ramp/hooks/useRampNavigation.test.ts | 78 +++++++++++++++++++ .../UI/Ramp/hooks/useRampNavigation.ts | 14 ++++ 2 files changed, 92 insertions(+) diff --git a/app/components/UI/Ramp/hooks/useRampNavigation.test.ts b/app/components/UI/Ramp/hooks/useRampNavigation.test.ts index f0fb9ef60ccb..8d8330afff77 100644 --- a/app/components/UI/Ramp/hooks/useRampNavigation.test.ts +++ b/app/components/UI/Ramp/hooks/useRampNavigation.test.ts @@ -11,6 +11,8 @@ import { getRampRoutingDecision, UnifiedRampRoutingType, } from '../../../../reducers/fiatOrders'; +import { createEligibilityFailedModalNavigationDetails } from '../components/EligibilityFailedModal/EligibilityFailedModal'; +import { createRampUnsupportedModalNavigationDetails } from '../components/RampUnsupportedModal/RampUnsupportedModal'; jest.mock('@react-navigation/native'); jest.mock('@react-navigation/compat', () => ({ @@ -122,6 +124,82 @@ describe('useRampNavigation', () => { mockUseRampsUnifiedV1Enabled.mockReturnValue(true); }); + describe('error and unsupported routing', () => { + it('navigates to eligibility failed modal when routing decision is ERROR', () => { + mockGetRampRoutingDecision.mockReturnValue( + UnifiedRampRoutingType.ERROR, + ); + const navDetails = createEligibilityFailedModalNavigationDetails(); + + const { result } = renderHookWithProvider(() => useRampNavigation()); + + result.current.goToBuy(); + + expect(mockNavigate).toHaveBeenCalledWith(...navDetails); + expect(mockCreateRampNavigationDetails).not.toHaveBeenCalled(); + expect(mockCreateDepositNavigationDetails).not.toHaveBeenCalled(); + expect( + mockCreateTokenSelectionNavigationDetails, + ).not.toHaveBeenCalled(); + }); + + it('navigates to eligibility failed modal when routing decision is ERROR with intent', () => { + mockGetRampRoutingDecision.mockReturnValue( + UnifiedRampRoutingType.ERROR, + ); + const intent = { assetId: 'eip155:1/erc20:0x123' }; + const navDetails = createEligibilityFailedModalNavigationDetails(); + + const { result } = renderHookWithProvider(() => useRampNavigation()); + + result.current.goToBuy(intent); + + expect(mockNavigate).toHaveBeenCalledWith(...navDetails); + expect(mockCreateRampNavigationDetails).not.toHaveBeenCalled(); + expect(mockCreateDepositNavigationDetails).not.toHaveBeenCalled(); + expect( + mockCreateTokenSelectionNavigationDetails, + ).not.toHaveBeenCalled(); + }); + + it('navigates to unsupported modal when routing decision is UNSUPPORTED', () => { + mockGetRampRoutingDecision.mockReturnValue( + UnifiedRampRoutingType.UNSUPPORTED, + ); + const navDetails = createRampUnsupportedModalNavigationDetails(); + + const { result } = renderHookWithProvider(() => useRampNavigation()); + + result.current.goToBuy(); + + expect(mockNavigate).toHaveBeenCalledWith(...navDetails); + expect(mockCreateRampNavigationDetails).not.toHaveBeenCalled(); + expect(mockCreateDepositNavigationDetails).not.toHaveBeenCalled(); + expect( + mockCreateTokenSelectionNavigationDetails, + ).not.toHaveBeenCalled(); + }); + + it('navigates to unsupported modal when routing decision is UNSUPPORTED with intent', () => { + mockGetRampRoutingDecision.mockReturnValue( + UnifiedRampRoutingType.UNSUPPORTED, + ); + const intent = { assetId: 'eip155:1/erc20:0x123' }; + const navDetails = createRampUnsupportedModalNavigationDetails(); + + const { result } = renderHookWithProvider(() => useRampNavigation()); + + result.current.goToBuy(intent); + + expect(mockNavigate).toHaveBeenCalledWith(...navDetails); + expect(mockCreateRampNavigationDetails).not.toHaveBeenCalled(); + expect(mockCreateDepositNavigationDetails).not.toHaveBeenCalled(); + expect( + mockCreateTokenSelectionNavigationDetails, + ).not.toHaveBeenCalled(); + }); + }); + describe('token selection routing', () => { it('navigates to TokenSelection when no assetId is provided', () => { const mockNavDetails = [ diff --git a/app/components/UI/Ramp/hooks/useRampNavigation.ts b/app/components/UI/Ramp/hooks/useRampNavigation.ts index ac603fea1543..dc829c1c3a77 100644 --- a/app/components/UI/Ramp/hooks/useRampNavigation.ts +++ b/app/components/UI/Ramp/hooks/useRampNavigation.ts @@ -13,6 +13,8 @@ import { getRampRoutingDecision, UnifiedRampRoutingType, } from '../../../../reducers/fiatOrders'; +import { createRampUnsupportedModalNavigationDetails } from '../components/RampUnsupportedModal/RampUnsupportedModal'; +import { createEligibilityFailedModalNavigationDetails } from '../components/EligibilityFailedModal/EligibilityFailedModal'; enum RampMode { AGGREGATOR = 'AGGREGATOR', @@ -45,6 +47,18 @@ export const useRampNavigation = () => { options || {}; if (isRampsUnifiedV1Enabled && !overrideUnifiedRouting) { + if (rampRoutingDecision === UnifiedRampRoutingType.ERROR) { + navigation.navigate( + ...createEligibilityFailedModalNavigationDetails(), + ); + return; + } + + if (rampRoutingDecision === UnifiedRampRoutingType.UNSUPPORTED) { + navigation.navigate(...createRampUnsupportedModalNavigationDetails()); + return; + } + // If no assetId is provided, route to TokenSelection if (!intent?.assetId) { navigation.navigate(...createTokenSelectionNavDetails()); From 54f8a7445537904e4edb15f2b9929036ccb2dad7 Mon Sep 17 00:00:00 2001 From: Michal Szorad Date: Fri, 21 Nov 2025 15:41:38 +0100 Subject: [PATCH 06/10] fix(perps): use static filter tab values to match FilterTab type cp-7.60.0 (#23102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fixed an issue where the `filterTabs` array in `PerpsTransactionsView` was using translated strings, which could cause mismatches with the filter logic that expects hardcoded values ('Trades', 'Orders', 'Funding', 'Deposits'). ## **Changelog** CHANGELOG entry: Fixed an issue where orders, and deposits would not show if the user is not using english locale ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TAT-2093 ## **Manual testing steps** ```gherkin Feature: Perps Transactions View Filter Tabs Scenario: user switches between transaction filter tabs Given the user is on the Perps Transactions screen And the user uses French language And the user has transaction history data When user taps on the "Orders" tab Then the Orders filter should be active And only order transactions should be displayed When user taps on the "Funding" tab Then the Funding filter should be active And only funding transactions should be displayed When user taps on the "Deposits" tab Then the Deposits filter should be active And only deposit/withdrawal transactions should be displayed When user taps on the "Trades" tab Then the Trades filter should be active And only trade transactions should be displayed ``` ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/53fd37db-a03c-4b68-985b-68d3beab20a7 ### **After** Simulator Screenshot - iPhone 16e -
2025-11-21 at 13 02 25 ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Use static `['Trades','Orders','Funding','Deposits']` for `filterTabs` in `PerpsTransactionsView` to align with `FilterTab` and avoid locale mismatches. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 22cf93de09b2214442803ad02461965840300a10. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../PerpsTransactionsView/PerpsTransactionsView.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsTransactionsView.tsx b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsTransactionsView.tsx index 68e871b5f182..b4fa29dec3fb 100644 --- a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsTransactionsView.tsx +++ b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsTransactionsView.tsx @@ -343,15 +343,7 @@ const PerpsTransactionsView: React.FC = () => { ); - const filterTabs: FilterTab[] = useMemo( - () => [ - strings('perps.transactions.tabs.trades'), - strings('perps.transactions.tabs.orders'), - strings('perps.transactions.tabs.funding'), - strings('perps.transactions.tabs.deposits'), - ], - [], - ); + const filterTabs: FilterTab[] = ['Trades', 'Orders', 'Funding', 'Deposits']; const filterTabDescription = useMemo(() => { if (activeFilter === 'Funding') { From 2dfbf91df282e7e58e79f17033119ab82c4359de Mon Sep 17 00:00:00 2001 From: Pedro Pablo Aste Kompen Date: Fri, 21 Nov 2025 12:09:44 -0300 Subject: [PATCH 07/10] refactor(ramp): deposit title to buy (#23070) ## **Description** This PR updates the Deposit BuildQuote screen title from "Deposit" to "Buy" to better align with user-facing terminology and improve clarity of the feature's purpose. **Context**: As part of TRAM-2853, we're standardizing the naming across the ramps experience. The Deposit feature is fundamentally a "buy crypto" flow, so the title should reflect that action. **Changes**: - Updated `deposit.buildQuote.title` in `locales/languages/en.json` from "Deposit" to "Buy" - Updated test snapshots to reflect the title change ## **Changelog** CHANGELOG entry: Changed Deposit BuildQuote screen title to "Buy" ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TRAM-2855 Refs: https://consensyssoftware.atlassian.net/browse/TRAM-2853 ## **Manual testing steps** ```gherkin Feature: Deposit BuildQuote Title Scenario: user views the Deposit BuildQuote screen Given the user has opened MetaMask Mobile And the user navigates to the Deposit flow When the user reaches the BuildQuote screen Then the screen title should display "Buy" instead of "Deposit" ``` ## **Screenshots/Recordings** ### **Before** image ### **After** image ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Renames the Ramp BuildQuote screen title from "Deposit" to "Buy" and updates corresponding snapshots and localization. > > - **Localization**: > - Update `deposit.buildQuote.title` in `locales/languages/en.json` from `"Deposit"` to `"Buy"`. > - **Tests**: > - Refresh `BuildQuote` snapshots to reflect UI text change from `"Deposit"` to `"Buy"` in `app/components/UI/Ramp/Deposit/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ab0383dd15367b8fa997184dfacc7bde15190edc. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../__snapshots__/BuildQuote.test.tsx.snap | 34 +++++++++---------- locales/languages/en.json | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/components/UI/Ramp/Deposit/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap b/app/components/UI/Ramp/Deposit/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap index 4bdcce5f13d2..fdbf13a0d2e3 100644 --- a/app/components/UI/Ramp/Deposit/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap +++ b/app/components/UI/Ramp/Deposit/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap @@ -231,7 +231,7 @@ exports[`BuildQuote Component Continue button functionality displays error when } } > - Deposit + Buy @@ -2105,7 +2105,7 @@ exports[`BuildQuote Component Continue button functionality displays error when } } > - Deposit + Buy @@ -3979,7 +3979,7 @@ exports[`BuildQuote Component Continue button functionality displays error when } } > - Deposit + Buy @@ -5853,7 +5853,7 @@ exports[`BuildQuote Component Keypad Functionality displays converted token amou } } > - Deposit + Buy @@ -7666,7 +7666,7 @@ exports[`BuildQuote Component Keypad Functionality updates amount when keypad is } } > - Deposit + Buy @@ -9478,7 +9478,7 @@ exports[`BuildQuote Component Payment Method Selection does not open payment met } } > - Deposit + Buy @@ -11291,7 +11291,7 @@ exports[`BuildQuote Component Payment Method Selection does not open payment met } } > - Deposit + Buy @@ -13197,7 +13197,7 @@ exports[`BuildQuote Component Payment Method Selection does not show the duratio } } > - Deposit + Buy @@ -14965,7 +14965,7 @@ exports[`BuildQuote Component Payment Method Selection shows the right duration } } > - Deposit + Buy @@ -16778,7 +16778,7 @@ exports[`BuildQuote Component Region Selection displays EUR currency when select } } > - Deposit + Buy @@ -18591,7 +18591,7 @@ exports[`BuildQuote Component Region Selection displays default US region on ini } } > - Deposit + Buy @@ -20404,7 +20404,7 @@ exports[`BuildQuote Component Region Selection does not open region modal when r } } > - Deposit + Buy @@ -22217,7 +22217,7 @@ exports[`BuildQuote Component Region Selection does not open region modal when r } } > - Deposit + Buy @@ -24123,7 +24123,7 @@ exports[`BuildQuote Component Token Selection does not open token modal when cry } } > - Deposit + Buy @@ -25934,7 +25934,7 @@ exports[`BuildQuote Component Token Selection does not open token modal when cry } } > - Deposit + Buy @@ -27838,7 +27838,7 @@ exports[`BuildQuote Component User Details Error displays user details error ale } } > - Deposit + Buy @@ -29744,7 +29744,7 @@ exports[`BuildQuote Component render matches snapshot 1`] = ` } } > - Deposit + Buy diff --git a/locales/languages/en.json b/locales/languages/en.json index 90eb35bdccb3..6a6245de4c7c 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -631,7 +631,7 @@ "unexpectedError": "An unexpected error occurred.", "quoteFetchError": "Failed to fetch quote.", "kycFormsFetchError": "Failed to fetch KYC forms.", - "title": "Deposit", + "title": "Buy", "limitExceeded": "This deposit would exceed your {{period}} limit. Your deposit including fees must be {{remaining}} or less.", "limitError": "Failed to check your deposit limits. Please try again later." }, From df58fc09fc19cbaf922898b8911567426d154a3d Mon Sep 17 00:00:00 2001 From: Salim TOUBAL Date: Fri, 21 Nov 2025 16:10:00 +0100 Subject: [PATCH 08/10] fix: cp-7.60.0 patch TokenBalancesController to resolve missing aggregated balance (#23113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** fix aggregated balances on mobile core PR: https://github.com/MetaMask/core/pull/7216 ## **Changelog** CHANGELOG entry: fix missing native token balances in wallet balance ## **Related issues** Fixes: #22775 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Patches @metamask/assets-controllers to lowercase account addresses when updating token balances, resolving missing aggregated balances. > > - **Assets Controllers Patch**: > - Normalize account addresses to lowercase in `dist/TokenBalancesController.{cjs,mjs}` when reading/writing `d.tokenBalances[account]` to ensure balance updates persist. > - **Dependencies**: > - Switch `@metamask/assets-controllers@89.0.1` to a Yarn `patch:` source in `package.json` and register the patch in `yarn.lock` (`.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch`). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0fb1c03d757dc093f3bcadfe0d9217c3ec11bfc5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- ...ts-controllers-npm-89.0.1-02fa7acd54.patch | 48 ++++++++++++++++ package.json | 2 +- yarn.lock | 56 ++++++++++++++++++- 3 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 .yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch diff --git a/.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch b/.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch new file mode 100644 index 000000000000..21be6c303395 --- /dev/null +++ b/.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch @@ -0,0 +1,48 @@ +diff --git a/dist/TokenBalancesController.cjs b/dist/TokenBalancesController.cjs +index 4918812dde60b8d0e24a7bded27d88f233968858..4e8018bce92b9e5d47fc40784409e16db22be615 100644 +--- a/dist/TokenBalancesController.cjs ++++ b/dist/TokenBalancesController.cjs +@@ -535,14 +535,16 @@ class TokenBalancesController extends (0, polling_controller_1.StaticIntervalPol + } + // Update with actual fetched balances only if the value has changed + aggregated.forEach(({ success, value, account, token, chainId }) => { +- var _a, _b, _c; ++ var _a, _b; + if (success && value !== undefined) { ++ // Ensure all accounts we add/update are in lower-case ++ const lowerCaseAccount = account.toLowerCase(); + const newBalance = (0, controller_utils_1.toHex)(value); + const tokenAddress = checksum(token); +- const currentBalance = d.tokenBalances[account]?.[chainId]?.[tokenAddress]; ++ const currentBalance = d.tokenBalances[lowerCaseAccount]?.[chainId]?.[tokenAddress]; + // Only update if the balance has actually changed + if (currentBalance !== newBalance) { +- ((_c = ((_a = d.tokenBalances)[_b = account] ?? (_a[_b] = {})))[chainId] ?? (_c[chainId] = {}))[tokenAddress] = newBalance; ++ ((_b = ((_a = d.tokenBalances)[lowerCaseAccount] ?? (_a[lowerCaseAccount] = {})))[chainId] ?? (_b[chainId] = {}))[tokenAddress] = newBalance; + } + } + }); +diff --git a/dist/TokenBalancesController.mjs b/dist/TokenBalancesController.mjs +index f64d13f8de56631345a44e6ebb025e62e03f51bc..99aa7f27c574c94b26daa56091ac50d15281dd30 100644 +--- a/dist/TokenBalancesController.mjs ++++ b/dist/TokenBalancesController.mjs +@@ -531,14 +531,16 @@ export class TokenBalancesController extends StaticIntervalPollingController() { + } + // Update with actual fetched balances only if the value has changed + aggregated.forEach(({ success, value, account, token, chainId }) => { +- var _a, _b, _c; ++ var _a, _b; + if (success && value !== undefined) { ++ // Ensure all accounts we add/update are in lower-case ++ const lowerCaseAccount = account.toLowerCase(); + const newBalance = toHex(value); + const tokenAddress = checksum(token); +- const currentBalance = d.tokenBalances[account]?.[chainId]?.[tokenAddress]; ++ const currentBalance = d.tokenBalances[lowerCaseAccount]?.[chainId]?.[tokenAddress]; + // Only update if the balance has actually changed + if (currentBalance !== newBalance) { +- ((_c = ((_a = d.tokenBalances)[_b = account] ?? (_a[_b] = {})))[chainId] ?? (_c[chainId] = {}))[tokenAddress] = newBalance; ++ ((_b = ((_a = d.tokenBalances)[lowerCaseAccount] ?? (_a[lowerCaseAccount] = {})))[chainId] ?? (_b[chainId] = {}))[tokenAddress] = newBalance; + } + } + }); diff --git a/package.json b/package.json index 824d35d63ef8..920d4cc0d1f1 100644 --- a/package.json +++ b/package.json @@ -197,7 +197,7 @@ "@metamask/address-book-controller": "^7.0.0", "@metamask/app-metadata-controller": "^2.0.0", "@metamask/approval-controller": "^8.0.0", - "@metamask/assets-controllers": "^89.0.1", + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A89.0.1#~/.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch", "@metamask/base-controller": "^9.0.0", "@metamask/bitcoin-wallet-snap": "^1.6.0", "@metamask/bridge-controller": "^61.0.0", diff --git a/yarn.lock b/yarn.lock index 0f0d8c07e6a1..1c2a7db6bd4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7326,7 +7326,7 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:^89.0.1": +"@metamask/assets-controllers@npm:89.0.1": version: 89.0.1 resolution: "@metamask/assets-controllers@npm:89.0.1" dependencies: @@ -7378,6 +7378,58 @@ __metadata: languageName: node linkType: hard +"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A89.0.1#~/.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch": + version: 89.0.1 + resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A89.0.1#~/.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch::version=89.0.1&hash=6be0d3" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@metamask/abi-utils": "npm:^2.0.3" + "@metamask/base-controller": "npm:^9.0.0" + "@metamask/contract-metadata": "npm:^2.4.0" + "@metamask/controller-utils": "npm:^11.15.0" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/keyring-api": "npm:^21.0.0" + "@metamask/messenger": "npm:^0.3.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/polling-controller": "npm:^15.0.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/snaps-sdk": "npm:^9.0.0" + "@metamask/snaps-utils": "npm:^11.0.0" + "@metamask/utils": "npm:^11.8.1" + "@types/bn.js": "npm:^5.1.5" + "@types/uuid": "npm:^8.3.0" + async-mutex: "npm:^0.5.0" + bitcoin-address-validation: "npm:^2.2.3" + bn.js: "npm:^5.2.1" + immer: "npm:^9.0.6" + lodash: "npm:^4.17.21" + multiformats: "npm:^9.9.0" + reselect: "npm:^5.1.1" + single-call-balance-checker-abi: "npm:^1.0.0" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/account-tree-controller": ^3.0.0 + "@metamask/accounts-controller": ^34.0.0 + "@metamask/approval-controller": ^8.0.0 + "@metamask/core-backend": ^4.1.0 + "@metamask/keyring-controller": ^24.0.0 + "@metamask/network-controller": ^25.0.0 + "@metamask/permission-controller": ^12.0.0 + "@metamask/phishing-controller": ^15.0.0 + "@metamask/preferences-controller": ^21.0.0 + "@metamask/providers": ^22.0.0 + "@metamask/snaps-controllers": ^14.0.0 + "@metamask/transaction-controller": ^61.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/b936b09bc22944626b3332844070c0fab559b7e3973873cc96c8321618e6879c1ee1215a588bb1f8f38029e4a69f796d01141ad1cb0726fd590df54ca111355b + languageName: node + linkType: hard + "@metamask/auth-network-utils@npm:^0.3.0": version: 0.3.1 resolution: "@metamask/auth-network-utils@npm:0.3.1" @@ -35291,7 +35343,7 @@ __metadata: "@metamask/address-book-controller": "npm:^7.0.0" "@metamask/app-metadata-controller": "npm:^2.0.0" "@metamask/approval-controller": "npm:^8.0.0" - "@metamask/assets-controllers": "npm:^89.0.1" + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A89.0.1#~/.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch" "@metamask/auto-changelog": "npm:^5.1.0" "@metamask/base-controller": "npm:^9.0.0" "@metamask/bitcoin-wallet-snap": "npm:^1.6.0" From ae23603cf4a1ff01164db19e86b9aec901206a49 Mon Sep 17 00:00:00 2001 From: Pedro Pablo Aste Kompen Date: Fri, 21 Nov 2025 12:11:50 -0300 Subject: [PATCH 09/10] refactor(ramp): sell menu button and build quote title (#23061) ## **Description** This PR updates the copy for the Sell/Withdraw functionality in the Ramps flow to improve clarity and consistency across the application. **What is the reason for the change?** The current implementation uses "Withdraw" terminology in various places when referring to the sell crypto functionality. This can be confusing for users as "Withdraw" typically implies moving funds from one account to another, rather than selling crypto for cash. **What is the improvement/solution?** Updated all user-facing strings to consistently use "Sell" instead of "Withdraw" for the sell crypto flow: 1. Fund Action Menu button and description 2. Asset Overview sell button 3. Build Quote screen titles (simplified from "Amount to buy/sell" to just "Buy/Sell") This creates a clearer mental model for users and aligns with the actual functionality - selling crypto for cash. ## **Changelog** CHANGELOG entry: Changed "Withdraw" to "Sell" in the Fund Action Menu and Asset Overview for improved clarity. ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TRAM-2854 (subtask of https://consensyssoftware.atlassian.net/browse/TRAM-2853) ## **Manual testing steps** ```gherkin Feature: Sell button copy changes Scenario: User views Fund Action Menu Given user is on the Wallet screen And user has a token with balance When user taps the "Buy" button on a token Then the Fund Action Menu should display And the sell option should read "Sell" (not "Withdraw") And the description should read "Sell crypto for cash" Scenario: User navigates to Build Quote for Sell Given user is on the Wallet screen And user has opened the Fund Action Menu When user taps the "Sell" button Then the Build Quote screen should open And the header title should display "Sell" (not "Amount to sell") Scenario: User navigates to Build Quote for Buy Given user is on the Wallet screen And user has opened the Fund Action Menu When user taps the "Buy" button Then the Build Quote screen should open And the header title should display "Buy" (not "Amount to buy") ``` ## **Screenshots/Recordings** | **Before** | **After** | |------------|-----------| | **Fund Action Menu - "Withdraw" button** | **Fund Action Menu - "Sell" button** | | image | image | | **Asset Overview - "Withdraw" button** | **Asset Overview - "Sell" button** | | image | image | | **Build Quote - "Amount to buy" title** | **Build Quote - "Buy" title** | | image | image | | **Build Quote - "Amount to sell" title** | **Build Quote - "Sell" title** | | image | image | ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Replaces "Withdraw" with "Sell" across ramp UI strings and updates Build Quote headers from "Amount to buy/sell" to "Buy/Sell", with snapshots refreshed. > > - **i18n (en)**: > - Update `fund_actionmenu.sell` and `asset_overview.sell_button` from `Withdraw` to `Sell` in `locales/languages/en.json`. > - Simplify Build Quote labels: `fiat_on_ramp_aggregator.amount_to_buy` -> `Buy`, `amount_to_sell` -> `Sell`. > - **UI tests**: > - Refresh snapshots in `app/components/UI/Ramp/Aggregator/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap` reflecting title changes from `Amount to buy/sell` to `Buy/Sell`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 93c5b2d800fca639c6dd2f91733a443bd7e62d57. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../__snapshots__/BuildQuote.test.tsx.snap | 32 +++++++++---------- locales/languages/en.json | 8 ++--- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/components/UI/Ramp/Aggregator/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap b/app/components/UI/Ramp/Aggregator/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap index 64c2c8aaaff7..bdbe4ab2c197 100644 --- a/app/components/UI/Ramp/Aggregator/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap +++ b/app/components/UI/Ramp/Aggregator/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap @@ -231,7 +231,7 @@ exports[`BuildQuote View Balance display displays balance from useBalance for no } } > - Amount to buy + Buy @@ -3090,7 +3090,7 @@ exports[`BuildQuote View Crypto Currency Data renders a special error page if cr } } > - Amount to buy + Buy @@ -3753,7 +3753,7 @@ exports[`BuildQuote View Crypto Currency Data renders a special error page if cr } } > - Amount to sell + Sell @@ -4513,7 +4513,7 @@ exports[`BuildQuote View Crypto Currency Data renders an error page when there i } } > - Amount to buy + Buy @@ -5273,7 +5273,7 @@ exports[`BuildQuote View Crypto Currency Data renders the loading page when cryp } } > - Amount to buy + Buy @@ -7836,7 +7836,7 @@ exports[`BuildQuote View Fiat Currency Data renders an error page when there is } } > - Amount to buy + Buy @@ -8596,7 +8596,7 @@ exports[`BuildQuote View Fiat Currency Data renders the loading page when fiats } } > - Amount to buy + Buy @@ -11068,7 +11068,7 @@ exports[`BuildQuote View Payment Method Data renders an error page when there is } } > - Amount to buy + Buy @@ -11828,7 +11828,7 @@ exports[`BuildQuote View Payment Method Data renders no icons if there are no pa } } > - Amount to buy + Buy @@ -14644,7 +14644,7 @@ exports[`BuildQuote View Payment Method Data renders the loading page when payme } } > - Amount to buy + Buy @@ -17382,7 +17382,7 @@ exports[`BuildQuote View Regions data renders an error page when there is a regi } } > - Amount to buy + Buy @@ -18142,7 +18142,7 @@ exports[`BuildQuote View Regions data renders the loading page when regions are } } > - Amount to buy + Buy @@ -20578,7 +20578,7 @@ exports[`BuildQuote View renders correctly 1`] = ` } } > - Amount to buy + Buy @@ -23340,7 +23340,7 @@ exports[`BuildQuote View renders correctly 2`] = ` } } > - Amount to sell + Sell @@ -26179,7 +26179,7 @@ exports[`BuildQuote View renders correctly when sdkError is present 1`] = ` } } > - Amount to buy + Buy @@ -26842,7 +26842,7 @@ exports[`BuildQuote View renders correctly when sdkError is present 2`] = ` } } > - Amount to sell + Sell diff --git a/locales/languages/en.json b/locales/languages/en.json index 6a6245de4c7c..886138d8fc92 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -3028,14 +3028,14 @@ "buy_description": "Good for buying a specific token", "buy_unified": "Buy", "buy_unified_description": "Buy crypto with cash", - "sell": "Withdraw", + "sell": "Sell", "sell_description": "Sell crypto for cash" }, "asset_overview": { "send_button": "Send", "buy_button": "Buy", "token_marketplace": "Token marketplace", - "sell_button": "Withdraw", + "sell_button": "Sell", "receive_button": "Receive", "portfolio_button": "Portfolio", "deposit_button": "Deposit", @@ -4542,8 +4542,8 @@ "quotes_timeout": "Quotes timeout", "request_new_quotes": "Please request new quotes to get the latest best rate.", "terms_of_service": "Terms of Service", - "amount_to_buy": "Amount to buy", - "amount_to_sell": "Amount to sell", + "amount_to_buy": "Buy", + "amount_to_sell": "Sell", "want_to_buy": "You want to buy", "want_to_sell": "You want to sell", "current_balance": "Current balance", From dd15576658e46fc2797dd3b041c1f9ad3c36c866 Mon Sep 17 00:00:00 2001 From: Pedro Pablo Aste Kompen Date: Fri, 21 Nov 2025 13:21:02 -0300 Subject: [PATCH 10/10] docs: update README branding and logo sizing (#23123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR updates the README.md to improve branding consistency and visual presentation: 1. **Updated main heading**: Changed from "MetaMask" to "MetaMask Mobile" to better reflect that this is the mobile repository 2. **Refined logo display**: Converted the logo from markdown image syntax to HTML `` tag with a fixed width of 50px for better size control and consistency These changes improve the README's visual hierarchy and make it immediately clear that this is the MetaMask Mobile repository. ## **Changelog** CHANGELOG entry: null ## **Related issues** Refs: ## **Manual testing steps** ```gherkin Feature: README documentation Scenario: user views the repository README Given the user navigates to the repository on GitHub When the user views the README.md Then the logo should display at 50px width And the main heading should read "MetaMask Mobile" ``` ## **Screenshots/Recordings** ### **Before** - Large logo using markdown syntax - Heading: "MetaMask" ### **After** - Compact logo (50px) using HTML img tag - Heading: "MetaMask Mobile" ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Update README: switch logo to 50px HTML image and rename main heading to “MetaMask Mobile.” > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a8669a1d99ade7501f75da9227e6fd579e91d568. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- README.md | 4 ++-- logo.png | Bin 5308 -> 3528 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 940055df9857..9e950686925b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -![MetaMask logo](logo.png?raw=true) +MetaMask logo -# MetaMask +# MetaMask Mobile [![CI](https://github.com/MetaMask/metamask-mobile/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/MetaMask/metamask-mobile/actions/workflows/ci.yml) [![CLA](https://github.com/MetaMask/metamask-mobile/actions/workflows/cla.yml/badge.svg?branch=main)](https://github.com/MetaMask/metamask-mobile/actions/workflows/cla.yml) diff --git a/logo.png b/logo.png index a28627a26b02b30a2b7fed5536c43af5b3b1f23c..5622df2bb4c36dd158863c3fe792350652a8436b 100644 GIT binary patch literal 3528 zcmV;(4L9vPva000erNklwg2LCm&=Y~60k-c6d@?{{X-+%q%h%$#%P%*;)@m;256 z`~CgR_no=-Zqg&e>&ZQ|_?x3U77fc=)1DDPOrP3A7ftL&Kq8{!udi+{OwR1SI61Rx z3m8)bE$1g^_6#Q+%impDUnpZS0x}^QURvFxbFZ!rSpWmG1q#Q(`7MDUx^*#$B_XyZ zL0Ujit|UVY$~zbWl*wc(TM!E?ieq()%hP0d?$vh)v6PJ8+e5g~DgV*}lHUYD5SYv_ zTzqJ53z#Grx@MXcw19QQ4lzVu5@(56lVjPM04Z#SKnMooaQ7W(kTF|l3nG!w6o@!0 zFpZcYcIexX2@owW1Ve$c2vD}Ng#)6NCmxD7VuhHALGHpCVpp=%^CKri458rsmFuFf zTt2eEOtT^`PckNm5n?6Bx>k3NIAw?w`XU1Yg>tO40Oli6#->0_%ae=)VuKjTv8m-O zDI5(tKnk050-+F!HERInLtVynwyc{Lm?Y0Wd_jmdpW|+q#TRfhd-z#7Ac^BwxPG0Fqy{9b1BO zTb8l1@+9Mf8xk>)W0#h*By&XM0m&@72!=s0tbzyGa$5j1%?f>al77Nx_%6qF+dLq( zdqGE>=NQ=X&` z@DaYsai}&0B$PnNfuS%MeGBRwm}yp&mnZ29e1ng2T({*cNskH@kaV)H0`SA&ORG1~ zwIu-4Y8jgXQC*&-6YvSX$#Ljb1thd={FcIBIXi>WN*Qx0Pckq7gMZVI!(c6ENP3P4 zKti1#It$?d9MP?n%pqGE0cM&NcI8Rh2p`~!95>YpK$;RR<-!p-B*zZz!sP79$>iMQ zp!&qJh<~{wnrOdiHQ5E*;e%;C#QcQf8$<%K5JG5~3^)YG%Jyj2c6?ep?KSBk3pT@c zFMS?Nz5I769x|+1nmi1xjd0x978sl%;vDJSB63P!s`0uc{%At{K3_vhO;(32!4FI| zwK=oPlI6_k%isixL-2gS9hXEP( zBBCYg;01@_c&X}720YNEq6(R?3AV}YEw^U@$k~>art0800+3F0+-0U_X>|Xm7?3+? z>GGRC{#EkqpSwfofUe5TEo?ySr5XlDcDrqH$Byowqf-osv?Tdw4{{i$UB8yffvy<< zB~zI)fOJ*15R_4p6=}4Z>a4G3Bmj&fO{ zYFzsJYwEDkK7i>0lzhxHfWTwUk_=*)Yl2XsaA%^Po8%97-)UO%pMQ5+vhqRz3T$c- zm`u5XJvu-No3Jq?AuC2)qC(2FmZh8_CLRD&6BLu^%J~8y`4p;@;IIK%G2#+cmK7Os z7QxHx4>$lQO=pYzbgr6R00fIolr7wGBhCtIFklEMBq$!T01Rx0DS=5_iA;cKH~SbQ zvjUF!@CW6|Jz?3(hV9=vILDuj8aUH(M)Y!HF$`6Io@}m7F?u z$SX#VJb2%6X4k%rfDlaY8wHuWWA2d=~_CuIENL@gkb=gt@zsDl>{)Y@tTXGdLo zOfl91LcQKh0aDj_!9OEUYr+;>3^DFutw7O4)VRWYR})t+ccr;Gy@nX;0V%0*bu1j; zxJhsS|GG1+y|+9gI^MB5IVJmDF0zzL5F)jcyIrqe3Xx)$p$E1VkyV3g)QH$2z zx=L<3Z;JiW=32@P%(`==0EmIN)-(bE$9#p1SKsnw4Z;W8;X@E%s*sOGg(q1JAd|u- zRfcOHj=&*TzWo02>f8A#>v0`ksRTa27ddXK)c_Js)!KVwQMbcM^pi9Wlwkk`KKWUp z;o@^7U$!!UggifB_*%tb{Gat(WTL$Jp@J&qn)G5`?$NdIX^n3*-CBsb3?Lx{LIw<8 zs{rGVEAP`U-?(8U$Wdv0&A()+a%8_uH*AypbS->a3k6E<9LWJv|NI!=;3uN~KH4_7 zOXrK2gTu=!H$~rcsk&25e7gBOn9#4=C+A>W*&g^d-nNrS`8#3B>)dkLQh8E9^0iFe z4@FWZS`g-~gofUv z#9jcR*09jJT;-_%J@frHP3OD`-jQCmbSe!q{)JKXftR2D?ibC8q`)U zIthr)fo~h+F+cKJWd|~p!D#=y(1%0-f>hBjw>*n-fdG}LMcG2IOSXt5YcQZwaCCcN z3>IO500bST?#dP`Pu~a-?$~sq*s%%rxa^#f83|oQ@m5Y{-`L zfBq*u_1v#Y>801MsuHJt$OhnOr?VaRBJrP>!3YHq3}=jR2j@Tm7%#s3k4i(a2YWLc zAq!w+Cc7WVas^9LKzI^q1H%055I{+ShyWwdw#H#Ec*KDX8IWbmEcUg3NdakJwY|p8 zJ^Sg}(eDrfY|j<%1kd`uW3>L@6Y<2%U8Bqg^Bf@A#jb4My!VR)y7q%#)5=f(O=vOR z`@ol-O#;vQ!#^evWUYUl`%KP)T4Klml9a8MRA<#7;0U}*m{WJ%{lJ&hp;bT617JV^ z2f6@IUXPXFK$|$`;&E4s%K(y0wswvn5D1D44#uFwjzY8dh)0H$@IV*9kv0GvP(g8H z6UB<80i?Jopgl_nf(!s!jBVxb^~ewx9)JUqH^|1}ldavHG6otzn{KKI>c9VBPB!p6Iv{snA~*zSlZg3as;1$zxH zY-Gw61Q!6|O;8HLx-T)m007RgQyWW(Tp_PNzkRVk{|R<-0w=|s?d9WpKTnVT=ou^e z%-#FR{S^t@`)s4i%l0o0_=aMNNPr^%q8cyOQ{*lH5^sspPmb}p@(SThSrDqYVyE3f zsgmor9tr#V^LL$K;=WnRfbZ;>#u0JmGo5|q+AVT>>0@jFgh~yf_{Dpov=j}*-Lt`S z@uKY&p!7Yn#a2i4zR6hzp%Mt4oB{uy#I*R>Uv8ip03go#h)K6QGpCJ}77$jT-OS>6 zKAQz4dwIM4yU!(}-#Rdn@TbqZ?JX_e$?@RQw!tK_hk+@A0|~s3b)%OhEg;DjFeH=( zAz5#viG$O08IwR%m!~>Ta?lx=6dY6&28cMOdXR`;MnDFl1daJX2Dz6amhJrpDE-E= z#lJjxJH(8Gq_ho0TsI0&SG+~Mjev;NjxT*KCH-K)$gJ3I3`WQ93iUG$h_H|^eFlce zw~R?3nk`RkW{~#>00030|4v7xz5oCK21!IgR09Brl3kjA*T8200000zS7M9FeL8dzc7@U@gIt(i!`(GGi^o%H+MTm5k6r)er6ebMn*;{cUyZ2 zT}9>pq~EWknH@bn;Sx}&kB<+Zj}V`my8~1}TwEN=F9;PBgxoVA9)7N#R=yBd50-z6 z{Ev>JorjIP6Wr6u&6V-5u9dZ$m!~u{^IxO?wtt_~)5-pSom@TsQ`UWf(7!uS0X}}{ zf3@#ZrT(H4TJBDE_s)OyWdx-DVg5htKR!~>zvlnvF#oRfAN0Od8GI?|f3HmjU!#Y^ z1OOl%S5thV?}*iI>KAMN&F^kP{H)`qofCF)xy3s`sD26 z)$g&)?z(rMPaS`@o*@G|f~3(z8KZ!}Ngt^J&%HRd7@wf^Be$1Jfm`;-Rj;2ZL9FLYDT0|#MOyZ{h6jj< z%9rSaUQBtccZGQW3_oj!yzNieG_a0n$CTdw?$=J!6}5Z|jS9-p-;ZIfsj z4*Bx2vvC1tL_9vwzAxfROEp!hx{xM!w^X8<2wG}#QFCvAoRNMf|JL<|yq4Tt+G48B z+e;&1QOLB|!A0<9er(9o8tpaBVL!M zShVhVwN7=e6|w>)k|7?Z(~(KJ5genT2`+D2QGvh8s)JUqiI88RE-p;%RV?H-^Iv*8 zy*_C1p#5yiA>ZoeD5;uTt=0*i#;6eYYt)`@PC~*M{6$EItYVjLi7T|+Qal}tK}uoY z@~h7leq1P-v?I%!r_aXrkab1|w5&hHC?;>g^W17y>`X7z-zYvS?YzviKn8zFy!l<^ z7qU-q^~cmPoqC-s&snZWmm}y;Ad{Q%Zs}7=vUmCrkvcKff}{y1e(Np6QCt zdQw>-3&SmJHZAbeo%p1((A{{|E9Vt`jIB?w#sifnE-eKe&K(X% zTt|Ui5yimHaCgPZllGwIScqC~?0~26l=*x10x*Dd2L_k zxp|9h-1dCRSXB;0;3WR{LwW}R&D?3EHoOu?%H_|q1Mw!pN=vO?ECn%8^!u7gMNEda z{d7YG@=|{aZpp@HSlU>^%u;O@I5w4-LR2JDOa1C^HjIbuk01=serN}%YXczh-(sI} zQil=yooAgs%$gK7uGa4gU`Uo%Lwe z%$VEX^w*LJQ9LxV0zZ{8W5%Rw%wmW+#f$!!2|0jqPp`~v<7?>~2AVQCL1 zu5{H>siQU;lUdSw_)t)7U=l>g^3(nk|6DRZm2HxvFy^S`TYB>Eu~@n2D+N=%nmXsu zqoXP+>w#1k+n`PZP!v)kovhWR{2}yp%5n|zHH8eF@X5wuLL9M)_2?!1sH<6*5GB>W zqH*AmYt|PV)H|IU^7XALQL361+veQHHWjBlSu>3=o+tt~{^fRc2o2hvf2!j#52BA! z>{~1*=$!%JI|Dg;Ks5L{XQ-%?o#c!GCRk`Qpf|aKY%2S5ybS}d4Ie|D;UcAJ!h8d}ET4j)7v=`eQ&BTg>Y!Ke^@b}kt?-CsVgSW7|feI?6jzQYv* z=lJk?or2A@ILG-A-U6nkg^=jNDFoTUXfFmm9sm2&$g3*yqPTQ>sRh<;hR8pRZDdF1 z!|@W>Fj$xQ!>8U2MI|K0JW0kK=Of-2y~aZ{iYZFWjQ#|_562`q`B$sNkx+IJ&c%+3 z!u&VN$w5#?dDrg&KGMP&LwmFR52fiG9j5xeJ~zJx+uK9IefZ6lYGiKbg=Sp|R079` zDNFq<88+Eyz{buPVT%wfU-YnLgFHtg8q&2-&Wf;(ibQN_hF|op0)r^Wa#Nwlr0DVU zmR0c155=(wAbeC186#|=Qb?S$WtNurUC}L78C0u1X1fCirFS|b-9P<_WMLx8H(WD+ z!C&qK0HXvRmCva*C8VBEW4||RbTv@zbfBGQE@h#X{8-QYF)nLK3GqFsX}i{5)O>4d zn-A4Y)r#^gNymy({8>CI!TO`UC;#FlLf`}*%TqJm&$!x*;G2%dvI8Y4vFROgh|@amQhoGGy5v!oR#$k5A42<3Ira?E?}Rr!qJmspy}v zCYXSzUM{+4w?GrC2!R-3ocey%y@Es%q_|6Rt5T!FW%Fl!$2g-C^#y`hUs+l69GCTj z1XrhMXFp4@Puhs^LO!x>74>(;O+S7cUg}FG0&F&#e4nZ2-zz?v35q$N9S_lRpkfSO_?`k$tJ0~D^5arQ z$6LZD1d?o4*>Wc56gYn(K(R!MbZYDaXOv*zF&J}SyjLI&2roDJHO1WYovJLzd1&&3 z)ze4sOlcw)xb(Gu1DgoY>9#RyJQB2qghDdx>oV~#GVUBgXYTlQ8#b4R8XO`l2U;b7 z<4(yFSol>ymd_6a8mV9kRC7W7gh#;N$5^1~88aLLLnZa)cyLW~>MVeqD%QWu6T2zY zmTsr1Qc}{IF})B2rc(8eK;***zd8L970qC&;vuhy@=-B*+NDm1Tb3WSdziIHJ-=9( z2n%&~1HCcX-)i9@M)>$MJ;wU$x@S1otX$^;dfKa_Ucm>=Vz8VdZP)>Wj~X=1^hmOn zn^uf0*(O}kgJm83nxEaeZm-Q+v41;|0Z90YF5mJjYOZLB7RWBPNj!0(;P&8xAr^>d zNLj3p<8w*Va+vIg@McDEe+!^>@j6||8J}Z^k$hv9@|+)Ip2Ck>?HQS2py}o#D<_<` zXaA5EsrtRNnzF^EUd7^jg3Orqm}GMAAc+^=r_+2T6i4!9oboqk#|M%i{y_r%^e)^4 zVxq_i3l^Uqv+HGYp}x_J&%V7<;D8iDM6I$-H|upEZifYpXl#6PVmHn&!nr;`Ahyg- zF3U$rBXLu{q3S5VP;K@^(i)PI#-xzM4u(8Am~kJ);X{oqzuj%u-CwSsS!@8HTl8!8 zIuy;s1}hsWgSBF>G~qXwCd0aNeFfUDb$xGMe@;Dw&FEb7ucvm$&*AF%!4~`tR@b^p zOhw4L91dPkTh!C(QsIxF+f}VzzX~O?G{oI0#GkkQBU0SqU?k)})TPG%MODUNAxndV zGzW_Yr`M$@Hp1;wes3{Mh91(@^CGEFqk#X!rCz<{=OCXhC)=boCb*Q^h~ZEu&XLy| zhrqU(W~AeV?kgPw859O%D4KfD30Dx0)(X+_K@^vwhg<(3yft&*@>g_TFTYlV1;YP0 z-IK@H6Xp6~HXrs-TsCbD_Rc(4O5B)4w%q0JY+~~iqCAvL?Smg?=s0h=q$j_1O4v|b zfE0i7fr!Oxf2WuHqSRC&mW>qOcRIB;_5Ef6)h}ku6cC3QcI(iA>-N^6Qlk6|pInhcU3#f4Spx_a+p{l zx)fhAE6Ikwbe!oC2kourqu;n*sNF3grBf?JYp}Ey0kSp21s-p;)eyqp&upSXXei9=h88Ex>Lg`2Mv65%H#C(hHF=19}eee zo@KXDvFJEkogLK-G|%qr4(DC2<%#NbG&dMFVpF)AtXUsO4Q>qN;-pL!QP=U^zvv{3 zg~!Ye=uKaB2v>Y*LfIV;~b8V!sqW-{8pVtaanL^~7O1j3~WEfZ=WNue`djFi~O z$bDl3?$>Uca@^3d$GlZXU@1U!;G)$k(YI#qI>LeR1vsq87@;SaKDGNCtHw+Uya^#H zDV%}dWP-!|;$&lm6w#88qs$4}KQJ8kzuM)Pl=Jp2fh_tA+b^>#fy1?jZvlWD;1jD;|I>Rsab<4SpBq&BW5Sk zFjpWUq!GiGZxdj^tQCY9W+;ZNttUDy^OPBDsDvGf(9GCS5<7ME9`5~?m#?T)uJ?7$ zIR=BOu-!}QFx(rJT~O8I((mF=k*{jB>m@pkHshe$1CMe&zF~PrDsyW;E;_K?m zjw5VZiqi7FU}CmqkoTSUypD9p;`GHbruC>8cRE_ZeOw60`I9r|ukia6v>L!oSE&=S z*)~{i^DvXH>dl7>*o;DFWbM=j57OqdJadmgX{pL5Fnj@*u?D6J1g`HAe|bx|7g1_36ew`XO9 zajzR|<#0zgH4X{$AStd%n^R3(O0tip0v&i)Vz+jp{I#0jqObz4o_ypbC^P*XSy@o} z#8@kayk)0QtK4eVz%@{$sv%!mWe#I9;8ypu#j%KB+6#=ukk=Fw>UN|f?wxVn(vdq1 zRVR8l$0G~Yh_83y-E7h8Ma4hq6re5Hs0{w1QIv(A)axmCHUjzgp-TgCK=(V*o=j-sq;xJ{P_*-dUSkA^;+f-dvRh_u*(#E zZJfsAQo_Ns7=ogobq|&rIOhuB+>T|7S6}r|-PHox`loPX#FnlO>;6ymo>Jzjzu6okOn3=e>nDIbL-w9+;De3#;qH$lNOY|dX?mM%gh@qy;EOE`PLtVcrA&Az$B{HPFz)4 zVQeQbaGjikGoA_ga||8*_rxfKV{_?6KTfCJZ7EH>i%}%=r^_rM{mNnrWQKR(TKpj6~l|#t2EnG zTithF^HHLhqklHR6lm}w#Q4oks|Hilz>Yg{Wr&xgEdt0Cl-IO&hTb@tcy(QY zLHo{<>T1iu`Q3o*4|i0WMP`HZh|OL2k2Hs%b|mVDHIfk=sy zMvF4$jfwQ=jQ#r%S?@3-OOaDrSM+Qh;hELklw&}LDRHp(vGmzqjFlBvbI_Id?(y5X zD3-F<7G%Z1HnYpq##Z$BaMTVQ>19*@`{u`y%9W0{+BHXZ;@CmLQ!3}be}rmEnu-