From c186ac44295d067905a2d0febc10b9e06dd9dda0 Mon Sep 17 00:00:00 2001 From: khanti42 Date: Tue, 24 Mar 2026 20:43:28 +0100 Subject: [PATCH 1/2] fix: hardware wallet eip 7702 issue (cp-7.71.0) (#27615) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR will provide a fix for hardware wallet to gas free network like Monad and Sei. Due to currently Hardware wallet is not supported for EIP 7702 gas sponsorship, and Swap feature is not working for hardware wallet user. This fix will fall back the Gasless transaction to User pay gas previous model so that user can still do the swap and sign transaction like bfore. This is temporately fix for current version of extensions, and we will do a proper support in the future. Similar to extension PR: https://github.com/MetaMask/metamask-extension/pull/40915 Ticket: https://consensyssoftware.atlassian.net/jira/software/c/projects/NEB/boards/3738/backlog?selectedIssue=NEB-767 CHANGELOG entry: Hardware wallet user will fall back to use `User pay gas` for those Gasless network due to hardware wallet not supported in Gasless network like Sei and Monad. Fixes: ```gherkin Feature: Gas sponsorship disabled for hardware wallet accounts Scenario: Hardware wallet user does not use gas sponsorship on sponsored network Given the user has added a hardware wallet account (Ledger or QR-based) And the hardware wallet account is selected as the active account And the user has added a gas-sponsored network (e.g. Monad) When the user attempts to perform a swap a dapp interaction or send a transaction on the sponsored network Then the transaction should not use gas sponsorship And the UI should not display any gas sponsorship labels (e.g. "No network fee", "Paid by MetaMask") And the user should see the normal network gas fee And the transaction should follow the standard user-pays-gas flow ``` > With HW account: Network list: Screenshot 2026-03-18 at 16 05 48 Tx flow: Screenshot 2026-03-18 at 15 53 04 Screenshot 2026-03-18 at 15 55 20 Screenshot 2026-03-18 at 15 55 47 > With HW account: Network list: Screenshot 2026-03-18 at 16 06 19 Tx flow: Screenshot 2026-03-18 at 15 49 55 Screenshot 2026-03-18 at 15 50 29 - [ ] 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. - [ ] 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] > **Medium Risk** > Touches gasless sponsorship and transaction publishing paths (including 7702 delegation), which can affect whether transactions are sponsored vs user-paid and could change behavior on supported chains. Changes are scoped to hardware-wallet detection gates with added tests, reducing regression risk. > > **Overview** > Hardware wallet accounts now **opt out of gasless / EIP-7702 sponsorship**, forcing swaps/bridge and confirmations to use the normal *user-pays-gas* path. > > This adds an `accountSupports7702` gate to `TransactionControllerInit` so `Delegation7702PublishHook` and `isEIP7702GasFeeTokensEnabled` only activate for keyrings that support 7702, and updates `useIsGaslessSupported`/`useIsGasIncluded7702Supported` (via new `useIsHardwareWalletForBridge`) to report unsupported for hardware signers. > > Network selection UI (`NetworkSelector`, `NetworkMultiSelectorList`, `CustomNetwork`) now hides the “No network fee” sponsored label for hardware wallets, and a patched `@metamask/bridge-status-controller` waits for approval tx confirmation when required. Tests were added/updated to cover the new hardware-wallet gating behavior. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 83f66fc1de28d1f8f4be8a455844a9bbf39fba4c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: metamaskbot Co-authored-by: Julien Fontanel Co-authored-by: Frederic HENG Co-authored-by: Arafet (CN - Hong Kong) <52028926+arafetbenmakhlouf@users.noreply.github.com> Co-authored-by: tommasini <46944231+tommasini@users.noreply.github.com> --- ...tus-controller-npm-68.1.0-8a2c809398.patch | 38 ++++++ ...tus-controller-npm-69.0.0-ec19aeeecf.patch | 38 ++++++ .../useBridgeQuoteRequest.test.ts | 44 +++++++ .../useIsGasIncluded7702Supported/index.ts | 8 +- .../useIsGasIncluded7702Supported.test.ts | 37 ++++++ .../index.test.ts | 56 +++++++++ .../useIsHardwareWalletForBridge/index.ts | 18 +++ app/components/UI/Bridge/utils/transaction.ts | 4 +- .../NetworkMultiSelectorList.test.tsx | 21 ++++ .../NetworkMultiSelectorList.tsx | 26 +++- .../Views/NetworkSelector/NetworkSelector.tsx | 15 ++- .../CustomNetworkView/CustomNetwork.tsx | 11 +- .../gas-fee-details-row.tsx | 3 +- .../hooks/gas/useIsGaslessSupported.ts | 10 +- .../transaction-controller-init.test.ts | 21 ++++ .../transaction-controller-init.ts | 23 +++- .../account-supports-7702.test.ts | 115 ++++++++++++++++++ .../transactions/account-supports-7702.ts | 51 ++++++++ package.json | 4 +- yarn.lock | 52 +++++++- 20 files changed, 575 insertions(+), 20 deletions(-) create mode 100644 .yarn/patches/@metamask-bridge-status-controller-npm-68.1.0-8a2c809398.patch create mode 100644 .yarn/patches/@metamask-bridge-status-controller-npm-69.0.0-ec19aeeecf.patch create mode 100644 app/components/UI/Bridge/hooks/useIsHardwareWalletForBridge/index.test.ts create mode 100644 app/components/UI/Bridge/hooks/useIsHardwareWalletForBridge/index.ts create mode 100644 app/util/transactions/account-supports-7702.test.ts create mode 100644 app/util/transactions/account-supports-7702.ts diff --git a/.yarn/patches/@metamask-bridge-status-controller-npm-68.1.0-8a2c809398.patch b/.yarn/patches/@metamask-bridge-status-controller-npm-68.1.0-8a2c809398.patch new file mode 100644 index 00000000000..d841b6a6b85 --- /dev/null +++ b/.yarn/patches/@metamask-bridge-status-controller-npm-68.1.0-8a2c809398.patch @@ -0,0 +1,38 @@ +diff --git a/dist/bridge-status-controller.cjs b/dist/bridge-status-controller.cjs +index b787174f2c8c448ed1ad9c8884204c5c8b6858be..af058623871badb4891564c003693ea19d0aa676 100644 +--- a/dist/bridge-status-controller.cjs ++++ b/dist/bridge-status-controller.cjs +@@ -834,7 +834,13 @@ class BridgeStatusController extends (0, polling_controller_1.StaticIntervalPoll + ? quoteResponse.approval + : undefined, quoteResponse.resetApproval, requireApproval); + approvalTxId = approvalTxMeta?.id; +- await (0, transaction_1.handleMobileHardwareWalletDelay)(requireApproval); ++ if (requireApproval && approvalTxMeta) { ++ await (0, transaction_1.handleMobileHardwareWalletDelay)(requireApproval); ++ await __classPrivateFieldGet(this, _BridgeStatusController_waitForTxConfirmation, "f").call(this, approvalTxMeta.id); ++ } ++ else { ++ await (0, transaction_1.handleMobileHardwareWalletDelay)(requireApproval); ++ } + // Generate actionId for pre-submission history (non-batch EVM only) + const actionId = (0, transaction_1.generateActionId)().toString(); + // Add pre-submission history keyed by actionId +diff --git a/dist/bridge-status-controller.mjs b/dist/bridge-status-controller.mjs +index 2fe71bdd2caf4d62f7946e9466b31367d360cd7c..fb9c0bc45abf88873452667b85ef2ea0cdfd929c 100644 +--- a/dist/bridge-status-controller.mjs ++++ b/dist/bridge-status-controller.mjs +@@ -831,7 +831,13 @@ export class BridgeStatusController extends StaticIntervalPollingController() { + ? quoteResponse.approval + : undefined, quoteResponse.resetApproval, requireApproval); + approvalTxId = approvalTxMeta?.id; +- await handleMobileHardwareWalletDelay(requireApproval); ++ if (requireApproval && approvalTxMeta) { ++ await handleMobileHardwareWalletDelay(requireApproval); ++ await __classPrivateFieldGet(this, _BridgeStatusController_waitForTxConfirmation, "f").call(this, approvalTxMeta.id); ++ } ++ else { ++ await handleMobileHardwareWalletDelay(requireApproval); ++ } + // Generate actionId for pre-submission history (non-batch EVM only) + const actionId = generateActionId().toString(); + // Add pre-submission history keyed by actionId diff --git a/.yarn/patches/@metamask-bridge-status-controller-npm-69.0.0-ec19aeeecf.patch b/.yarn/patches/@metamask-bridge-status-controller-npm-69.0.0-ec19aeeecf.patch new file mode 100644 index 00000000000..d308173b6b2 --- /dev/null +++ b/.yarn/patches/@metamask-bridge-status-controller-npm-69.0.0-ec19aeeecf.patch @@ -0,0 +1,38 @@ +diff --git a/dist/bridge-status-controller.cjs b/dist/bridge-status-controller.cjs +index ec19aeeecfa32a3cdf955ccc1152829ee4ddfd8f..d9b427f9f0f4b05238d79c731fc81566634a7c25 100644 +--- a/dist/bridge-status-controller.cjs ++++ b/dist/bridge-status-controller.cjs +@@ -855,7 +855,13 @@ + ? quoteResponse.approval + : undefined, quoteResponse.resetApproval, requireApproval); + approvalTxId = approvalTxMeta?.id; +- await (0, transaction_1.handleMobileHardwareWalletDelay)(requireApproval); ++ if (requireApproval && approvalTxMeta) { ++ await (0, transaction_1.handleMobileHardwareWalletDelay)(requireApproval); ++ await __classPrivateFieldGet(this, _BridgeStatusController_waitForTxConfirmation, "f").call(this, approvalTxMeta.id); ++ } ++ else { ++ await (0, transaction_1.handleMobileHardwareWalletDelay)(requireApproval); ++ } + // Generate actionId for pre-submission history (non-batch EVM only) + const actionId = (0, transaction_1.generateActionId)().toString(); + // Add pre-submission history keyed by actionId +diff --git a/dist/bridge-status-controller.mjs b/dist/bridge-status-controller.mjs +index a5661d63c35b5ad3526c1804936dc0e189c90c29..86efc019968599662466e643dae7002ebf5f5014 100644 +--- a/dist/bridge-status-controller.mjs ++++ b/dist/bridge-status-controller.mjs +@@ -852,7 +852,13 @@ + ? quoteResponse.approval + : undefined, quoteResponse.resetApproval, requireApproval); + approvalTxId = approvalTxMeta?.id; +- await handleMobileHardwareWalletDelay(requireApproval); ++ if (requireApproval && approvalTxMeta) { ++ await handleMobileHardwareWalletDelay(requireApproval); ++ await __classPrivateFieldGet(this, _BridgeStatusController_waitForTxConfirmation, "f").call(this, approvalTxMeta.id); ++ } ++ else { ++ await handleMobileHardwareWalletDelay(requireApproval); ++ } + // Generate actionId for pre-submission history (non-batch EVM only) + const actionId = generateActionId().toString(); + // Add pre-submission history keyed by actionId diff --git a/app/components/UI/Bridge/hooks/useBridgeQuoteRequest/useBridgeQuoteRequest.test.ts b/app/components/UI/Bridge/hooks/useBridgeQuoteRequest/useBridgeQuoteRequest.test.ts index ecf661c8351..eb9c0b82cfd 100644 --- a/app/components/UI/Bridge/hooks/useBridgeQuoteRequest/useBridgeQuoteRequest.test.ts +++ b/app/components/UI/Bridge/hooks/useBridgeQuoteRequest/useBridgeQuoteRequest.test.ts @@ -484,6 +484,50 @@ describe('useBridgeQuoteRequest', () => { }); }); + describe('hardware wallet accounts', () => { + it('sends gasIncluded and gasIncluded7702 false when useIsGasIncluded7702Supported dispatches false for hardware wallet', async () => { + // useIsGasIncluded7702Supported now incorporates the HW wallet check and + // dispatches isGasIncluded7702Supported=false for hardware wallets. + // useIsGasIncludedSTXSendBundleSupported already dispatches false for HW + // wallets via selectShouldUseSmartTransaction. + const testState = createBridgeTestState({ + bridgeReducerOverrides: { + isGasIncludedSTXSendBundleSupported: false, + isGasIncluded7702Supported: false, + sourceToken: { + address: '0xSourceToken', + chainId: '0x1', + decimals: 18, + symbol: 'SRC', + }, + destToken: { + address: '0xDestToken', + chainId: '0x1', + decimals: 18, + symbol: 'DEST', + }, + }, + }); + + const { result } = renderHookWithProvider(() => useBridgeQuoteRequest(), { + state: testState, + }); + + await act(async () => { + await result.current(); + jest.advanceTimersByTime(DEBOUNCE_WAIT); + }); + + expect(spyUpdateBridgeQuoteRequestParams).toHaveBeenCalledWith( + expect.objectContaining({ + gasIncluded: false, + gasIncluded7702: false, + }), + undefined, + ); + }); + }); + describe('insufficientBal parameter', () => { it('includes insufficientBal false when balance is sufficient', async () => { mockUseIsInsufficientBalance.mockReturnValue(false); diff --git a/app/components/UI/Bridge/hooks/useIsGasIncluded7702Supported/index.ts b/app/components/UI/Bridge/hooks/useIsGasIncluded7702Supported/index.ts index bafe1d3eac6..c3488f1cea1 100644 --- a/app/components/UI/Bridge/hooks/useIsGasIncluded7702Supported/index.ts +++ b/app/components/UI/Bridge/hooks/useIsGasIncluded7702Supported/index.ts @@ -8,13 +8,15 @@ import { formatChainIdToHex, isNonEvmChainId, } from '@metamask/bridge-controller'; +import { useIsHardwareWalletForBridge } from '../useIsHardwareWalletForBridge'; /** * Hook that determines if 7702 gasless support is available for bridge/swap. * Should be used at the page level (e.g., BridgeView) to avoid repeated calculations. * - * Requirement for 7702: + * Requirements for 7702: * - Relay must be supported (for 7702 delegation) + * - Source wallet must not be a hardware wallet * * @param chainId - The chain ID to check (can be Hex, CAIP, or other format) - only EVM chains are supported */ @@ -40,9 +42,11 @@ export const useIsGasIncluded7702Supported = ( return isRelaySupported(evmChainId as Hex); }, [evmChainId]); + const isHardwareWallet = useIsHardwareWalletForBridge(); + // 7702 is available when ALL conditions are met const isGasIncluded7702Supported = Boolean( - evmChainId && !!isRelaySupportedForChain, + evmChainId && !!isRelaySupportedForChain && !isHardwareWallet, ); useEffect(() => { diff --git a/app/components/UI/Bridge/hooks/useIsGasIncluded7702Supported/useIsGasIncluded7702Supported.test.ts b/app/components/UI/Bridge/hooks/useIsGasIncluded7702Supported/useIsGasIncluded7702Supported.test.ts index 7c31e0e6dd7..1a3d7c3a8de 100644 --- a/app/components/UI/Bridge/hooks/useIsGasIncluded7702Supported/useIsGasIncluded7702Supported.test.ts +++ b/app/components/UI/Bridge/hooks/useIsGasIncluded7702Supported/useIsGasIncluded7702Supported.test.ts @@ -7,8 +7,18 @@ import configureStore from '../../../../../util/test/configureStore'; // Mock dependencies jest.mock('../../../../../util/transactions/transaction-relay'); +jest.mock('../useIsHardwareWalletForBridge', () => ({ + useIsHardwareWalletForBridge: jest.fn().mockReturnValue(false), +})); const mockIsRelaySupported = jest.mocked(isRelaySupported); +const { useIsHardwareWalletForBridge } = jest.requireMock( + '../useIsHardwareWalletForBridge', +); +const mockUseIsHardwareWalletForBridge = + useIsHardwareWalletForBridge as jest.MockedFunction< + typeof useIsHardwareWalletForBridge + >; describe('useIsGasIncluded7702Supported', () => { const MAINNET_CHAIN_ID = '0x1' as Hex; @@ -28,6 +38,7 @@ describe('useIsGasIncluded7702Supported', () => { beforeEach(() => { jest.clearAllMocks(); mockIsRelaySupported.mockResolvedValue(false); + mockUseIsHardwareWalletForBridge.mockReturnValue(false); }); afterEach(() => { @@ -147,6 +158,32 @@ describe('useIsGasIncluded7702Supported', () => { }); }); + describe('when source wallet is a hardware account', () => { + it('updates isGasIncluded7702Supported to false even when relay is supported', async () => { + mockIsRelaySupported.mockResolvedValue(true); + mockUseIsHardwareWalletForBridge.mockReturnValue(true); + + const { store } = renderHookWithProvider( + () => useIsGasIncluded7702Supported(MAINNET_CHAIN_ID), + { state: {} }, + ); + + await expectGasIncluded7702State(store, false); + }); + + it('updates isGasIncluded7702Supported to false for hardware wallet regardless of chain', async () => { + mockIsRelaySupported.mockResolvedValue(true); + mockUseIsHardwareWalletForBridge.mockReturnValue(true); + + const { store } = renderHookWithProvider( + () => useIsGasIncluded7702Supported('eip155:59144'), // Linea + { state: {} }, + ); + + await expectGasIncluded7702State(store, false); + }); + }); + describe('edge cases', () => { it('handles case-insensitive chainId matching', async () => { mockIsRelaySupported.mockResolvedValue(true); diff --git a/app/components/UI/Bridge/hooks/useIsHardwareWalletForBridge/index.test.ts b/app/components/UI/Bridge/hooks/useIsHardwareWalletForBridge/index.test.ts new file mode 100644 index 00000000000..f1082ff5e73 --- /dev/null +++ b/app/components/UI/Bridge/hooks/useIsHardwareWalletForBridge/index.test.ts @@ -0,0 +1,56 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useSelector } from 'react-redux'; +import { useIsHardwareWalletForBridge } from './index'; +import { isHardwareAccount } from '../../../../../util/address'; + +jest.mock('react-redux', () => ({ + useSelector: jest.fn(), +})); + +jest.mock('../../../../../util/address', () => ({ + isHardwareAccount: jest.fn(), +})); + +const mockUseSelector = useSelector as jest.MockedFunction; +const mockIsHardwareAccount = isHardwareAccount as jest.MockedFunction< + typeof isHardwareAccount +>; + +describe('useIsHardwareWalletForBridge', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUseSelector.mockReturnValue(undefined); + mockIsHardwareAccount.mockReturnValue(false); + }); + + it('returns false when source wallet address is undefined', () => { + mockUseSelector.mockReturnValue(undefined); + + const { result } = renderHook(() => useIsHardwareWalletForBridge()); + + expect(result.current).toBe(false); + expect(mockIsHardwareAccount).not.toHaveBeenCalled(); + }); + + it('returns true when source wallet is a hardware account', () => { + const address = '0x1234567890123456789012345678901234567890'; + mockUseSelector.mockReturnValue(address); + mockIsHardwareAccount.mockReturnValue(true); + + const { result } = renderHook(() => useIsHardwareWalletForBridge()); + + expect(result.current).toBe(true); + expect(mockIsHardwareAccount).toHaveBeenCalledWith(address); + }); + + it('returns false when source wallet is not a hardware account', () => { + const address = '0x1234567890123456789012345678901234567890'; + mockUseSelector.mockReturnValue(address); + mockIsHardwareAccount.mockReturnValue(false); + + const { result } = renderHook(() => useIsHardwareWalletForBridge()); + + expect(result.current).toBe(false); + expect(mockIsHardwareAccount).toHaveBeenCalledWith(address); + }); +}); diff --git a/app/components/UI/Bridge/hooks/useIsHardwareWalletForBridge/index.ts b/app/components/UI/Bridge/hooks/useIsHardwareWalletForBridge/index.ts new file mode 100644 index 00000000000..c4ba9df5dee --- /dev/null +++ b/app/components/UI/Bridge/hooks/useIsHardwareWalletForBridge/index.ts @@ -0,0 +1,18 @@ +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { selectSourceWalletAddress } from '../../../../../selectors/bridge'; +import { isHardwareAccount } from '../../../../../util/address'; + +/** + * Returns whether the current bridge source account is a hardware wallet. + * Used to omit gas-included / 7702 params from bridge quote requests so responses + * are non-sponsored for hardware signers. + */ +export function useIsHardwareWalletForBridge(): boolean { + const walletAddress = useSelector(selectSourceWalletAddress); + + return useMemo( + () => Boolean(walletAddress && isHardwareAccount(walletAddress)), + [walletAddress], + ); +} diff --git a/app/components/UI/Bridge/utils/transaction.ts b/app/components/UI/Bridge/utils/transaction.ts index 77f981e8156..300a6fefb56 100644 --- a/app/components/UI/Bridge/utils/transaction.ts +++ b/app/components/UI/Bridge/utils/transaction.ts @@ -11,7 +11,9 @@ export const getIsBridgeTransaction = (txMeta: TransactionMeta) => { return ( origin === ORIGIN_METAMASK && (txMeta.type === TransactionType.bridgeApproval || - txMeta.type === TransactionType.bridge) + txMeta.type === TransactionType.bridge || + txMeta.type === TransactionType.swap || + txMeta.type === TransactionType.swapApproval) ); }; diff --git a/app/components/UI/NetworkMultiSelectorList/NetworkMultiSelectorList.test.tsx b/app/components/UI/NetworkMultiSelectorList/NetworkMultiSelectorList.test.tsx index 8d97ae24468..329ec5984d1 100644 --- a/app/components/UI/NetworkMultiSelectorList/NetworkMultiSelectorList.test.tsx +++ b/app/components/UI/NetworkMultiSelectorList/NetworkMultiSelectorList.test.tsx @@ -31,6 +31,27 @@ jest.mock('react-redux', () => ({ useSelector: jest.fn(), })); +// Avoid loading keyring-utils, keyring-api, and the network/Engine chain in this test +jest.mock('../../../selectors/accountsController', () => ({ + selectSelectedInternalAccountFormattedAddress: jest.fn(), +})); + +jest.mock('../../../util/address', () => ({ + isHardwareAccount: jest.fn(() => false), +})); + +jest.mock('@metamask/keyring-api', () => ({ + EntropySourceId: {}, + BtcMethod: {}, + EthMethod: {}, + SolAccountType: {}, + SolMethod: {}, + TrxMethod: {}, + isEvmAccountType: jest.fn(), + KeyringAccountType: {}, + EthScope: {}, +})); + jest.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: jest.fn(), })); diff --git a/app/components/UI/NetworkMultiSelectorList/NetworkMultiSelectorList.tsx b/app/components/UI/NetworkMultiSelectorList/NetworkMultiSelectorList.tsx index 9a3afef91ee..0ef4472b59f 100644 --- a/app/components/UI/NetworkMultiSelectorList/NetworkMultiSelectorList.tsx +++ b/app/components/UI/NetworkMultiSelectorList/NetworkMultiSelectorList.tsx @@ -59,6 +59,8 @@ import { selectEvmChainId } from '../../../selectors/networkController'; import { formatChainIdToCaip } from '@metamask/bridge-controller'; import { NETWORK_MULTI_SELECTOR_TEST_IDS } from '../NetworkMultiSelector/NetworkMultiSelector.constants'; import { getGasFeesSponsoredNetworkEnabled } from '../../../selectors/featureFlagController/gasFeesSponsored/index.ts'; +import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController'; +import { isHardwareAccount } from '../../../util/address'; import { strings } from '../../../../locales/i18n'; import TagColored, { TagColor, @@ -106,6 +108,12 @@ const NetworkMultiSelectList = ({ const isGasFeesSponsoredNetworkEnabled = useSelector( getGasFeesSponsoredNetworkEnabled, ); + const selectedAddress = useSelector( + selectSelectedInternalAccountFormattedAddress, + ); + const isHardwareWallet = Boolean( + selectedAddress && isHardwareAccount(selectedAddress), + ); const { styles } = useStyles(styleSheet, {}); @@ -269,7 +277,8 @@ const NetworkMultiSelectList = ({ const isDisabled = isLoading || isSelectionDisabled; const showButtonIcon = Boolean(networkTypeOrRpcUrl); - const isGasSponsored = isGasFeesSponsoredNetworkEnabled(chainId); + const isGasSponsored = + !isHardwareWallet && isGasFeesSponsoredNetworkEnabled(chainId); return ( @@ -342,6 +351,7 @@ const NetworkMultiSelectList = ({ isSelectAllNetworksSection, openRpcModal, isGasFeesSponsoredNetworkEnabled, + isHardwareWallet, styles.centeredNetworkCell, styles.noNetworkFeeContainer, ], @@ -351,11 +361,17 @@ const NetworkMultiSelectList = ({ if (!networks.length || !isAutoScrollEnabled) return; if (networksLengthRef.current !== networks.length) { const selectedNetwork = networks.find(({ isSelected }) => isSelected); - networkListRef?.current?.scrollToOffset({ - offset: selectedNetwork?.yOffset ?? 0, - animated: false, - }); + const offset = selectedNetwork?.yOffset ?? 0; networksLengthRef.current = networks.length; + // Defer scroll so FlashList has time to lay out items and avoid "index out of bounds" + requestAnimationFrame(() => { + if (networkListRef?.current?.scrollToOffset) { + networkListRef.current.scrollToOffset({ + offset, + animated: false, + }); + } + }); } }, [networks, isAutoScrollEnabled]); diff --git a/app/components/Views/NetworkSelector/NetworkSelector.tsx b/app/components/Views/NetworkSelector/NetworkSelector.tsx index f1a34a6fed8..12351d22ead 100644 --- a/app/components/Views/NetworkSelector/NetworkSelector.tsx +++ b/app/components/Views/NetworkSelector/NetworkSelector.tsx @@ -107,6 +107,8 @@ import { removeItemFromChainIdList } from '../../../util/metrics/MultichainAPI/n import { analytics } from '../../../util/analytics/analytics'; import { NETWORK_SELECTOR_SOURCES } from '../../../constants/networkSelector'; import { getGasFeesSponsoredNetworkEnabled } from '../../../selectors/featureFlagController/gasFeesSponsored'; +import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController'; +import { isHardwareAccount } from '../../../util/address'; import TagColored, { TagColor, } from '../../../component-library/components-temp/TagColored'; @@ -137,6 +139,12 @@ const NetworkSelector = ({ route }: NetworkSelectorProps) => { const isGasFeesSponsoredNetworkEnabled = useSelector( getGasFeesSponsoredNetworkEnabled, ); + const selectedAddress = useSelector( + selectSelectedInternalAccountFormattedAddress, + ); + const isHardwareWallet = Boolean( + selectedAddress && isHardwareAccount(selectedAddress), + ); const networkConfigurations = useSelector( selectEvmNetworkConfigurationsByChainId, @@ -559,7 +567,8 @@ const NetworkSelector = ({ route }: NetworkSelectorProps) => { {name} - {isGasFeesSponsoredNetworkEnabled(chainId) ? ( + {!isHardwareWallet && + isGasFeesSponsoredNetworkEnabled(chainId) ? ( { ) } tertiaryText={ - isSendFlow && isGasFeesSponsoredNetworkEnabled(chainId) + isSendFlow && + !isHardwareWallet && + isGasFeesSponsoredNetworkEnabled(chainId) ? strings('networks.no_network_fee') : undefined } diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.tsx b/app/components/Views/Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.tsx index 16aca39e32b..1edd8ab10f8 100644 --- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.tsx +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.tsx @@ -34,6 +34,8 @@ import Icon, { } from '../../../../../../component-library/components/Icons/Icon'; import { selectAdditionalNetworksBlacklistFeatureFlag } from '../../../../../../selectors/featureFlagController/networkBlacklist'; import { getGasFeesSponsoredNetworkEnabled } from '../../../../../../selectors/featureFlagController/gasFeesSponsored'; +import { selectSelectedInternalAccountFormattedAddress } from '../../../../../../selectors/accountsController'; +import { isHardwareAccount } from '../../../../../../util/address'; import TagColored, { TagColor, } from '../../../../../../component-library/components-temp/TagColored'; @@ -65,6 +67,12 @@ const CustomNetwork = ({ const isGasFeesSponsoredNetworkEnabled = useSelector( getGasFeesSponsoredNetworkEnabled, ); + const selectedAddress = useSelector( + selectSelectedInternalAccountFormattedAddress, + ); + const isHardwareWallet = Boolean( + selectedAddress && isHardwareAccount(selectedAddress), + ); const { safeChains } = useSafeChains(); const blacklistedChainIds = useSelector( selectAdditionalNetworksBlacklistFeatureFlag, @@ -181,7 +189,8 @@ const CustomNetwork = ({ {networkConfiguration.nickname} - {isGasFeesSponsoredNetworkEnabled( + {!isHardwareWallet && + isGasFeesSponsoredNetworkEnabled( networkConfiguration.chainId, ) ? ( { const handleTransactionAddedEventForMetricsMock = jest.mocked( handleTransactionAddedEventForMetrics, ); + const accountSupports7702Mock = jest.mocked(accountSupports7702); const isSendBundleSupportedMock = jest.mocked(isSendBundleSupported); const selectMetaMaskPayFlagsMock = jest.mocked(selectMetaMaskPayFlags); const payHookClassMock = jest.mocked(TransactionPayPublishHook); @@ -422,6 +425,7 @@ describe('Transaction Controller Init', () => { let mockDelegation7702Hook: jest.MockedFn; beforeEach(() => { + accountSupports7702Mock.mockResolvedValue(true); payHookMock.mockResolvedValue({ transactionHash: undefined }); mockDelegation7702Hook = jest .fn() @@ -434,6 +438,20 @@ describe('Transaction Controller Init', () => { ); }); + it('skips Delegation7702PublishHook for hardware wallet accounts', async () => { + accountSupports7702Mock.mockResolvedValue(false); + selectShouldUseSmartTransactionMock.mockReturnValue(false); + + const hooks = testConstructorOption('hooks'); + await hooks?.publish?.({ + ...MOCK_TRANSACTION_META, + chainId: '0x13', + }); + + expect(Delegation7702PublishHookMock).not.toHaveBeenCalled(); + expect(mockDelegation7702Hook).not.toHaveBeenCalled(); + }); + it('falls back to Delegation7702PublishHook when smart transactions are disabled', async () => { selectShouldUseSmartTransactionMock.mockReturnValue(false); const hooks = testConstructorOption('hooks'); @@ -718,6 +736,7 @@ describe('Transaction Controller Init', () => { }); it('returns true if isExternalSign', async () => { + accountSupports7702Mock.mockResolvedValue(true); const mockTransactionMeta = { id: '123', status: 'approved', @@ -732,6 +751,7 @@ describe('Transaction Controller Init', () => { }); it('calls getNonceLock and releaseLock via Delegation7702PublishHook getNextNonce', async () => { + accountSupports7702Mock.mockResolvedValue(true); const releaseLockMock = jest.fn(); const getNonceLockMock = jest.fn().mockResolvedValue({ nextNonce: 99, @@ -773,6 +793,7 @@ describe('Transaction Controller Init', () => { }); it('calls 7702 publish hook if isExternalSign', async () => { + accountSupports7702Mock.mockResolvedValue(true); const delegation7702Mock: jest.MockedFn = jest.fn(); jest.mocked(Delegation7702PublishHook).mockImplementation( diff --git a/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts b/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts index b61e47c745c..a758e7066b4 100644 --- a/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts +++ b/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts @@ -52,6 +52,7 @@ import { } from '@metamask/transaction-pay-controller'; import { selectMetaMaskPayFlags } from '../../../../selectors/featureFlagController/confirmations'; import { trace } from '../../../../util/trace'; +import { accountSupports7702 } from '../../../../util/transactions/account-supports-7702'; import { Delegation7702PublishHook } from '../../../../util/transactions/hooks/delegation-7702-publish'; import { isSendBundleSupported } from '../../../../util/transactions/sentinel-api'; import { NetworkClientId } from '@metamask/network-controller'; @@ -110,6 +111,7 @@ export const TransactionControllerInit: ControllerInitFunction< publishHook({ transactionMeta, getState, + keyringController, transactionController, smartTransactionsController, initMessenger, @@ -134,6 +136,15 @@ export const TransactionControllerInit: ControllerInitFunction< isFirstTimeInteractionEnabled: () => isFirstTimeInteractionEnabled(preferencesController), isEIP7702GasFeeTokensEnabled: async (transactionMeta) => { + if ( + !(await accountSupports7702( + transactionMeta.txParams?.from, + keyringController as Parameters[1], + )) + ) { + return false; + } + const { chainId, isExternalSign } = transactionMeta; const state = getState(); @@ -191,6 +202,7 @@ async function getNextNonce( async function publishHook({ transactionMeta, getState, + keyringController, transactionController, smartTransactionsController, initMessenger, @@ -198,6 +210,7 @@ async function publishHook({ }: { transactionMeta: TransactionMeta; getState: () => RootState; + keyringController: Parameters[1]; transactionController: TransactionController; smartTransactionsController: SmartTransactionsController; initMessenger: TransactionControllerInitMessenger; @@ -224,7 +237,15 @@ async function publishHook({ const { isExternalSign } = transactionMeta; - if (!shouldUseSmartTransaction || !sendBundleSupport || isExternalSign) { + const keyringSupports7702 = await accountSupports7702( + transactionMeta.txParams?.from, + keyringController, + ); + + if ( + keyringSupports7702 && + (!shouldUseSmartTransaction || !sendBundleSupport || isExternalSign) + ) { const hook = new Delegation7702PublishHook({ isAtomicBatchSupported: transactionController.isAtomicBatchSupported.bind( transactionController, diff --git a/app/util/transactions/account-supports-7702.test.ts b/app/util/transactions/account-supports-7702.test.ts new file mode 100644 index 00000000000..cb9402df523 --- /dev/null +++ b/app/util/transactions/account-supports-7702.test.ts @@ -0,0 +1,115 @@ +import ExtendedKeyringTypes from '../../constants/keyringTypes'; +import { accountSupports7702 } from './account-supports-7702'; + +const SAMPLE_ADDRESS = '0x0000000000000000000000000000000000000001'; + +function createMockKeyringController(keyring: unknown): { + getKeyringForAccount: jest.Mock; +} { + return { + getKeyringForAccount: jest.fn().mockResolvedValue(keyring), + }; +} + +describe('accountSupports7702', () => { + it('returns true when address is undefined', async () => { + const controller = createMockKeyringController({ + type: ExtendedKeyringTypes.ledger, + }); + await expect(accountSupports7702(undefined, controller)).resolves.toBe( + true, + ); + expect(controller.getKeyringForAccount).not.toHaveBeenCalled(); + }); + + it('returns true when address is empty', async () => { + const controller = createMockKeyringController({ + type: ExtendedKeyringTypes.ledger, + }); + await expect(accountSupports7702('', controller)).resolves.toBe(true); + expect(controller.getKeyringForAccount).not.toHaveBeenCalled(); + }); + + it('returns true for HD Key Tree keyring', async () => { + const controller = createMockKeyringController({ + type: ExtendedKeyringTypes.hd, + }); + await expect(accountSupports7702(SAMPLE_ADDRESS, controller)).resolves.toBe( + true, + ); + expect(controller.getKeyringForAccount).toHaveBeenCalledWith( + SAMPLE_ADDRESS, + ); + }); + + it('returns true for Simple Key Pair keyring', async () => { + const controller = createMockKeyringController({ + type: ExtendedKeyringTypes.simple, + }); + await expect(accountSupports7702(SAMPLE_ADDRESS, controller)).resolves.toBe( + true, + ); + }); + + it('returns false for Ledger hardware keyring', async () => { + const controller = createMockKeyringController({ + type: ExtendedKeyringTypes.ledger, + }); + await expect(accountSupports7702(SAMPLE_ADDRESS, controller)).resolves.toBe( + false, + ); + }); + + it('returns false for QR hardware keyring', async () => { + const controller = createMockKeyringController({ + type: ExtendedKeyringTypes.qr, + }); + await expect(accountSupports7702(SAMPLE_ADDRESS, controller)).resolves.toBe( + false, + ); + }); + + it('returns false when keyring type is not in the allowlist', async () => { + const controller = createMockKeyringController({ + type: 'Snap Keyring', + }); + await expect(accountSupports7702(SAMPLE_ADDRESS, controller)).resolves.toBe( + false, + ); + }); + + it('returns false when keyring has no string type', async () => { + const controller = createMockKeyringController({ type: 123 }); + await expect(accountSupports7702(SAMPLE_ADDRESS, controller)).resolves.toBe( + false, + ); + }); + + it('returns false when keyring is null', async () => { + const controller = createMockKeyringController(null); + await expect(accountSupports7702(SAMPLE_ADDRESS, controller)).resolves.toBe( + false, + ); + }); + + it('returns true when getKeyringForAccount throws', async () => { + const controller = { + getKeyringForAccount: jest.fn().mockRejectedValue(new Error('not found')), + }; + await expect(accountSupports7702(SAMPLE_ADDRESS, controller)).resolves.toBe( + true, + ); + }); + + it('resolves the controller from a getter when a function is passed', async () => { + const controller = createMockKeyringController({ + type: ExtendedKeyringTypes.hd, + }); + await expect( + accountSupports7702(SAMPLE_ADDRESS, () => controller), + ).resolves.toBe(true); + expect(controller.getKeyringForAccount).toHaveBeenCalledWith( + SAMPLE_ADDRESS, + ); + }); +}); diff --git a/app/util/transactions/account-supports-7702.ts b/app/util/transactions/account-supports-7702.ts new file mode 100644 index 00000000000..e04243fd8b9 --- /dev/null +++ b/app/util/transactions/account-supports-7702.ts @@ -0,0 +1,51 @@ +import ExtendedKeyringTypes from '../../constants/keyringTypes'; + +/** Minimal shape; KeyringController.getKeyringForAccount is typed as Promise. */ +interface KeyringControllerLike { + getKeyringForAccount: (address: string) => Promise; +} + +/** + * Keyring types that support EIP-7702 (Setup Smart Account). + * Only HD (entropy) and simple (private key) accounts support this; hardware and snap do not. + */ +const KEYRING_TYPES_SUPPORTING_7702: string[] = [ + ExtendedKeyringTypes.hd, + ExtendedKeyringTypes.simple, +]; + +/** + * Returns whether the given account's keyring supports EIP-7702 gas fee tokens. + * Used to avoid requesting 7702 from sentinel for hardware and other unsupported keyrings. + * + * @param address - Account address (e.g. request.from or transactionMeta.txParams?.from). + * @param keyringControllerOrGetter - KeyringController instance or a function that returns it. + * @returns True if the account supports 7702 (or address is missing / lookup fails; assume supported). + */ +export async function accountSupports7702( + address: string | undefined, + keyringControllerOrGetter: + | KeyringControllerLike + | (() => KeyringControllerLike), +): Promise { + if (!address) { + return true; + } + const keyringController = + typeof keyringControllerOrGetter === 'function' + ? keyringControllerOrGetter() + : keyringControllerOrGetter; + try { + const keyring = await keyringController.getKeyringForAccount(address); + const keyringType = + keyring && + typeof keyring === 'object' && + 'type' in keyring && + typeof (keyring as { type: unknown }).type === 'string' + ? (keyring as { type: string }).type + : ''; + return KEYRING_TYPES_SUPPORTING_7702.includes(keyringType); + } catch { + return true; + } +} diff --git a/package.json b/package.json index 1fd95d9ba02..cda2f712c30 100644 --- a/package.json +++ b/package.json @@ -184,8 +184,10 @@ "viem": "2.31.3", "@metamask/accounts-controller": "37.0.0", "@metamask/core-backend": "^5.0.0", + "@metamask/bridge-status-controller@npm:^69.0.0": "patch:@metamask/bridge-status-controller@npm%3A69.0.0#~/.yarn/patches/@metamask-bridge-status-controller-npm-69.0.0-ec19aeeecf.patch", "bn.js@npm:4.11.6": "4.12.3", "bn.js@npm:5.2.1": "5.2.3", + "@metamask/bridge-status-controller@npm:^68.1.0": "patch:@metamask/bridge-status-controller@npm%3A68.1.0#~/.yarn/patches/@metamask-bridge-status-controller-npm-68.1.0-8a2c809398.patch", "expo-web-browser@npm:~14.0.2": "patch:expo-web-browser@npm%3A14.0.2#~/.yarn/patches/expo-web-browser-npm-14.0.2-98d00ce880.patch" }, "dependencies": { @@ -216,7 +218,7 @@ "@metamask/base-controller": "^9.0.0", "@metamask/bitcoin-wallet-snap": "^1.10.0", "@metamask/bridge-controller": "^69.1.1", - "@metamask/bridge-status-controller": "^68.1.0", + "@metamask/bridge-status-controller": "patch:@metamask/bridge-status-controller@npm%3A68.1.0#~/.yarn/patches/@metamask-bridge-status-controller-npm-68.1.0-8a2c809398.patch", "@metamask/chain-agnostic-permission": "^1.3.0", "@metamask/compliance-controller": "^1.0.1", "@metamask/connectivity-controller": "^0.1.0", diff --git a/yarn.lock b/yarn.lock index c98b0ac0f33..3a709443f69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7888,7 +7888,7 @@ __metadata: languageName: node linkType: hard -"@metamask/bridge-status-controller@npm:^68.1.0": +"@metamask/bridge-status-controller@npm:68.1.0": version: 68.1.0 resolution: "@metamask/bridge-status-controller@npm:68.1.0" dependencies: @@ -7911,7 +7911,7 @@ __metadata: languageName: node linkType: hard -"@metamask/bridge-status-controller@npm:^69.0.0": +"@metamask/bridge-status-controller@npm:69.0.0": version: 69.0.0 resolution: "@metamask/bridge-status-controller@npm:69.0.0" dependencies: @@ -7934,6 +7934,52 @@ __metadata: languageName: node linkType: hard +"@metamask/bridge-status-controller@patch:@metamask/bridge-status-controller@npm%3A68.1.0#~/.yarn/patches/@metamask-bridge-status-controller-npm-68.1.0-8a2c809398.patch": + version: 68.1.0 + resolution: "@metamask/bridge-status-controller@patch:@metamask/bridge-status-controller@npm%3A68.1.0#~/.yarn/patches/@metamask-bridge-status-controller-npm-68.1.0-8a2c809398.patch::version=68.1.0&hash=358095" + dependencies: + "@metamask/accounts-controller": "npm:^37.0.0" + "@metamask/base-controller": "npm:^9.0.0" + "@metamask/bridge-controller": "npm:^69.1.0" + "@metamask/controller-utils": "npm:^11.19.0" + "@metamask/gas-fee-controller": "npm:^26.0.3" + "@metamask/keyring-controller": "npm:^25.1.0" + "@metamask/network-controller": "npm:^30.0.0" + "@metamask/polling-controller": "npm:^16.0.3" + "@metamask/profile-sync-controller": "npm:^28.0.0" + "@metamask/snaps-controllers": "npm:^17.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/transaction-controller": "npm:^62.21.0" + "@metamask/utils": "npm:^11.9.0" + bignumber.js: "npm:^9.1.2" + uuid: "npm:^8.3.2" + checksum: 10/e2fb1a7667030e5d486e0c70c5f673223a1f48b488490ee4fbcf662116111936596c447bddbcc75b166d58d3c726c5e5892578213db8b9206891e78a4f03c136 + languageName: node + linkType: hard + +"@metamask/bridge-status-controller@patch:@metamask/bridge-status-controller@npm%3A69.0.0#~/.yarn/patches/@metamask-bridge-status-controller-npm-69.0.0-ec19aeeecf.patch": + version: 69.0.0 + resolution: "@metamask/bridge-status-controller@patch:@metamask/bridge-status-controller@npm%3A69.0.0#~/.yarn/patches/@metamask-bridge-status-controller-npm-69.0.0-ec19aeeecf.patch::version=69.0.0&hash=41006d" + dependencies: + "@metamask/accounts-controller": "npm:^37.0.0" + "@metamask/base-controller": "npm:^9.0.0" + "@metamask/bridge-controller": "npm:^69.1.1" + "@metamask/controller-utils": "npm:^11.19.0" + "@metamask/gas-fee-controller": "npm:^26.1.0" + "@metamask/keyring-controller": "npm:^25.1.0" + "@metamask/network-controller": "npm:^30.0.0" + "@metamask/polling-controller": "npm:^16.0.3" + "@metamask/profile-sync-controller": "npm:^28.0.0" + "@metamask/snaps-controllers": "npm:^17.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/transaction-controller": "npm:^63.0.0" + "@metamask/utils": "npm:^11.9.0" + bignumber.js: "npm:^9.1.2" + uuid: "npm:^8.3.2" + checksum: 10/da79a48e1fbae222f682aedfe008f6c0ef1213c78ff0faa85e57d59258e517477456a2cab3ef09a68b44cefa68bd515c0b4b46a06294998bebeec269b40920d7 + languageName: node + linkType: hard + "@metamask/browser-passworder@npm:^5.0.0": version: 5.0.0 resolution: "@metamask/browser-passworder@npm:5.0.0" @@ -35536,7 +35582,7 @@ __metadata: "@metamask/base-controller": "npm:^9.0.0" "@metamask/bitcoin-wallet-snap": "npm:^1.10.0" "@metamask/bridge-controller": "npm:^69.1.1" - "@metamask/bridge-status-controller": "npm:^68.1.0" + "@metamask/bridge-status-controller": "patch:@metamask/bridge-status-controller@npm%3A68.1.0#~/.yarn/patches/@metamask-bridge-status-controller-npm-68.1.0-8a2c809398.patch" "@metamask/browser-passworder": "npm:^5.0.0" "@metamask/browser-playground": "npm:0.3.0" "@metamask/build-utils": "npm:^3.0.0" From 982b2cfd1f9b9b90926d9198d7d99a7a6e6b6491 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Wed, 25 Mar 2026 09:02:11 +0000 Subject: [PATCH 2/2] [skip ci] Bump version number to 4180 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d91fbcd5d44..5babb60263d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -188,7 +188,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.71.0" - versionCode 4173 + versionCode 4180 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index a82f6f06ee2..137c2c3654b 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3534,13 +3534,13 @@ app: VERSION_NAME: 7.71.0 - opts: is_expand: false - VERSION_NUMBER: 4173 + VERSION_NUMBER: 4180 - opts: is_expand: false FLASK_VERSION_NAME: 7.71.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 4173 + FLASK_VERSION_NUMBER: 4180 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 6b9e475e392..dd1f91b84dc 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4173; + CURRENT_PROJECT_VERSION = 4180; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4173; + CURRENT_PROJECT_VERSION = 4180; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4173; + CURRENT_PROJECT_VERSION = 4180; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4173; + CURRENT_PROJECT_VERSION = 4180; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4173; + CURRENT_PROJECT_VERSION = 4180; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4173; + CURRENT_PROJECT_VERSION = 4180; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;