Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,6 +38,7 @@ describe('useIsGasIncluded7702Supported', () => {
beforeEach(() => {
jest.clearAllMocks();
mockIsRelaySupported.mockResolvedValue(false);
mockUseIsHardwareWalletForBridge.mockReturnValue(false);
});

afterEach(() => {
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof useSelector>;
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);
});
});
Original file line number Diff line number Diff line change
@@ -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],
);
}
4 changes: 3 additions & 1 deletion app/components/UI/Bridge/utils/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -106,6 +108,12 @@ const NetworkMultiSelectList = ({
const isGasFeesSponsoredNetworkEnabled = useSelector(
getGasFeesSponsoredNetworkEnabled,
);
const selectedAddress = useSelector(
selectSelectedInternalAccountFormattedAddress,
);
const isHardwareWallet = Boolean(
selectedAddress && isHardwareAccount(selectedAddress),
);

const { styles } = useStyles(styleSheet, {});

Expand Down Expand Up @@ -269,7 +277,8 @@ const NetworkMultiSelectList = ({
const isDisabled = isLoading || isSelectionDisabled;
const showButtonIcon = Boolean(networkTypeOrRpcUrl);

const isGasSponsored = isGasFeesSponsoredNetworkEnabled(chainId);
const isGasSponsored =
!isHardwareWallet && isGasFeesSponsoredNetworkEnabled(chainId);

return (
<View>
Expand Down Expand Up @@ -342,6 +351,7 @@ const NetworkMultiSelectList = ({
isSelectAllNetworksSection,
openRpcModal,
isGasFeesSponsoredNetworkEnabled,
isHardwareWallet,
styles.centeredNetworkCell,
styles.noNetworkFeeContainer,
],
Expand All @@ -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]);

Expand Down
Loading
Loading