const { strict: assert } = require('assert'); const { toHex } = require('@metamask/controller-utils'); const FixtureBuilder = require('../fixture-builder'); const { withFixtures, generateGanacheOptions, DEFAULT_GANACHE_OPTIONS, unlockWallet, getEventPayloads, assertInAnyOrder, genRandInitBal, } = require('../helpers'); const { buildQuote, reviewQuote, waitForTransactionToComplete, checkActivityTransaction, changeExchangeRate, } = require('../swaps/shared'); const { MetaMetricsEventCategory, MetaMetricsEventName, } = require('../../../shared/constants/metametrics'); const { TOKENS_API_MOCK_RESULT, TOP_ASSETS_API_MOCK_RESULT, AGGREGATOR_METADATA_API_MOCK_RESULT, GAS_PRICE_API_MOCK_RESULT, FEATURE_FLAGS_API_MOCK_RESULT, NETWORKS_API_MOCK_RESULT, TRADES_API_MOCK_RESULT, NETWORKS_2_API_MOCK_RESULT, } = require('./mock-data'); const numberOfSegmentRequests = 19; async function mockSegmentAndMetaswapRequests(mockServer) { return [ await mockServer .forPost('https://api.segment.io/v1/batch') .withJsonBodyIncluding({ batch: [{ properties: { category: MetaMetricsEventCategory.Swaps } }], }) .times() .thenCallback(() => ({ statusCode: 200 })), await mockServer .forGet('https://swap.metaswap.codefi.network/networks/1/tokens') .thenCallback(() => ({ statusCode: 200, json: TOKENS_API_MOCK_RESULT })), await mockServer .forGet('https://swap.metaswap.codefi.network/networks/1/topAssets') .thenCallback(() => ({ statusCode: 200, json: TOP_ASSETS_API_MOCK_RESULT, })), await mockServer .forGet( 'https://swap.metaswap.codefi.network/networks/1/aggregatorMetadata', ) .thenCallback(() => ({ statusCode: 200, json: AGGREGATOR_METADATA_API_MOCK_RESULT, })), await mockServer .forGet('https://gas-api.metaswap.codefi.network/networks/1/gasPrices') .thenCallback(() => ({ statusCode: 200, json: GAS_PRICE_API_MOCK_RESULT, })), await mockServer .forGet('https://swap.metaswap.codefi.network/featureFlags') .thenCallback(() => ({ statusCode: 200, json: FEATURE_FLAGS_API_MOCK_RESULT, })), await mockServer .forGet('https://tx-insights.metaswap.codefi.network/networks') .thenCallback(() => ({ statusCode: 200, json: NETWORKS_API_MOCK_RESULT, })), await mockServer .forGet('https://swap.metaswap.codefi.network/networks/1/trades') .thenCallback(() => ({ statusCode: 200, json: TRADES_API_MOCK_RESULT, })), await mockServer .forGet('https://swap.metaswap.codefi.network/networks/1') .thenCallback(() => ({ statusCode: 200, json: NETWORKS_2_API_MOCK_RESULT, })), await mockServer .forGet('https://token-api.metaswap.codefi.network/token/1337') .thenCallback(() => ({ statusCode: 200, json: {}, })), ]; } describe('Swap Eth for another Token', function () { it('Completes a Swap between ETH and DAI after changing initial rate', async function () { const { initialBalanceInHex } = genRandInitBal(); await withFixtures( { fixtures: new FixtureBuilder() .withMetaMetricsController({ metaMetricsId: 'fake-metrics-id', participateInMetaMetrics: true, }) .build(), ganacheOptions: generateGanacheOptions({ accounts: [ { secretKey: DEFAULT_GANACHE_OPTIONS.accounts[0].secretKey, balance: initialBalanceInHex, }, ], }), title: this.test.title, testSpecificMock: mockSegmentAndMetaswapRequests, }, async ({ driver, mockedEndpoint: mockedEndpoints }) => { await driver.navigate(); await unlockWallet(driver); await getQuoteAndSwapTokens(driver); const metricsReqs = await assertReqsNumAndFilterMetrics( driver, mockedEndpoints, ); await assertNavSwapButtonClickedEvent(metricsReqs); await assertPrepareSwapPageLoadedEvents(metricsReqs); await assertQuotesRequestedEvents(metricsReqs); await assertQuotesReceivedAndBestQuoteReviewedEvents(metricsReqs); await assertAllAvailableQuotesOpenedEvents(metricsReqs); await assertSwapStartedEvents(metricsReqs); await assertSwapCompletedEvents(metricsReqs); await assertExitedSwapsEvents(metricsReqs); }, ); }); }); async function getQuoteAndSwapTokens(driver) { await buildQuote(driver, { amount: 2, swapTo: 'DAI', }); await reviewQuote(driver, { amount: 2, swapFrom: 'TESTETH', swapTo: 'DAI', }); await changeExchangeRate(driver); await reviewQuote(driver, { amount: 2, swapFrom: 'TESTETH', swapTo: 'DAI', skipCounter: true, }); await driver.clickElement({ text: 'Swap', tag: 'button' }); await waitForTransactionToComplete(driver, { tokenName: 'DAI' }); await checkActivityTransaction(driver, { index: 0, amount: '2', swapFrom: 'TESTETH', swapTo: 'DAI', }); } async function assertReqsNumAndFilterMetrics(driver, mockedEndpoints) { const events = await getEventPayloads(driver, mockedEndpoints); const numberOfMetaswapRequests = 9; assert.equal( events.length, numberOfSegmentRequests + numberOfMetaswapRequests, ); const reqs = events.slice(0, numberOfSegmentRequests); return reqs; } async function assertNavSwapButtonClickedEvent(reqs) { assert.equal(reqs[0].event, MetaMetricsEventName.NavSwapButtonClicked); assert.deepStrictEqual(reqs[0].properties, { category: MetaMetricsEventCategory.Swaps, chain_id: toHex(1337), environment_type: 'fullscreen', locale: 'en', location: 'Main View', text: 'Swap', token_symbol: 'ETH', }); } async function assertPrepareSwapPageLoadedEvents(reqs) { const assertionsReq1 = [ (req) => req.event === MetaMetricsEventName.PrepareSwapPageLoaded, (req) => Object.keys(req.properties).length === 7, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', (req) => req.properties?.current_stx_enabled === false, (req) => req.properties?.is_hardware_wallet === false, (req) => req.properties?.stx_enabled === false, ]; const assertionsReq2 = [ (req) => req.event === MetaMetricsEventName.PrepareSwapPageLoaded, (req) => Object.keys(req.properties).length === 4, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', ]; assert.ok( assertInAnyOrder([reqs[1], reqs[2]], [assertionsReq1, assertionsReq2]), 'assertPrepareSwapPageLoadedEvents(): reqs[1] and reqs[2] did not match what was expected', ); } async function assertQuotesRequestedEvents(reqs) { const assertionsReq3 = [ (req) => req.event === MetaMetricsEventName.QuotesRequested, (req) => Object.keys(req.properties).length === 14, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', (req) => req.properties?.anonymizedData === true, (req) => req.properties?.current_stx_enabled === false, (req) => req.properties?.custom_slippage === false, (req) => req.properties?.is_hardware_wallet === false, (req) => req.properties?.request_type === 'Order', (req) => req.properties?.slippage === 2, (req) => req.properties?.stx_enabled === false, (req) => req.properties?.token_from === 'TESTETH', (req) => req.properties?.token_from_amount === '2', (req) => req.properties?.token_to === 'DAI', ]; const assertionsReq4 = [ (req) => req.event === MetaMetricsEventName.QuotesRequested, (req) => Object.keys(req.properties).length === 4, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', ]; assert.ok( assertInAnyOrder([reqs[3], reqs[4]], [assertionsReq3, assertionsReq4]), 'assertQuotesRequestedEvents(): reqs[3] and reqs[4] did not match what was expected', ); } async function assertQuotesReceivedAndBestQuoteReviewedEvents(reqs) { const assertionsReq5 = [ (req) => req.event === MetaMetricsEventName.QuotesReceived, (req) => Object.keys(req.properties).length === 18, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', (req) => req.properties?.anonymizedData === true, (req) => typeof req.properties?.available_quotes === 'number', (req) => typeof req.properties?.best_quote_source === 'string', (req) => req.properties?.current_stx_enabled === false, (req) => req.properties?.custom_slippage === false, (req) => req.properties?.is_hardware_wallet === false, (req) => req.properties?.request_type === 'Order', (req) => typeof req.properties?.response_time === 'number', (req) => req.properties?.slippage === 2, (req) => req.properties?.stx_enabled === false, (req) => req.properties?.token_from === 'TESTETH', (req) => req.properties?.token_from_amount === '2', (req) => req.properties?.token_to === 'DAI', (req) => typeof req.properties?.token_to_amount === 'string', ]; const assertionsReq6 = [ (req) => req.event === MetaMetricsEventName.QuotesReceived, (req) => Object.keys(req.properties).length === 4, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', ]; const assertionsReq7 = [ (req) => req.event === MetaMetricsEventName.BestQuoteReviewed, (req) => Object.keys(req.properties).length === 17, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', (req) => typeof req.properties?.available_quotes === 'number', (req) => typeof req.properties?.best_quote_source === 'string', (req) => req.properties?.current_stx_enabled === false, (req) => req.properties?.custom_slippage === false, (req) => req.properties?.is_hardware_wallet === false, (req) => req.properties?.request_type === false, (req) => req.properties?.slippage === 2, (req) => req.properties?.stx_enabled === false, (req) => req.properties?.token_from === 'TESTETH', (req) => req.properties?.token_from_amount === '2', (req) => req.properties?.token_to === 'DAI', (req) => typeof req.properties?.token_to_amount === 'string', ]; const assertionsReq8 = [ (req) => req.event === MetaMetricsEventName.BestQuoteReviewed, (req) => Object.keys(req.properties).length === 4, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', ]; // When running this test on Chrome in particular, reqs[5], reqs[6], reqs[7] // and reqs[8] sometimes switch order so we bundled them together for the // assertion assert.ok( assertInAnyOrder( [reqs[5], reqs[6], reqs[7], reqs[8]], [assertionsReq5, assertionsReq6, assertionsReq7, assertionsReq8], ), 'assertQuotesReceivedAndBestQuoteReviewedEvents(): reqs[5], reqs[6], reqs[7] and reqs[8] did not match what was expected', ); } async function assertAllAvailableQuotesOpenedEvents(reqs) { const assertionsReq9 = [ (req) => req.event === MetaMetricsEventName.AllAvailableQuotesOpened, (req) => Object.keys(req.properties).length === 18, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', (req) => typeof req.properties?.available_quotes === 'number', (req) => typeof req.properties?.best_quote_source === 'string', (req) => req.properties?.current_stx_enabled === false, (req) => req.properties?.custom_slippage === false, (req) => req.properties?.is_hardware_wallet === false, (req) => req.properties?.request_type === false, (req) => req.properties?.slippage === 2, (req) => req.properties?.stx_enabled === false, (req) => req.properties?.token_from === 'TESTETH', (req) => req.properties?.token_from_amount === '2', (req) => req.properties?.token_to === 'DAI', (req) => req.properties?.token_to === 'DAI', (req) => req.properties?.other_quote_selected === false, (req) => req.properties?.other_quote_selected_source === null, (req) => typeof req.properties?.token_to_amount === 'string', ]; const assertionsReq10 = [ (req) => req.event === MetaMetricsEventName.AllAvailableQuotesOpened, (req) => Object.keys(req.properties).length === 4, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', ]; assert.ok( assertInAnyOrder([reqs[9], reqs[10]], [assertionsReq9, assertionsReq10]), 'assertAllAvailableQuotesOpenedEvents(): reqs[9] and reqs[10] did not match what was expected', ); } async function assertSwapStartedEvents(reqs) { const assertionsReq11 = [ (req) => req.event === MetaMetricsEventName.SwapStarted, (req) => Object.keys(req.properties).length === 24, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', (req) => req.properties?.token_from === 'TESTETH', (req) => req.properties?.token_from_amount === '2', (req) => req.properties?.token_to === 'DAI', (req) => req.properties?.slippage === 2, (req) => req.properties?.custom_slippage === false, (req) => req.properties?.is_hardware_wallet === false, (req) => req.properties?.stx_enabled === false, (req) => req.properties?.current_stx_enabled === false, (req) => typeof req.properties?.token_to_amount === 'string', (req) => typeof req.properties?.best_quote_source === 'string', (req) => typeof req.properties?.other_quote_selected === 'boolean', (req) => typeof req.properties?.gas_fees === 'string', (req) => typeof req.properties?.estimated_gas === 'string', (req) => typeof req.properties?.suggested_gas_price === 'string', (req) => typeof req.properties?.reg_tx_fee_in_usd === 'number', (req) => typeof req.properties?.reg_tx_fee_in_eth === 'number', (req) => typeof req.properties?.reg_tx_max_fee_in_usd === 'number', (req) => typeof req.properties?.reg_tx_max_fee_in_eth === 'number', (req) => typeof req.properties?.other_quote_selected_source === 'string', ]; const assertionsReq12 = [ (req) => req.event === MetaMetricsEventName.SwapStarted, (req) => Object.keys(req.properties).length === 4, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', ]; assert.ok( assertInAnyOrder([reqs[11], reqs[12]], [assertionsReq11, assertionsReq12]), 'assertSwapStartedEvents(): reqs[11] and reqs[12] did not match what was expected', ); } async function assertSwapCompletedEvents(reqs) { const assertionsReq13 = [ (req) => req.event === MetaMetricsEventName.SwapCompleted, (req) => Object.keys(req.properties).length === 30, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'background', (req) => req.properties?.locale === 'en', (req) => req.properties?.token_from === 'TESTETH', (req) => req.properties?.token_from_amount === '2', (req) => req.properties?.token_to === 'DAI', (req) => typeof req.properties?.token_to_amount === 'string', (req) => req.properties?.slippage === 2, (req) => req.properties?.custom_slippage === false, (req) => req.properties?.best_quote_source === 'airswapV4', (req) => typeof req.properties?.other_quote_selected === 'boolean', (req) => typeof req.properties?.other_quote_selected_source === 'string', (req) => typeof req.properties?.gas_fees === 'string', (req) => typeof req.properties?.estimated_gas === 'string', (req) => req.properties?.suggested_gas_price === '30', (req) => req.properties?.used_gas_price === '30', (req) => req.properties?.is_hardware_wallet === false, (req) => req.properties?.stx_enabled === false, (req) => req.properties?.current_stx_enabled === false, (req) => typeof req.properties?.reg_tx_fee_in_usd === 'number', (req) => typeof req.properties?.reg_tx_fee_in_eth === 'number', (req) => typeof req.properties?.reg_tx_max_fee_in_usd === 'number', (req) => typeof req.properties?.reg_tx_max_fee_in_eth === 'number', (req) => req.properties?.token_to_amount_received === '', (req) => req.properties?.quote_vs_executionRatio === null, (req) => req.properties?.estimated_vs_used_gasRatio === '100%', (req) => req.properties?.approval_gas_cost_in_eth === 0, (req) => typeof req.properties?.trade_gas_cost_in_eth === 'number', (req) => typeof req.properties?.trade_and_approval_gas_cost_in_eth === 'number', ]; const assertionsReq14 = [ (req) => req.event === MetaMetricsEventName.SwapCompleted, (req) => Object.keys(req.properties).length === 4, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'background', (req) => req.properties?.locale === 'en', ]; assert.ok( assertInAnyOrder([reqs[13], reqs[14]], [assertionsReq13, assertionsReq14]), 'assertSwapCompletedEvents(): reqs[13] and reqs[14] did not match what was expected', ); } async function assertExitedSwapsEvents(reqs) { const assertionsReq15 = [ (req) => req.event === MetaMetricsEventName.ExitedSwaps, (req) => Object.keys(req.properties).length === 12, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', (req) => req.properties?.token_from_amount === '2', (req) => req.properties?.request_type === false, (req) => req.properties?.slippage === 2, (req) => req.properties?.custom_slippage === false, (req) => req.properties?.current_screen === 'awaiting-swap', (req) => req.properties?.is_hardware_wallet === false, (req) => req.properties?.stx_enabled === false, (req) => req.properties?.current_stx_enabled === false, ]; const assertionsReq16 = [ (req) => req.event === MetaMetricsEventName.ExitedSwaps, (req) => Object.keys(req.properties).length === 4, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', ]; assert.ok( assertInAnyOrder([reqs[15], reqs[16]], [assertionsReq15, assertionsReq16]), 'assertExitedSwapsEvents(): reqs[15] and reqs[16] did not match what was expected', ); const assertionsReq17 = [ (req) => req.event === MetaMetricsEventName.ExitedSwaps, (req) => Object.keys(req.properties).length === 9, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', (req) => req.properties?.custom_slippage === true, (req) => req.properties?.current_screen === 'awaiting-swap', (req) => req.properties?.is_hardware_wallet === false, (req) => req.properties?.stx_enabled === false, (req) => req.properties?.current_stx_enabled === false, ]; const assertionsReq18 = [ (req) => req.event === MetaMetricsEventName.ExitedSwaps, (req) => Object.keys(req.properties).length === 4, (req) => req.properties?.category === MetaMetricsEventCategory.Swaps, (req) => req.properties?.chain_id === toHex(1337), (req) => req.properties?.environment_type === 'fullscreen', (req) => req.properties?.locale === 'en', ]; assert.ok( assertInAnyOrder([reqs[17], reqs[18]], [assertionsReq17, assertionsReq18]), 'assertExitedSwapsEvents(): reqs[17] and reqs[18] did not match what was expected', ); }