From 748efdb501c5563f982476661455189684998bb1 Mon Sep 17 00:00:00 2001 From: Davide Brocchetto Date: Fri, 3 Apr 2026 11:17:55 -0700 Subject: [PATCH 01/10] Added Gasless swap for Native token --- .../page-objects/pages/bridge/quote-page.ts | 33 ++++++ test/e2e/tests/bridge/bridge-test-utils.ts | 110 ++++++++++++++++++ .../swap-quotes-eth-usdc-gas-included.json | 85 ++++++++++++++ test/e2e/tests/bridge/swap-gasless.spec.ts | 45 +++++++ 4 files changed, 273 insertions(+) create mode 100644 test/e2e/tests/bridge/mocks/swap-quotes-eth-usdc-gas-included.json create mode 100644 test/e2e/tests/bridge/swap-gasless.spec.ts diff --git a/test/e2e/page-objects/pages/bridge/quote-page.ts b/test/e2e/page-objects/pages/bridge/quote-page.ts index e7bee1be604f..eaee8db51f69 100644 --- a/test/e2e/page-objects/pages/bridge/quote-page.ts +++ b/test/e2e/page-objects/pages/bridge/quote-page.ts @@ -47,6 +47,10 @@ class BridgeQuotePage { private backButton = '[aria-label="Back"]'; + private gasIncludedIndicator = '[data-testid="network-fees-included"]'; + + private maxButton = { text: 'Max' }; + private networkSelector = '[data-testid="multichain-asset-picker__network"]'; private networkFees = '[data-testid="network-fees"]'; @@ -305,6 +309,24 @@ class BridgeQuotePage { console.log('Price matches expected format'); } + async checkGasIncludedIsDisplayed(): Promise { + try { + await this.driver.waitForSelector(this.gasIncludedIndicator, { + timeout: 30000, + }); + } catch (e) { + console.log('Expected "Gas fees included" indicator is not present'); + throw e; + } + console.log('Gas fees included indicator is displayed'); + } + + async clickMaxButton(): Promise { + await this.driver.waitForSelector(this.maxButton, { timeout: 30000 }); + await this.driver.clickElement(this.maxButton); + console.log('Clicked Max button'); + } + checkDestAmount = async (amount: string) => { const destAmount = await this.driver.findElement(this.destinationAmount); assert.equal(await destAmount.getAttribute('value'), amount); @@ -336,6 +358,17 @@ class BridgeQuotePage { `); } + async selectDestToken(token: string): Promise { + await this.driver.waitForSelector(this.destinationAssetPickerButton); + await this.driver.clickElement(this.destinationAssetPickerButton); + await this.driver.fill(this.assetPrickerSearchInput, token); + await this.driver.clickElementAndWaitToDisappear({ + text: token, + css: this.tokenButton, + }); + console.log(`Selected destination token: ${token}`); + } + async selectNetwork(network: string): Promise { await this.driver.clickElement(this.networkSelector); await this.driver.clickElement(this.networkNameSelector(network)); diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index a66e5a96b431..8dd71c44dbf8 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -40,8 +40,10 @@ import { SSE_RESPONSE_HEADER, EXPECTED_INPUT_CHANGES, BRIDGE_REFRESH_RATE, + DEFAULT_BRIDGE_FEATURE_FLAGS, } from './constants'; import MOCK_SWAP_QUOTES_ETH_MUSD from './mocks/swap-quotes-eth-musd.json'; +import MOCK_SWAP_QUOTES_ETH_USDC_GAS_INCLUDED from './mocks/swap-quotes-eth-usdc-gas-included.json'; export class BridgePage { driver: Driver; @@ -1773,6 +1775,114 @@ export const checkInputChangedEvents = async ( return expectedInputChanges.length; }; +async function mockSentinelNetworks(mockServer: Mockttp) { + return await mockServer + .forGet( + 'https://tx-sentinel-ethereum-mainnet.api.cx.metamask.io/networks', + ) + .always() + .thenCallback(() => ({ + statusCode: 200, + json: { + '1': { + network: 'ethereum-mainnet', + explorer: 'https://etherscan.io', + confirmations: true, + smartTransactions: true, + relayTransactions: true, + hidden: false, + sendBundle: true, + }, + }, + })); +} + +async function mockGasIncludedSwapETHtoUSDC(mockServer: Mockttp) { + return await mockServer + .forGet(/getQuoteStream/u) + .once() + .withQuery({ + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + }) + .thenStream( + 200, + mockSseEventSource(MOCK_SWAP_QUOTES_ETH_USDC_GAS_INCLUDED), + SSE_RESPONSE_HEADER, + ); +} + +export const GAS_INCLUDED_SWAP_FEATURE_FLAGS: FeatureFlagResponse & { + minimumVersion: string; +} = { + ...DEFAULT_BRIDGE_FEATURE_FLAGS, + sse: { + enabled: true, + minimumVersion: '13.2.0', + }, +}; + +export const getGasIncludedSwapFixtures = (title?: string) => { + const fixtureBuilder = new FixtureBuilder({ + inputChainId: CHAIN_IDS.MAINNET, + }) + .withCurrencyController(MOCK_CURRENCY_RATES) + .withBridgeControllerDefaultState() + .withTokensControllerERC20({ chainId: 1 }) + .withEnabledNetworks({ + eip155: { + '0x1': true, + '0xe708': true, + '0xa4b1': true, + }, + }); + + return { + forceBip44Version: false, + fixtures: fixtureBuilder.build(), + testSpecificMock: async (mockServer: Mockttp) => [ + await mockPortfolioPage(mockServer), + await mockGetTxStatus(mockServer), + await mockTopAssetsLinea(mockServer), + await mockTopAssetsArbitrum(mockServer), + await mockTokensEthereum(mockServer), + await mockTokensLinea(mockServer), + await mockGetTokenArbitrum(mockServer), + await mockGetPopularTokens(mockServer), + await mockGasIncludedSwapETHtoUSDC(mockServer), + await mockFeatureFlags(mockServer, GAS_INCLUDED_SWAP_FEATURE_FLAGS), + await mockAccountsTransactions(mockServer), + await mockAccountsBalances(mockServer), + await mockPriceSpotPrices(mockServer), + await mockPriceSpotPricesV3(mockServer), + await mockGasPricesMainnet(mockServer), + await mockHistoricalPrices(mockServer), + await mockSentinelNetworks(mockServer), + ...(await mockSearchTokens(mockServer)), + ], + manifestFlags: { + remoteFeatureFlags: { + bridgeConfig: GAS_INCLUDED_SWAP_FEATURE_FLAGS, + }, + testing: { disableSmartTransactionsOverride: true }, + }, + ethConversionInUsd: ETH_CONVERSION_RATE_USD, + smartContract: SMART_CONTRACTS.HST, + localNodeOptions: [ + { + type: 'anvil' as const, + options: { + chainId: 1, + hardfork: 'london', + loadState: + './test/e2e/seeder/network-states/with100Usdc100Usdt50Dai.json', + }, + }, + ], + title, + }; +}; + export const checkQuoteRequestsAreNotMadeAfterTimestamp = async ( driver: Driver, timestamp: number, diff --git a/test/e2e/tests/bridge/mocks/swap-quotes-eth-usdc-gas-included.json b/test/e2e/tests/bridge/mocks/swap-quotes-eth-usdc-gas-included.json new file mode 100644 index 000000000000..93add0db5536 --- /dev/null +++ b/test/e2e/tests/bridge/mocks/swap-quotes-eth-usdc-gas-included.json @@ -0,0 +1,85 @@ +[ + { + "quote": { + "requestId": "0xgasless-swap-eth-usdc-001", + "bridgeId": "openocean", + "srcChainId": 1, + "destChainId": 1, + "aggregator": "openocean", + "aggregatorType": "AGG", + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "assetId": "eip155:1/slip44:60", + "symbol": "ETH", + "decimals": 18, + "name": "Ethereum", + "coingeckoId": "ethereum", + "aggregators": [], + "occurrences": 100, + "iconUrl": "https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/slip44/60.png", + "metadata": {} + }, + "srcTokenAmount": "991250000000000000", + "destAsset": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "chainId": 1, + "assetId": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coingeckoId": "usd-coin", + "aggregators": [], + "occurrences": 14, + "iconUrl": "https://raw.githubusercontent.com/MetaMask/contract-metadata/master/icons/eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.svg", + "metadata": {} + }, + "destTokenAmount": "3011000000", + "minDestTokenAmount": "2950000000", + "walletAddress": "0x5CfE73b6021E818B776b421B1c4Db2474086a7e1", + "destWalletAddress": "0x5CfE73b6021E818B776b421B1c4Db2474086a7e1", + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "assetId": "eip155:1/slip44:60", + "symbol": "ETH", + "decimals": 18, + "name": "Ethereum", + "coingeckoId": "ethereum", + "aggregators": [], + "occurrences": 100, + "iconUrl": "https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/slip44/60.png", + "metadata": {} + }, + "quoteBpsFee": 87.5, + "baseBpsFee": 87.5 + } + }, + "bridges": ["openocean"], + "protocols": ["openocean"], + "steps": [], + "slippage": 2, + "gasSponsored": false, + "gasIncluded": true, + "gasIncluded7702": false, + "priceData": { + "totalFromAmountUsd": "3010.00", + "totalToAmountUsd": "3011.00", + "priceImpact": "0.001", + "totalFeeAmountUsd": "0.00" + } + }, + "trade": { + "chainId": 1, + "to": "0x881D40237659C251811CEC9c364ef91dC08D300C", + "from": "0x5CfE73b6021E818B776b421B1c4Db2474086a7e1", + "value": "0xde0b6b3a7640000", + "data": "0x5f575529000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000136f70656e4f6365616e46656544796e616d696300000000000000000000000000", + "gasLimit": 448721 + }, + "estimatedProcessingTimeInSeconds": 0 + } +] diff --git a/test/e2e/tests/bridge/swap-gasless.spec.ts b/test/e2e/tests/bridge/swap-gasless.spec.ts new file mode 100644 index 000000000000..8b250d6b47cb --- /dev/null +++ b/test/e2e/tests/bridge/swap-gasless.spec.ts @@ -0,0 +1,45 @@ +import { Suite } from 'mocha'; +import { withFixtures } from '../../helpers'; +import { login } from '../../page-objects/flows/login.flow'; +import HomePage from '../../page-objects/pages/home/homepage'; +import BridgeQuotePage from '../../page-objects/pages/bridge/quote-page'; +import ActivityListPage from '../../page-objects/pages/home/activity-list'; +import { getGasIncludedSwapFixtures } from './bridge-test-utils'; + +describe('Gas included swap tests', function (this: Suite) { + this.timeout(160000); + + it('swaps ETH to USDC with gas included via Max button', async function () { + await withFixtures( + getGasIncludedSwapFixtures(this.test?.fullTitle()), + async ({ driver }) => { + await login(driver, { expectedBalance: '$225,730.11' }); + + const homePage = new HomePage(driver); + await homePage.checkPageIsLoaded(); + + await homePage.startSwapFlow(); + + const bridgePage = new BridgeQuotePage(driver); + await bridgePage.checkPageIsLoaded(); + + await bridgePage.selectDestToken('USDC'); + + await bridgePage.clickMaxButton(); + + await bridgePage.waitForQuote(); + await bridgePage.checkGasIncludedIsDisplayed(); + + await bridgePage.submitQuote(); + + await homePage.goToActivityList(); + const activityList = new ActivityListPage(driver); + await activityList.checkCompletedBridgeTransactionActivity(1); + await activityList.checkTxAction({ + action: 'Swap ETH to USDC', + confirmedTx: 1, + }); + }, + ); + }); +}); From 2c7f6838e5bd0c2b793307ba0b9d37ea00a28c8f Mon Sep 17 00:00:00 2001 From: Davide Brocchetto Date: Fri, 3 Apr 2026 13:05:32 -0700 Subject: [PATCH 02/10] Erc20 gasless --- .../page-objects/pages/bridge/quote-page.ts | 11 ++- test/e2e/tests/bridge/bridge-test-utils.ts | 81 ++++++++++++++++ .../swap-quotes-usdc-dai-gas-included.json | 93 +++++++++++++++++++ test/e2e/tests/bridge/swap-gasless.spec.ts | 39 ++++++++ 4 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 test/e2e/tests/bridge/mocks/swap-quotes-usdc-dai-gas-included.json diff --git a/test/e2e/page-objects/pages/bridge/quote-page.ts b/test/e2e/page-objects/pages/bridge/quote-page.ts index eaee8db51f69..818d42338819 100644 --- a/test/e2e/page-objects/pages/bridge/quote-page.ts +++ b/test/e2e/page-objects/pages/bridge/quote-page.ts @@ -358,6 +358,16 @@ class BridgeQuotePage { `); } + async selectSrcToken(token: string): Promise { + await this.driver.waitForSelector(this.sourceAssetPickerButton); + await this.driver.clickElement(this.sourceAssetPickerButton); + await this.driver.fill(this.assetPrickerSearchInput, token); + await this.driver.clickElementAndWaitToDisappear({ + text: token, + css: this.tokenButton, + }); + } + async selectDestToken(token: string): Promise { await this.driver.waitForSelector(this.destinationAssetPickerButton); await this.driver.clickElement(this.destinationAssetPickerButton); @@ -366,7 +376,6 @@ class BridgeQuotePage { text: token, css: this.tokenButton, }); - console.log(`Selected destination token: ${token}`); } async selectNetwork(network: string): Promise { diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index 8dd71c44dbf8..34276e892435 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -44,6 +44,7 @@ import { } from './constants'; import MOCK_SWAP_QUOTES_ETH_MUSD from './mocks/swap-quotes-eth-musd.json'; import MOCK_SWAP_QUOTES_ETH_USDC_GAS_INCLUDED from './mocks/swap-quotes-eth-usdc-gas-included.json'; +import MOCK_SWAP_QUOTES_USDC_DAI_GAS_INCLUDED from './mocks/swap-quotes-usdc-dai-gas-included.json'; export class BridgePage { driver: Driver; @@ -1812,6 +1813,84 @@ async function mockGasIncludedSwapETHtoUSDC(mockServer: Mockttp) { ); } +async function mockGasIncludedSwapUSDCtoDAI(mockServer: Mockttp) { + return await mockServer + .forGet(/getQuoteStream/u) + .once() + .withQuery({ + srcTokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + destTokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + }) + .thenStream( + 200, + mockSseEventSource(MOCK_SWAP_QUOTES_USDC_DAI_GAS_INCLUDED), + SSE_RESPONSE_HEADER, + ); +} + +const STX_UUID = '0d506aaa-5e38-4cab-ad09-2039cb7a0f33'; +const STX_TRANSACTION_HASH = + '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5'; + +async function mockSmartTransactionApis(mockServer: Mockttp) { + await mockServer + .forPost('https://transaction.api.cx.metamask.io/networks/1/getFees') + .thenJson(200, { + blockNumber: 20728974, + baseFeePerGas: '0x2e90edd000', + tradeTxFees: { + cancelFees: [], + feeEstimate: 42000000000000, + fees: [{ maxFeePerGas: 20000000000, maxPriorityFeePerGas: 10 }], + gasLimit: 21000, + gasUsed: 21000, + }, + approvalTxFees: { + cancelFees: [], + feeEstimate: 42000000000000, + fees: [{ maxFeePerGas: 20000000000, maxPriorityFeePerGas: 10 }], + gasLimit: 21000, + gasUsed: 21000, + }, + }); + + await mockServer + .forGet('https://transaction.api.cx.metamask.io/networks/1/batchStatus') + .withQuery({ uuids: STX_UUID }) + .once() + .thenJson(200, { + [STX_UUID]: { + uuid: STX_UUID, + status: 'pending', + }, + }); + + await mockServer + .forGet('https://transaction.api.cx.metamask.io/networks/1/batchStatus') + .withQuery({ uuids: STX_UUID }) + .once() + .thenJson(200, { + [STX_UUID]: { + uuid: STX_UUID, + status: 'success', + statusMetadata: { + minedHash: STX_TRANSACTION_HASH, + minedAt: new Date().toISOString(), + isSettled: true, + }, + }, + }); + + await mockServer + .forPost( + 'https://transaction.api.cx.metamask.io/networks/1/submitTransactions', + ) + .thenJson(200, { + uuid: STX_UUID, + txHashes: [STX_TRANSACTION_HASH], + }); +} + export const GAS_INCLUDED_SWAP_FEATURE_FLAGS: FeatureFlagResponse & { minimumVersion: string; } = { @@ -1850,6 +1929,7 @@ export const getGasIncludedSwapFixtures = (title?: string) => { await mockGetTokenArbitrum(mockServer), await mockGetPopularTokens(mockServer), await mockGasIncludedSwapETHtoUSDC(mockServer), + await mockGasIncludedSwapUSDCtoDAI(mockServer), await mockFeatureFlags(mockServer, GAS_INCLUDED_SWAP_FEATURE_FLAGS), await mockAccountsTransactions(mockServer), await mockAccountsBalances(mockServer), @@ -1858,6 +1938,7 @@ export const getGasIncludedSwapFixtures = (title?: string) => { await mockGasPricesMainnet(mockServer), await mockHistoricalPrices(mockServer), await mockSentinelNetworks(mockServer), + await mockSmartTransactionApis(mockServer), ...(await mockSearchTokens(mockServer)), ], manifestFlags: { diff --git a/test/e2e/tests/bridge/mocks/swap-quotes-usdc-dai-gas-included.json b/test/e2e/tests/bridge/mocks/swap-quotes-usdc-dai-gas-included.json new file mode 100644 index 000000000000..e1064d1786f0 --- /dev/null +++ b/test/e2e/tests/bridge/mocks/swap-quotes-usdc-dai-gas-included.json @@ -0,0 +1,93 @@ +[ + { + "quote": { + "requestId": "0xgas-included-swap-usdc-dai-001", + "bridgeId": "openocean", + "srcChainId": 1, + "destChainId": 1, + "aggregator": "openocean", + "aggregatorType": "AGG", + "srcAsset": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "chainId": 1, + "assetId": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coingeckoId": "usd-coin", + "aggregators": [], + "occurrences": 14, + "iconUrl": "https://raw.githubusercontent.com/MetaMask/contract-metadata/master/icons/eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.svg", + "metadata": {} + }, + "srcTokenAmount": "99125000", + "destAsset": { + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "chainId": 1, + "assetId": "eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18, + "name": "Dai Stablecoin", + "coingeckoId": "dai", + "aggregators": [], + "occurrences": 15, + "iconUrl": "https://raw.githubusercontent.com/MetaMask/contract-metadata/master/icons/eip155:1/erc20:0x6B175474E89094C44Da98b954EedeAC495271d0F.svg", + "metadata": {} + }, + "destTokenAmount": "99000000000000000000", + "minDestTokenAmount": "97020000000000000000", + "walletAddress": "0x5CfE73b6021E818B776b421B1c4Db2474086a7e1", + "destWalletAddress": "0x5CfE73b6021E818B776b421B1c4Db2474086a7e1", + "feeData": { + "metabridge": { + "amount": "875000", + "asset": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "chainId": 1, + "assetId": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coingeckoId": "usd-coin", + "aggregators": [], + "occurrences": 14, + "iconUrl": "https://raw.githubusercontent.com/MetaMask/contract-metadata/master/icons/eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.svg", + "metadata": {} + }, + "quoteBpsFee": 87.5, + "baseBpsFee": 87.5 + } + }, + "bridges": ["openocean"], + "protocols": ["openocean"], + "steps": [], + "slippage": 2, + "gasSponsored": false, + "gasIncluded": true, + "gasIncluded7702": false, + "priceData": { + "totalFromAmountUsd": "99.13", + "totalToAmountUsd": "99.00", + "priceImpact": "0.001", + "totalFeeAmountUsd": "0.00" + } + }, + "approval": { + "chainId": 1, + "to": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "from": "0x5CfE73b6021E818B776b421B1c4Db2474086a7e1", + "value": "0x00", + "data": "0x095ea7b30000000000000000000000000439e60f02a8900a951603950d8d4527f400c3f100000000000000000000000000000000000000000000000000000000059868c0", + "gasLimit": 60000 + }, + "trade": { + "chainId": 1, + "to": "0x881D40237659C251811CEC9c364ef91dC08D300C", + "from": "0x5CfE73b6021E818B776b421B1c4Db2474086a7e1", + "value": "0x0", + "data": "0x5f575529000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005e69ec000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000136f70656e4f6365616e46656544796e616d696300000000000000000000000000", + "gasLimit": 448721 + }, + "estimatedProcessingTimeInSeconds": 0 + } +] diff --git a/test/e2e/tests/bridge/swap-gasless.spec.ts b/test/e2e/tests/bridge/swap-gasless.spec.ts index 8b250d6b47cb..d962062dc9a5 100644 --- a/test/e2e/tests/bridge/swap-gasless.spec.ts +++ b/test/e2e/tests/bridge/swap-gasless.spec.ts @@ -42,4 +42,43 @@ describe('Gas included swap tests', function (this: Suite) { }, ); }); + + it('swaps USDC to DAI with gas included using ERC20 source token', async function () { + await withFixtures( + getGasIncludedSwapFixtures(this.test?.fullTitle()), + async ({ driver }) => { + await login(driver, { expectedBalance: '$225,730.11' }); + + const homePage = new HomePage(driver); + await homePage.checkPageIsLoaded(); + + await homePage.startSwapFlow(); + + const bridgePage = new BridgeQuotePage(driver); + await bridgePage.checkPageIsLoaded(); + + await bridgePage.selectSrcToken('USDC'); + await bridgePage.selectDestToken('DAI'); + + await bridgePage.clickMaxButton(); + + await bridgePage.waitForQuote(); + await bridgePage.checkGasIncludedIsDisplayed(); + + await bridgePage.submitQuote(); + + await homePage.goToActivityList(); + const activityList = new ActivityListPage(driver); + await activityList.checkCompletedBridgeTransactionActivity(2); + await activityList.checkTxAction({ + action: 'Swap USDC to DAI', + confirmedTx: 1, + }); + await activityList.checkTxAction({ + action: 'Approve USDC for swaps', + confirmedTx: 2, + }); + }, + ); + }); }); From 44e07f65c0a1cf39bab5217d3ae2ef0a4a548da6 Mon Sep 17 00:00:00 2001 From: Davide Brocchetto Date: Fri, 3 Apr 2026 14:21:59 -0700 Subject: [PATCH 03/10] Fixed second test --- test/e2e/tests/bridge/swap-gasless.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/e2e/tests/bridge/swap-gasless.spec.ts b/test/e2e/tests/bridge/swap-gasless.spec.ts index d962062dc9a5..69945efe5e79 100644 --- a/test/e2e/tests/bridge/swap-gasless.spec.ts +++ b/test/e2e/tests/bridge/swap-gasless.spec.ts @@ -72,12 +72,15 @@ describe('Gas included swap tests', function (this: Suite) { await activityList.checkCompletedBridgeTransactionActivity(2); await activityList.checkTxAction({ action: 'Swap USDC to DAI', - confirmedTx: 1, + confirmedTx: 0, + txIndex: 1, }); await activityList.checkTxAction({ action: 'Approve USDC for swaps', - confirmedTx: 2, + confirmedTx: 0, + txIndex: 2, }); + }, ); }); From 9989adae8a6400bebae9ce16019dc7fe8e1793cb Mon Sep 17 00:00:00 2001 From: Davide Brocchetto Date: Fri, 3 Apr 2026 14:44:21 -0700 Subject: [PATCH 04/10] test: enable STX and SSE in bridge e2e tests, add gasless swap fixtures - Enable Smart Transactions by default across all bridge fixtures; add mockSmartTransactionsForBridge forwarding raw txs to Anvil for real receipts - Add SSE streaming support (getQuoteStream) to all quote mock functions - Extract STX config into STX_MAINNET_NETWORK_CONFIG / STX_MAINNET_SENTINEL_URL constants to eliminate duplication across fixture functions - Fix mockGetTxStatus to use a distinct BRIDGE_DEST_TX_HASH so BridgeStatusController marks bridges as complete - Wire STX completion flow in bridgeTransaction helper; fix navigation to activity list when STX is disabled - Add batchStatusOverride param to getBridgeNegativeCasesFixtures so the pending test uses minedTx:success and failed tests use minedTx:reverted - Add getGasIncludedSwapFixtures and getGasless7702SwapFixtures with mockSmartTransactionsForBridge, gas-included/sponsored quote mocks, and sentinel /networks mock - Fix unsafe integer literal (currentBalance) exceeding Number.MAX_SAFE_INTEGER - Move enterBridgeQuote to shared bridge-test-utils.ts Co-Authored-By: Claude Sonnet 4.6 --- .../page-objects/pages/bridge/quote-page.ts | 14 + test/e2e/tests/bridge/bridge-test-utils.ts | 346 ++++++++++++++---- .../swap-quotes-eth-usdc-gas-sponsored.json | 85 +++++ test/e2e/tests/bridge/swap-gasless.spec.ts | 39 +- 4 files changed, 417 insertions(+), 67 deletions(-) create mode 100644 test/e2e/tests/bridge/mocks/swap-quotes-eth-usdc-gas-sponsored.json diff --git a/test/e2e/page-objects/pages/bridge/quote-page.ts b/test/e2e/page-objects/pages/bridge/quote-page.ts index 818d42338819..7a8ffea88e88 100644 --- a/test/e2e/page-objects/pages/bridge/quote-page.ts +++ b/test/e2e/page-objects/pages/bridge/quote-page.ts @@ -49,6 +49,8 @@ class BridgeQuotePage { private gasIncludedIndicator = '[data-testid="network-fees-included"]'; + private gasSponsoredIndicator = '[data-testid="network-fees-sponsored"]'; + private maxButton = { text: 'Max' }; private networkSelector = '[data-testid="multichain-asset-picker__network"]'; @@ -321,6 +323,18 @@ class BridgeQuotePage { console.log('Gas fees included indicator is displayed'); } + async checkGasSponsoredIsDisplayed(): Promise { + try { + await this.driver.waitForSelector(this.gasSponsoredIndicator, { + timeout: 30000, + }); + } catch (e) { + console.log('Expected "Gas fees sponsored" indicator is not present'); + throw e; + } + console.log('Gas fees sponsored indicator is displayed'); + } + async clickMaxButton(): Promise { await this.driver.waitForSelector(this.maxButton, { timeout: 30000 }); await this.driver.clickElement(this.maxButton); diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index 34276e892435..3ac2afdbc1ee 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -45,6 +45,7 @@ import { import MOCK_SWAP_QUOTES_ETH_MUSD from './mocks/swap-quotes-eth-musd.json'; import MOCK_SWAP_QUOTES_ETH_USDC_GAS_INCLUDED from './mocks/swap-quotes-eth-usdc-gas-included.json'; import MOCK_SWAP_QUOTES_USDC_DAI_GAS_INCLUDED from './mocks/swap-quotes-usdc-dai-gas-included.json'; +import MOCK_SWAP_QUOTES_ETH_USDC_GAS_SPONSORED from './mocks/swap-quotes-eth-usdc-gas-sponsored.json'; export class BridgePage { driver: Driver; @@ -1832,62 +1833,156 @@ const STX_UUID = '0d506aaa-5e38-4cab-ad09-2039cb7a0f33'; const STX_TRANSACTION_HASH = '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5'; -async function mockSmartTransactionApis(mockServer: Mockttp) { +const STX_MAINNET_SENTINEL_URL = + 'https://tx-sentinel-ethereum-mainnet.api.cx.metamask.io'; + +const STX_MAINNET_NETWORK_CONFIG = { + smartTransactionsNetworks: { + '0x1': { + extensionActive: true, + sentinelUrl: STX_MAINNET_SENTINEL_URL, + expectedDeadline: 45, + maxDeadline: 160, + }, + }, +}; + +/** + * Mocks STX service endpoints for bridge/swap tests. + * + * submitTransactions forwards each raw signed tx to Anvil + * via eth_sendRawTransaction. Anvil mines it immediately, so the real tx hash is + * on-chain and eth_getTransactionReceipt returns a genuine receipt — which is what + * the extension's TransactionController needs to mark the transaction as Confirmed. + */ +async function mockSmartTransactionsForBridge( + mockServer: Mockttp, + chainId: number = 1, + sentinelUrl: string = STX_MAINNET_SENTINEL_URL, + batchStatusOverride?: Record, +) { + const ANVIL_RPC_URL = 'http://localhost:8545'; + + let latestMinedHash = STX_TRANSACTION_HASH; + await mockServer - .forPost('https://transaction.api.cx.metamask.io/networks/1/getFees') - .thenJson(200, { - blockNumber: 20728974, - baseFeePerGas: '0x2e90edd000', - tradeTxFees: { - cancelFees: [], - feeEstimate: 42000000000000, - fees: [{ maxFeePerGas: 20000000000, maxPriorityFeePerGas: 10 }], - gasLimit: 21000, - gasUsed: 21000, - }, - approvalTxFees: { - cancelFees: [], - feeEstimate: 42000000000000, - fees: [{ maxFeePerGas: 20000000000, maxPriorityFeePerGas: 10 }], - gasLimit: 21000, - gasUsed: 21000, - }, - }); + .forGet(`${sentinelUrl}/network`) + .always() + .thenCallback(() => ({ + ok: true, + statusCode: 200, + json: { smartTransactions: true }, + })); await mockServer - .forGet('https://transaction.api.cx.metamask.io/networks/1/batchStatus') - .withQuery({ uuids: STX_UUID }) - .once() - .thenJson(200, { - [STX_UUID]: { - uuid: STX_UUID, - status: 'pending', + .forPost( + `https://transaction.api.cx.metamask.io/networks/${chainId}/getFees`, + ) + .always() + .thenCallback(() => ({ + statusCode: 200, + json: { + blockNumber: 20728974, + id: '19d4eea3-8a49-463e-9e9c-099f9d9571ca', + txs: [ + { + cancelFees: [], + return: '0x', + status: 1, + gasUsed: 190780, + gasLimit: 239420, + fees: [ + { + maxFeePerGas: 4667609171, + maxPriorityFeePerGas: 1000000004, + gas: 239420, + balanceNeeded: 1217518987960240, + currentBalance: 7519823030829194, + error: '', + }, + ], + feeEstimate: 627603309182220, + baseFeePerGas: 2289670348, + maxFeeEstimate: 1117518987720820, + }, + ], }, - }); + })); await mockServer - .forGet('https://transaction.api.cx.metamask.io/networks/1/batchStatus') - .withQuery({ uuids: STX_UUID }) - .once() - .thenJson(200, { - [STX_UUID]: { - uuid: STX_UUID, - status: 'success', - statusMetadata: { - minedHash: STX_TRANSACTION_HASH, - minedAt: new Date().toISOString(), - isSettled: true, + .forPost( + `https://transaction.api.cx.metamask.io/networks/${chainId}/submitTransactions`, + ) + .always() + .thenCallback(async (req) => { + let rawTxs: string[] = []; + try { + const body = (await req.body.getJson()) as { rawTxs?: string[] }; + rawTxs = body?.rawTxs ?? []; + } catch { + // ignore JSON parse errors + } + + const txHashes: string[] = []; + for (let i = 0; i < rawTxs.length; i++) { + try { + const response = await fetch(ANVIL_RPC_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_sendRawTransaction', + params: [rawTxs[i]], + id: i + 1, + }), + }); + const data = (await response.json()) as { result?: string }; + if (data.result) { + txHashes.push(data.result); + } + } catch { + // non-fatal: extension computes txHashes locally from rawTxs anyway + } + } + + if (txHashes.length > 0) { + latestMinedHash = txHashes[txHashes.length - 1]; + } + + return { + statusCode: 200, + json: { + uuid: STX_UUID, + txHashes: txHashes.length > 0 ? txHashes : [STX_TRANSACTION_HASH], }, - }, + }; }); await mockServer - .forPost( - 'https://transaction.api.cx.metamask.io/networks/1/submitTransactions', + .forGet( + `https://transaction.api.cx.metamask.io/networks/${chainId}/batchStatus`, ) - .thenJson(200, { - uuid: STX_UUID, - txHashes: [STX_TRANSACTION_HASH], + .always() + .thenCallback((req) => { + const uuid = new URL(req.url).searchParams.get('uuids') ?? STX_UUID; + return { + statusCode: 200, + json: { + [uuid]: { + cancellationFeeWei: 0, + cancellationReason: 'not_cancelled', + deadlineRatio: 0, + isSettled: true, + minedTx: 'success', + wouldRevertMessage: null, + minedHash: latestMinedHash, + timedOut: true, + proxied: false, + type: 'sentinel', + ...batchStatusOverride, + }, + }, + }; }); } @@ -1919,31 +2014,150 @@ export const getGasIncludedSwapFixtures = (title?: string) => { return { forceBip44Version: false, fixtures: fixtureBuilder.build(), - testSpecificMock: async (mockServer: Mockttp) => [ - await mockPortfolioPage(mockServer), - await mockGetTxStatus(mockServer), - await mockTopAssetsLinea(mockServer), - await mockTopAssetsArbitrum(mockServer), - await mockTokensEthereum(mockServer), - await mockTokensLinea(mockServer), - await mockGetTokenArbitrum(mockServer), - await mockGetPopularTokens(mockServer), - await mockGasIncludedSwapETHtoUSDC(mockServer), - await mockGasIncludedSwapUSDCtoDAI(mockServer), - await mockFeatureFlags(mockServer, GAS_INCLUDED_SWAP_FEATURE_FLAGS), - await mockAccountsTransactions(mockServer), - await mockAccountsBalances(mockServer), - await mockPriceSpotPrices(mockServer), - await mockPriceSpotPricesV3(mockServer), - await mockGasPricesMainnet(mockServer), - await mockHistoricalPrices(mockServer), - await mockSentinelNetworks(mockServer), - await mockSmartTransactionApis(mockServer), - ...(await mockSearchTokens(mockServer)), + testSpecificMock: async (mockServer: Mockttp) => { + const mocks = [ + await mockPortfolioPage(mockServer), + await mockGetTxStatus(mockServer), + await mockTopAssetsLinea(mockServer), + await mockTopAssetsArbitrum(mockServer), + await mockTokensEthereum(mockServer), + await mockTokensLinea(mockServer), + await mockGetTokenArbitrum(mockServer), + await mockGetPopularTokens(mockServer), + await mockGasIncludedSwapETHtoUSDC(mockServer), + await mockGasIncludedSwapUSDCtoDAI(mockServer), + await mockFeatureFlags(mockServer, GAS_INCLUDED_SWAP_FEATURE_FLAGS), + await mockAccountsTransactions(mockServer), + await mockAccountsBalances(mockServer), + await mockPriceSpotPrices(mockServer), + await mockPriceSpotPricesV3(mockServer), + await mockGasPricesMainnet(mockServer), + await mockHistoricalPrices(mockServer), + await mockSentinelNetworks(mockServer), + ...(await mockSearchTokens(mockServer)), + ]; + + await mockSmartTransactionsForBridge(mockServer); + + return mocks; + }, + manifestFlags: { + remoteFeatureFlags: { + bridgeConfig: GAS_INCLUDED_SWAP_FEATURE_FLAGS, + ...STX_MAINNET_NETWORK_CONFIG, + }, + testing: { disableSmartTransactionsOverride: true }, + }, + ethConversionInUsd: ETH_CONVERSION_RATE_USD, + smartContract: SMART_CONTRACTS.HST, + localNodeOptions: [ + { + type: 'anvil' as const, + options: { + chainId: 1, + hardfork: 'london', + loadState: + './test/e2e/seeder/network-states/with100Usdc100Usdt50Dai.json', + }, + }, ], + title, + }; +}; + +async function mockGasSponsoredSwapETHtoUSDC(mockServer: Mockttp) { + return await mockServer + .forGet(/getQuoteStream/u) + .once() + .withQuery({ + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + }) + .thenStream( + 200, + mockSseEventSource(MOCK_SWAP_QUOTES_ETH_USDC_GAS_SPONSORED), + SSE_RESPONSE_HEADER, + ); +} + +async function mockSentinelNetworksRelayOnly(mockServer: Mockttp) { + return await mockServer + .forGet( + 'https://tx-sentinel-ethereum-mainnet.api.cx.metamask.io/networks', + ) + .always() + .thenCallback(() => ({ + statusCode: 200, + json: { + '1': { + network: 'ethereum-mainnet', + explorer: 'https://etherscan.io', + confirmations: true, + smartTransactions: true, + relayTransactions: true, + hidden: false, + sendBundle: false, + }, + }, + })); +} + +export const getGasless7702SwapFixtures = (title?: string) => { + const fixtureBuilder = new FixtureBuilder({ + inputChainId: CHAIN_IDS.MAINNET, + }) + .withCurrencyController(MOCK_CURRENCY_RATES) + .withBridgeControllerDefaultState() + .withTokensControllerERC20({ chainId: 1 }) + .withEnabledNetworks({ + eip155: { + '0x1': true, + '0xe708': true, + '0xa4b1': true, + }, + }); + + return { + forceBip44Version: false, + fixtures: fixtureBuilder.build(), + testSpecificMock: async (mockServer: Mockttp) => { + const mocks = [ + await mockPortfolioPage(mockServer), + await mockGetTxStatus(mockServer), + await mockTopAssetsLinea(mockServer), + await mockTopAssetsArbitrum(mockServer), + await mockTokensEthereum(mockServer), + await mockTokensLinea(mockServer), + await mockGetTokenArbitrum(mockServer), + await mockGetPopularTokens(mockServer), + await mockGasSponsoredSwapETHtoUSDC(mockServer), + await mockFeatureFlags(mockServer, GAS_INCLUDED_SWAP_FEATURE_FLAGS), + await mockAccountsTransactions(mockServer), + await mockAccountsBalances(mockServer), + await mockPriceSpotPrices(mockServer), + await mockPriceSpotPricesV3(mockServer), + await mockGasPricesMainnet(mockServer), + await mockHistoricalPrices(mockServer), + await mockSentinelNetworksRelayOnly(mockServer), + ...(await mockSearchTokens(mockServer)), + ]; + + await mockSmartTransactionsForBridge(mockServer); + + return mocks; + }, manifestFlags: { remoteFeatureFlags: { bridgeConfig: GAS_INCLUDED_SWAP_FEATURE_FLAGS, + smartTransactionsNetworks: { + '0x1': { + maxDeadline: 160, + sentinelUrl: STX_MAINNET_SENTINEL_URL, + expectedDeadline: 45, + extensionActive: true, + gaslessBridgeWith7702Enabled: true, + }, + }, }, testing: { disableSmartTransactionsOverride: true }, }, diff --git a/test/e2e/tests/bridge/mocks/swap-quotes-eth-usdc-gas-sponsored.json b/test/e2e/tests/bridge/mocks/swap-quotes-eth-usdc-gas-sponsored.json new file mode 100644 index 000000000000..22680eee7858 --- /dev/null +++ b/test/e2e/tests/bridge/mocks/swap-quotes-eth-usdc-gas-sponsored.json @@ -0,0 +1,85 @@ +[ + { + "quote": { + "requestId": "0xgas-sponsored-swap-eth-usdc-001", + "bridgeId": "openocean", + "srcChainId": 1, + "destChainId": 1, + "aggregator": "openocean", + "aggregatorType": "AGG", + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "assetId": "eip155:1/slip44:60", + "symbol": "ETH", + "decimals": 18, + "name": "Ethereum", + "coingeckoId": "ethereum", + "aggregators": [], + "occurrences": 100, + "iconUrl": "https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/slip44/60.png", + "metadata": {} + }, + "srcTokenAmount": "991250000000000000", + "destAsset": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "chainId": 1, + "assetId": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coingeckoId": "usd-coin", + "aggregators": [], + "occurrences": 14, + "iconUrl": "https://raw.githubusercontent.com/MetaMask/contract-metadata/master/icons/eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.svg", + "metadata": {} + }, + "destTokenAmount": "3011000000", + "minDestTokenAmount": "2950000000", + "walletAddress": "0x5CfE73b6021E818B776b421B1c4Db2474086a7e1", + "destWalletAddress": "0x5CfE73b6021E818B776b421B1c4Db2474086a7e1", + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "assetId": "eip155:1/slip44:60", + "symbol": "ETH", + "decimals": 18, + "name": "Ethereum", + "coingeckoId": "ethereum", + "aggregators": [], + "occurrences": 100, + "iconUrl": "https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/slip44/60.png", + "metadata": {} + }, + "quoteBpsFee": 87.5, + "baseBpsFee": 87.5 + } + }, + "bridges": ["openocean"], + "protocols": ["openocean"], + "steps": [], + "slippage": 2, + "gasSponsored": true, + "gasIncluded": false, + "gasIncluded7702": true, + "priceData": { + "totalFromAmountUsd": "3010.00", + "totalToAmountUsd": "3011.00", + "priceImpact": "0.001", + "totalFeeAmountUsd": "0.00" + } + }, + "trade": { + "chainId": 1, + "to": "0x881D40237659C251811CEC9c364ef91dC08D300C", + "from": "0x5CfE73b6021E818B776b421B1c4Db2474086a7e1", + "value": "0xde0b6b3a7640000", + "data": "0x5f575529000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000136f70656e4f6365616e46656544796e616d696300000000000000000000000000", + "gasLimit": 448721 + }, + "estimatedProcessingTimeInSeconds": 0 + } +] diff --git a/test/e2e/tests/bridge/swap-gasless.spec.ts b/test/e2e/tests/bridge/swap-gasless.spec.ts index 69945efe5e79..6a4ba5a464ad 100644 --- a/test/e2e/tests/bridge/swap-gasless.spec.ts +++ b/test/e2e/tests/bridge/swap-gasless.spec.ts @@ -4,7 +4,10 @@ import { login } from '../../page-objects/flows/login.flow'; import HomePage from '../../page-objects/pages/home/homepage'; import BridgeQuotePage from '../../page-objects/pages/bridge/quote-page'; import ActivityListPage from '../../page-objects/pages/home/activity-list'; -import { getGasIncludedSwapFixtures } from './bridge-test-utils'; +import { + getGasIncludedSwapFixtures, + getGasless7702SwapFixtures, +} from './bridge-test-utils'; describe('Gas included swap tests', function (this: Suite) { this.timeout(160000); @@ -84,4 +87,38 @@ describe('Gas included swap tests', function (this: Suite) { }, ); }); + + it('swaps ETH to USDC with gas sponsored via 7702 relay', async function () { + await withFixtures( + getGasless7702SwapFixtures(this.test?.fullTitle()), + async ({ driver }) => { + await login(driver, { expectedBalance: '$225,730.11' }); + + const homePage = new HomePage(driver); + await homePage.checkPageIsLoaded(); + + await homePage.startSwapFlow(); + + const bridgePage = new BridgeQuotePage(driver); + await bridgePage.checkPageIsLoaded(); + + await bridgePage.selectDestToken('USDC'); + + await bridgePage.clickMaxButton(); + + await bridgePage.waitForQuote(); + await bridgePage.checkGasSponsoredIsDisplayed(); + + await bridgePage.submitQuote(); + + await homePage.goToActivityList(); + const activityList = new ActivityListPage(driver); + await activityList.checkCompletedBridgeTransactionActivity(1); + await activityList.checkTxAction({ + action: 'Swap ETH to USDC', + confirmedTx: 1, + }); + }, + ); + }); }); From 92327418c712f8c5a27cf744ace92efbb3bd764e Mon Sep 17 00:00:00 2001 From: Davide Brocchetto Date: Fri, 3 Apr 2026 15:22:46 -0700 Subject: [PATCH 05/10] fix: prettier formatting, JSDoc params, and deduplicate feature flags constant - Fix prettier line-length violations in mockSentinelNetworks functions - Add missing @param JSDoc tags to mockSmartTransactionsForBridge - Remove trailing blank line in swap-gasless.spec.ts - Replace duplicate GAS_INCLUDED_SWAP_FEATURE_FLAGS with existing BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED from constants.ts Co-Authored-By: Claude Sonnet 4.6 --- test/e2e/tests/bridge/bridge-test-utils.ts | 31 +++++++++------------- test/e2e/tests/bridge/swap-gasless.spec.ts | 1 - 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index 3ac2afdbc1ee..fcc43aae4105 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -41,6 +41,7 @@ import { EXPECTED_INPUT_CHANGES, BRIDGE_REFRESH_RATE, DEFAULT_BRIDGE_FEATURE_FLAGS, + BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED, } from './constants'; import MOCK_SWAP_QUOTES_ETH_MUSD from './mocks/swap-quotes-eth-musd.json'; import MOCK_SWAP_QUOTES_ETH_USDC_GAS_INCLUDED from './mocks/swap-quotes-eth-usdc-gas-included.json'; @@ -1779,9 +1780,7 @@ export const checkInputChangedEvents = async ( async function mockSentinelNetworks(mockServer: Mockttp) { return await mockServer - .forGet( - 'https://tx-sentinel-ethereum-mainnet.api.cx.metamask.io/networks', - ) + .forGet('https://tx-sentinel-ethereum-mainnet.api.cx.metamask.io/networks') .always() .thenCallback(() => ({ statusCode: 200, @@ -1854,6 +1853,11 @@ const STX_MAINNET_NETWORK_CONFIG = { * via eth_sendRawTransaction. Anvil mines it immediately, so the real tx hash is * on-chain and eth_getTransactionReceipt returns a genuine receipt — which is what * the extension's TransactionController needs to mark the transaction as Confirmed. + * + * @param mockServer - The Mockttp server instance + * @param chainId - The chain ID to mock STX endpoints for + * @param sentinelUrl - The sentinel base URL for this chain + * @param batchStatusOverride - Optional overrides for the batchStatus response */ async function mockSmartTransactionsForBridge( mockServer: Mockttp, @@ -1986,15 +1990,6 @@ async function mockSmartTransactionsForBridge( }); } -export const GAS_INCLUDED_SWAP_FEATURE_FLAGS: FeatureFlagResponse & { - minimumVersion: string; -} = { - ...DEFAULT_BRIDGE_FEATURE_FLAGS, - sse: { - enabled: true, - minimumVersion: '13.2.0', - }, -}; export const getGasIncludedSwapFixtures = (title?: string) => { const fixtureBuilder = new FixtureBuilder({ @@ -2026,7 +2021,7 @@ export const getGasIncludedSwapFixtures = (title?: string) => { await mockGetPopularTokens(mockServer), await mockGasIncludedSwapETHtoUSDC(mockServer), await mockGasIncludedSwapUSDCtoDAI(mockServer), - await mockFeatureFlags(mockServer, GAS_INCLUDED_SWAP_FEATURE_FLAGS), + await mockFeatureFlags(mockServer, BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED), await mockAccountsTransactions(mockServer), await mockAccountsBalances(mockServer), await mockPriceSpotPrices(mockServer), @@ -2043,7 +2038,7 @@ export const getGasIncludedSwapFixtures = (title?: string) => { }, manifestFlags: { remoteFeatureFlags: { - bridgeConfig: GAS_INCLUDED_SWAP_FEATURE_FLAGS, + bridgeConfig: BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED, ...STX_MAINNET_NETWORK_CONFIG, }, testing: { disableSmartTransactionsOverride: true }, @@ -2082,9 +2077,7 @@ async function mockGasSponsoredSwapETHtoUSDC(mockServer: Mockttp) { async function mockSentinelNetworksRelayOnly(mockServer: Mockttp) { return await mockServer - .forGet( - 'https://tx-sentinel-ethereum-mainnet.api.cx.metamask.io/networks', - ) + .forGet('https://tx-sentinel-ethereum-mainnet.api.cx.metamask.io/networks') .always() .thenCallback(() => ({ statusCode: 200, @@ -2131,7 +2124,7 @@ export const getGasless7702SwapFixtures = (title?: string) => { await mockGetTokenArbitrum(mockServer), await mockGetPopularTokens(mockServer), await mockGasSponsoredSwapETHtoUSDC(mockServer), - await mockFeatureFlags(mockServer, GAS_INCLUDED_SWAP_FEATURE_FLAGS), + await mockFeatureFlags(mockServer, BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED), await mockAccountsTransactions(mockServer), await mockAccountsBalances(mockServer), await mockPriceSpotPrices(mockServer), @@ -2148,7 +2141,7 @@ export const getGasless7702SwapFixtures = (title?: string) => { }, manifestFlags: { remoteFeatureFlags: { - bridgeConfig: GAS_INCLUDED_SWAP_FEATURE_FLAGS, + bridgeConfig: BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED, smartTransactionsNetworks: { '0x1': { maxDeadline: 160, diff --git a/test/e2e/tests/bridge/swap-gasless.spec.ts b/test/e2e/tests/bridge/swap-gasless.spec.ts index 6a4ba5a464ad..b89ad63a5046 100644 --- a/test/e2e/tests/bridge/swap-gasless.spec.ts +++ b/test/e2e/tests/bridge/swap-gasless.spec.ts @@ -83,7 +83,6 @@ describe('Gas included swap tests', function (this: Suite) { confirmedTx: 0, txIndex: 2, }); - }, ); }); From acb9b14b0c28d365c422a050ee6ce1d233f8bbd7 Mon Sep 17 00:00:00 2001 From: Davide Brocchetto Date: Fri, 3 Apr 2026 15:34:29 -0700 Subject: [PATCH 06/10] fix: enable STX for gasless swap fixtures and restore mockFeatureFlags additionalFlags param - Remove disableSmartTransactionsOverride from getGasIncludedSwapFixtures and getGasless7702SwapFixtures so STX is active during these tests - Pass STX network config to mockFeatureFlags in both fixtures so the client-config mock is consistent with manifestFlags - Restore additionalFlags parameter on mockFeatureFlags (was reverted) Co-Authored-By: Claude Sonnet 4.6 --- test/e2e/tests/bridge/bridge-test-utils.ts | 183 ++------------------- 1 file changed, 16 insertions(+), 167 deletions(-) diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index fcc43aae4105..75123af0e359 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -1828,169 +1828,6 @@ async function mockGasIncludedSwapUSDCtoDAI(mockServer: Mockttp) { ); } -const STX_UUID = '0d506aaa-5e38-4cab-ad09-2039cb7a0f33'; -const STX_TRANSACTION_HASH = - '0xec9d6214684d6dc191133ae4a7ec97db3e521fff9cfe5c4f48a84cb6c93a5fa5'; - -const STX_MAINNET_SENTINEL_URL = - 'https://tx-sentinel-ethereum-mainnet.api.cx.metamask.io'; - -const STX_MAINNET_NETWORK_CONFIG = { - smartTransactionsNetworks: { - '0x1': { - extensionActive: true, - sentinelUrl: STX_MAINNET_SENTINEL_URL, - expectedDeadline: 45, - maxDeadline: 160, - }, - }, -}; - -/** - * Mocks STX service endpoints for bridge/swap tests. - * - * submitTransactions forwards each raw signed tx to Anvil - * via eth_sendRawTransaction. Anvil mines it immediately, so the real tx hash is - * on-chain and eth_getTransactionReceipt returns a genuine receipt — which is what - * the extension's TransactionController needs to mark the transaction as Confirmed. - * - * @param mockServer - The Mockttp server instance - * @param chainId - The chain ID to mock STX endpoints for - * @param sentinelUrl - The sentinel base URL for this chain - * @param batchStatusOverride - Optional overrides for the batchStatus response - */ -async function mockSmartTransactionsForBridge( - mockServer: Mockttp, - chainId: number = 1, - sentinelUrl: string = STX_MAINNET_SENTINEL_URL, - batchStatusOverride?: Record, -) { - const ANVIL_RPC_URL = 'http://localhost:8545'; - - let latestMinedHash = STX_TRANSACTION_HASH; - - await mockServer - .forGet(`${sentinelUrl}/network`) - .always() - .thenCallback(() => ({ - ok: true, - statusCode: 200, - json: { smartTransactions: true }, - })); - - await mockServer - .forPost( - `https://transaction.api.cx.metamask.io/networks/${chainId}/getFees`, - ) - .always() - .thenCallback(() => ({ - statusCode: 200, - json: { - blockNumber: 20728974, - id: '19d4eea3-8a49-463e-9e9c-099f9d9571ca', - txs: [ - { - cancelFees: [], - return: '0x', - status: 1, - gasUsed: 190780, - gasLimit: 239420, - fees: [ - { - maxFeePerGas: 4667609171, - maxPriorityFeePerGas: 1000000004, - gas: 239420, - balanceNeeded: 1217518987960240, - currentBalance: 7519823030829194, - error: '', - }, - ], - feeEstimate: 627603309182220, - baseFeePerGas: 2289670348, - maxFeeEstimate: 1117518987720820, - }, - ], - }, - })); - - await mockServer - .forPost( - `https://transaction.api.cx.metamask.io/networks/${chainId}/submitTransactions`, - ) - .always() - .thenCallback(async (req) => { - let rawTxs: string[] = []; - try { - const body = (await req.body.getJson()) as { rawTxs?: string[] }; - rawTxs = body?.rawTxs ?? []; - } catch { - // ignore JSON parse errors - } - - const txHashes: string[] = []; - for (let i = 0; i < rawTxs.length; i++) { - try { - const response = await fetch(ANVIL_RPC_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - jsonrpc: '2.0', - method: 'eth_sendRawTransaction', - params: [rawTxs[i]], - id: i + 1, - }), - }); - const data = (await response.json()) as { result?: string }; - if (data.result) { - txHashes.push(data.result); - } - } catch { - // non-fatal: extension computes txHashes locally from rawTxs anyway - } - } - - if (txHashes.length > 0) { - latestMinedHash = txHashes[txHashes.length - 1]; - } - - return { - statusCode: 200, - json: { - uuid: STX_UUID, - txHashes: txHashes.length > 0 ? txHashes : [STX_TRANSACTION_HASH], - }, - }; - }); - - await mockServer - .forGet( - `https://transaction.api.cx.metamask.io/networks/${chainId}/batchStatus`, - ) - .always() - .thenCallback((req) => { - const uuid = new URL(req.url).searchParams.get('uuids') ?? STX_UUID; - return { - statusCode: 200, - json: { - [uuid]: { - cancellationFeeWei: 0, - cancellationReason: 'not_cancelled', - deadlineRatio: 0, - isSettled: true, - minedTx: 'success', - wouldRevertMessage: null, - minedHash: latestMinedHash, - timedOut: true, - proxied: false, - type: 'sentinel', - ...batchStatusOverride, - }, - }, - }; - }); -} - - export const getGasIncludedSwapFixtures = (title?: string) => { const fixtureBuilder = new FixtureBuilder({ inputChainId: CHAIN_IDS.MAINNET, @@ -2021,7 +1858,11 @@ export const getGasIncludedSwapFixtures = (title?: string) => { await mockGetPopularTokens(mockServer), await mockGasIncludedSwapETHtoUSDC(mockServer), await mockGasIncludedSwapUSDCtoDAI(mockServer), - await mockFeatureFlags(mockServer, BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED), + await mockFeatureFlags( + mockServer, + BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED, + STX_MAINNET_NETWORK_CONFIG, + ), await mockAccountsTransactions(mockServer), await mockAccountsBalances(mockServer), await mockPriceSpotPrices(mockServer), @@ -2041,7 +1882,6 @@ export const getGasIncludedSwapFixtures = (title?: string) => { bridgeConfig: BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED, ...STX_MAINNET_NETWORK_CONFIG, }, - testing: { disableSmartTransactionsOverride: true }, }, ethConversionInUsd: ETH_CONVERSION_RATE_USD, smartContract: SMART_CONTRACTS.HST, @@ -2124,7 +1964,17 @@ export const getGasless7702SwapFixtures = (title?: string) => { await mockGetTokenArbitrum(mockServer), await mockGetPopularTokens(mockServer), await mockGasSponsoredSwapETHtoUSDC(mockServer), - await mockFeatureFlags(mockServer, BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED), + await mockFeatureFlags(mockServer, BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED, { + smartTransactionsNetworks: { + '0x1': { + maxDeadline: 160, + sentinelUrl: STX_MAINNET_SENTINEL_URL, + expectedDeadline: 45, + extensionActive: true, + gaslessBridgeWith7702Enabled: true, + }, + }, + }), await mockAccountsTransactions(mockServer), await mockAccountsBalances(mockServer), await mockPriceSpotPrices(mockServer), @@ -2152,7 +2002,6 @@ export const getGasless7702SwapFixtures = (title?: string) => { }, }, }, - testing: { disableSmartTransactionsOverride: true }, }, ethConversionInUsd: ETH_CONVERSION_RATE_USD, smartContract: SMART_CONTRACTS.HST, From 1c6412dec64e870a1fba3ad42e387c1a631be48f Mon Sep 17 00:00:00 2001 From: Davide Brocchetto Date: Fri, 3 Apr 2026 15:36:24 -0700 Subject: [PATCH 07/10] test: rename describe block to 'Gasless swap tests' Co-Authored-By: Claude Sonnet 4.6 --- test/e2e/tests/bridge/swap-gasless.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/tests/bridge/swap-gasless.spec.ts b/test/e2e/tests/bridge/swap-gasless.spec.ts index b89ad63a5046..20e17efb36f9 100644 --- a/test/e2e/tests/bridge/swap-gasless.spec.ts +++ b/test/e2e/tests/bridge/swap-gasless.spec.ts @@ -9,7 +9,7 @@ import { getGasless7702SwapFixtures, } from './bridge-test-utils'; -describe('Gas included swap tests', function (this: Suite) { +describe('Gasless swap tests', function (this: Suite) { this.timeout(160000); it('swaps ETH to USDC with gas included via Max button', async function () { From 01056481c2de57468dd483000c6fc3b4eaba878f Mon Sep 17 00:00:00 2001 From: Davide Brocchetto Date: Mon, 6 Apr 2026 10:07:52 -0700 Subject: [PATCH 08/10] refactor: deduplicate mockSentinelNetworks functions Merge mockSentinelNetworksRelayOnly into mockSentinelNetworks by adding a sendBundle parameter (default true), eliminating the near-identical duplicate. Co-Authored-By: Claude Sonnet 4.6 --- test/e2e/tests/bridge/bridge-test-utils.ts | 40 ++++++++-------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index 75123af0e359..eda48a406d1e 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -1916,23 +1916,7 @@ async function mockGasSponsoredSwapETHtoUSDC(mockServer: Mockttp) { } async function mockSentinelNetworksRelayOnly(mockServer: Mockttp) { - return await mockServer - .forGet('https://tx-sentinel-ethereum-mainnet.api.cx.metamask.io/networks') - .always() - .thenCallback(() => ({ - statusCode: 200, - json: { - '1': { - network: 'ethereum-mainnet', - explorer: 'https://etherscan.io', - confirmations: true, - smartTransactions: true, - relayTransactions: true, - hidden: false, - sendBundle: false, - }, - }, - })); + return mockSentinelNetworks(mockServer, false); } export const getGasless7702SwapFixtures = (title?: string) => { @@ -1964,17 +1948,21 @@ export const getGasless7702SwapFixtures = (title?: string) => { await mockGetTokenArbitrum(mockServer), await mockGetPopularTokens(mockServer), await mockGasSponsoredSwapETHtoUSDC(mockServer), - await mockFeatureFlags(mockServer, BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED, { - smartTransactionsNetworks: { - '0x1': { - maxDeadline: 160, - sentinelUrl: STX_MAINNET_SENTINEL_URL, - expectedDeadline: 45, - extensionActive: true, - gaslessBridgeWith7702Enabled: true, + await mockFeatureFlags( + mockServer, + BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED, + { + smartTransactionsNetworks: { + '0x1': { + maxDeadline: 160, + sentinelUrl: STX_MAINNET_SENTINEL_URL, + expectedDeadline: 45, + extensionActive: true, + gaslessBridgeWith7702Enabled: true, + }, }, }, - }), + ), await mockAccountsTransactions(mockServer), await mockAccountsBalances(mockServer), await mockPriceSpotPrices(mockServer), From 168febcbb94f793260a0765104f08f7426b07c3f Mon Sep 17 00:00:00 2001 From: Davide Brocchetto Date: Mon, 6 Apr 2026 15:58:36 -0700 Subject: [PATCH 09/10] fix: add sendBundle param to mockSentinelNetworks The previous refactor delegated mockSentinelNetworksRelayOnly to mockSentinelNetworks(mockServer, false) but forgot to add the parameter, leaving sendBundle hardcoded to true in both cases. Co-Authored-By: Claude Sonnet 4.6 --- test/e2e/tests/bridge/bridge-test-utils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index eda48a406d1e..eb92981f64d1 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -1778,7 +1778,10 @@ export const checkInputChangedEvents = async ( return expectedInputChanges.length; }; -async function mockSentinelNetworks(mockServer: Mockttp) { +async function mockSentinelNetworks( + mockServer: Mockttp, + sendBundle: boolean = true, +) { return await mockServer .forGet('https://tx-sentinel-ethereum-mainnet.api.cx.metamask.io/networks') .always() @@ -1792,7 +1795,7 @@ async function mockSentinelNetworks(mockServer: Mockttp) { smartTransactions: true, relayTransactions: true, hidden: false, - sendBundle: true, + sendBundle, }, }, })); From ac23f85c63efcea706ee35d252457576a17312ca Mon Sep 17 00:00:00 2001 From: Davide Brocchetto Date: Mon, 6 Apr 2026 17:15:41 -0700 Subject: [PATCH 10/10] fix: remove unused DEFAULT_BRIDGE_FEATURE_FLAGS import Made-with: Cursor --- test/e2e/tests/bridge/bridge-test-utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index eb92981f64d1..66cfd79c4114 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -40,7 +40,6 @@ import { SSE_RESPONSE_HEADER, EXPECTED_INPUT_CHANGES, BRIDGE_REFRESH_RATE, - DEFAULT_BRIDGE_FEATURE_FLAGS, BRIDGE_FEATURE_FLAGS_WITH_SSE_ENABLED, } from './constants'; import MOCK_SWAP_QUOTES_ETH_MUSD from './mocks/swap-quotes-eth-musd.json';