From a70915f1120cf43d9f17bb8dc98514303f846336 Mon Sep 17 00:00:00 2001 From: Daniel <80175477+dan437@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:36:12 +0100 Subject: [PATCH 1/9] fix: Bump smart-transactions-controller to ^16.0.1 (#12847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Release notes for 16.0.1: https://github.com/MetaMask/smart-transactions-controller/releases/tag/v16.0.1 ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2eeeb68a5a5..0c94bf43d3d 100644 --- a/package.json +++ b/package.json @@ -198,7 +198,7 @@ "@metamask/selected-network-controller": "^19.0.0", "@metamask/signature-controller": "^23.1.0", "@metamask/slip44": "^4.1.0", - "@metamask/smart-transactions-controller": "^16.0.0", + "@metamask/smart-transactions-controller": "^16.0.1", "@metamask/snaps-controllers": "^9.15.0", "@metamask/snaps-execution-environments": "^6.10.0", "@metamask/snaps-rpc-methods": "^11.7.0", diff --git a/yarn.lock b/yarn.lock index 0634bd37f44..b2aa515ace3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5211,10 +5211,10 @@ resolved "https://registry.yarnpkg.com/@metamask/slip44/-/slip44-4.1.0.tgz#6f2702de7ba64dad3ab6586ea3ac4e5647804b0a" integrity sha512-RQ2MJO0X3QLnJo0rFlb83h2tNAkqqx/VNOPLc3/S2CvY3/cXy3UAEw/xRM/475BeAAkWI93yiIn/FoGUy3E0Ig== -"@metamask/smart-transactions-controller@^16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@metamask/smart-transactions-controller/-/smart-transactions-controller-16.0.0.tgz#d5f26e3f25945dc695c7e7152f8ab4c9ffa85ac9" - integrity sha512-NfX4yvWlB5MQvkpp+1hsInom1+f0D+xK6b3n/csGJgsDuTWXIS+C3hdYBMS5bpZIrjobFRBG1LH+YQBBsndPHg== +"@metamask/smart-transactions-controller@^16.0.1": + version "16.0.1" + resolved "https://registry.yarnpkg.com/@metamask/smart-transactions-controller/-/smart-transactions-controller-16.0.1.tgz#daf658e98b22f5bf57dd960a6d5d5836b0f40d34" + integrity sha512-ZKvBd0pMiZn6baVDVYmf9NG7HS8dWuyVz8dOVZ8ddYAGY9mBvsf9VldqHmUdAqzGqew89uLpeJbAnCYbmc4nXg== dependencies: "@babel/runtime" "^7.24.1" "@ethereumjs/tx" "^5.2.1" From 4449dd5ed26607edaedb80e985f3c52f30127539 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Wed, 8 Jan 2025 08:48:57 -0700 Subject: [PATCH 2/9] chore: Bump `@metamask/swaps-controller` to 12.0.0 (#12378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This version upgrades `@metamask/swaps-controller` so that it is less reliant on the global network. Specifically: - The `setChainId` and `setProvider` methods have been removed from SwapsController. - The `fetchGasFeeEstimates` and `fetchEstimatedMultiLayerL1Fee` SwapsController constructor options are now expected to take a `networkClientId`. - The SwapsController constructor no longer takes a `chainId` option. - `startFetchAndSetQuotes`, `fetchTokenWithCache`, `fetchTopAssetsWithCache`, and `fetchAggregatorMetadataWithCache` now take a `networkClientId`. - The `fetchParamsMetaData` SwapsController state property now includes a `networkClientId`. - The chain cache in SwapsController state will now automatically be updated whenever the network has changed. See full changelog here: https://github.com/MetaMask/swaps-controller/blob/main/CHANGELOG.md#1200 At the moment, the global network client ID is still passed into SwapsController whenever it is used, but now that can be changed to use a dapp-level network client ID without needing to update SwapsController. ## Related issues Fixes #12470. Also see: - https://github.com/MetaMask/swaps-controller/pull/347 - https://github.com/MetaMask/swaps-controller/issues/204 ## Manual testing steps - Tap on the Swap button - Select a destination token and proceed - You should see new quotes - Ideally, you should be able to complete the swaps flow and create a transaction ## Screenshots/Recordings The Swaps flow should work exactly like it does now. Here is a video demonstrating fetching Swaps quotes: https://github.com/user-attachments/assets/62a490c9-32c4-49a2-886d-136328761658 ## Pre-merge author checklist - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## Pre-merge reviewer checklist - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../UI/AssetOverview/AssetOverview.tsx | 16 +- app/components/UI/Swaps/QuotesView.js | 14 +- app/components/UI/Swaps/QuotesView.test.ts | 10 +- .../__snapshots__/QuotesView.test.ts.snap | 2 +- .../__snapshots__/index.test.tsx.snap | 1719 +++++++++++++++++ .../components/LoadingAnimation/index.js | 12 +- .../LoadingAnimation/index.test.tsx | 24 + app/components/UI/Swaps/index.js | 23 +- app/components/UI/Swaps/index.test.tsx | 48 + app/components/UI/Swaps/utils/index.js | 3 + app/core/Engine/Engine.test.ts | 19 +- app/core/Engine/Engine.ts | 14 +- app/reducers/swaps/index.js | 42 +- app/reducers/swaps/swaps.test.ts | 68 + .../logs/__snapshots__/index.test.ts.snap | 1 + app/util/networks/engineNetworkUtils.ts | 14 +- app/util/test/initial-background-state.json | 3 +- package.json | 2 +- yarn.lock | 8 +- 19 files changed, 1967 insertions(+), 75 deletions(-) create mode 100644 app/components/UI/Swaps/components/LoadingAnimation/__snapshots__/index.test.tsx.snap create mode 100644 app/components/UI/Swaps/components/LoadingAnimation/index.test.tsx create mode 100644 app/components/UI/Swaps/index.test.tsx diff --git a/app/components/UI/AssetOverview/AssetOverview.tsx b/app/components/UI/AssetOverview/AssetOverview.tsx index cbcd738dca6..1ceae3ed380 100644 --- a/app/components/UI/AssetOverview/AssetOverview.tsx +++ b/app/components/UI/AssetOverview/AssetOverview.tsx @@ -11,8 +11,9 @@ import AppConstants from '../../../core/AppConstants'; import Engine from '../../../core/Engine'; import { selectChainId, - selectTicker, selectNativeCurrencyByChainId, + selectSelectedNetworkClientId, + selectTicker, } from '../../../selectors/networkController'; import { selectConversionRate, @@ -58,7 +59,7 @@ import Routes from '../../../constants/navigation/Routes'; import TokenDetails from './TokenDetails'; import { RootState } from '../../../reducers'; import useGoToBridge from '../Bridge/utils/useGoToBridge'; -import SwapsController, { swapsUtils } from '@metamask/swaps-controller'; +import { swapsUtils } from '@metamask/swaps-controller'; import { MetaMetricsEvents } from '../../../core/Analytics'; import { getDecimalChainId, @@ -114,6 +115,7 @@ const AssetOverview: React.FC = ({ ? (asset.chainId as Hex) : selectedChainId; const ticker = isPortfolioViewEnabled() ? nativeCurrency : selectedTicker; + const selectedNetworkClientId = useSelector(selectSelectedNetworkClientId); let currentAddress: Hex; @@ -136,12 +138,12 @@ const AssetOverview: React.FC = ({ const dispatch = useDispatch(); useEffect(() => { - const { SwapsController: SwapsControllerFromEngine } = Engine.context as { - SwapsController: SwapsController; - }; + const { SwapsController } = Engine.context; const fetchTokenWithCache = async () => { try { - await SwapsControllerFromEngine.fetchTokenWithCache(); + await SwapsController.fetchTokenWithCache({ + networkClientId: selectedNetworkClientId, + }); // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { @@ -152,7 +154,7 @@ const AssetOverview: React.FC = ({ } }; fetchTokenWithCache(); - }, []); + }, [selectedNetworkClientId]); const onReceive = () => { navigation.navigate(Routes.QR_TAB_SWITCHER, { diff --git a/app/components/UI/Swaps/QuotesView.js b/app/components/UI/Swaps/QuotesView.js index 9d58c556bec..9273641eabb 100644 --- a/app/components/UI/Swaps/QuotesView.js +++ b/app/components/UI/Swaps/QuotesView.js @@ -8,7 +8,7 @@ import { TouchableOpacity, Linking, } from 'react-native'; -import { connect } from 'react-redux'; +import { connect, useSelector } from 'react-redux'; import IonicIcon from 'react-native-vector-icons/Ionicons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import BigNumber from 'bignumber.js'; @@ -96,7 +96,7 @@ import { import { selectChainId, selectIsEIP1559Network, - selectNetworkClientId, + selectSelectedNetworkClientId, selectTicker, } from '../../../selectors/networkController'; import { @@ -304,6 +304,7 @@ async function resetAndStartPolling({ destinationToken, sourceAmount, walletAddress, + networkClientId, }) { if (!sourceToken || !destinationToken) { return; @@ -316,6 +317,7 @@ async function resetAndStartPolling({ destinationToken, sourceAmount, walletAddress, + networkClientId, }); await SwapsController.stopPollingAndResetState(); await SwapsController.startFetchAndSetQuotes( @@ -762,6 +764,8 @@ function SwapsQuotesView({ } }, [error, navigation]); + const selectedNetworkClientId = useSelector(selectSelectedNetworkClientId); + const handleRetryFetchQuotes = useCallback(() => { if (error?.key === swapsUtils.SwapsError.QUOTES_EXPIRED_ERROR) { navigation.setParams({ leftAction: strings('navigation.back') }); @@ -776,6 +780,7 @@ function SwapsQuotesView({ destinationToken, sourceAmount, walletAddress: selectedAddress, + networkClientId: selectedNetworkClientId, }); } else { navigation.pop(); @@ -788,6 +793,7 @@ function SwapsQuotesView({ sourceAmount, selectedAddress, navigation, + selectedNetworkClientId, ]); const updateSwapsTransactions = useCallback( @@ -1412,6 +1418,7 @@ function SwapsQuotesView({ destinationToken, sourceAmount, walletAddress: selectedAddress, + networkClientId: selectedNetworkClientId, }); return () => { @@ -1425,6 +1432,7 @@ function SwapsQuotesView({ slippage, sourceAmount, sourceToken.address, + selectedNetworkClientId, ]); /** selectedQuote alert effect */ @@ -2403,7 +2411,7 @@ SwapsQuotesView.propTypes = { const mapStateToProps = (state) => ({ accounts: selectAccounts(state), chainId: selectChainId(state), - networkClientId: selectNetworkClientId(state), + networkClientId: selectSelectedNetworkClientId(state), ticker: selectTicker(state), balances: selectContractBalances(state), selectedAddress: selectSelectedInternalAccountFormattedAddress(state), diff --git a/app/components/UI/Swaps/QuotesView.test.ts b/app/components/UI/Swaps/QuotesView.test.ts index eafafb20e98..eac7a44da30 100644 --- a/app/components/UI/Swaps/QuotesView.test.ts +++ b/app/components/UI/Swaps/QuotesView.test.ts @@ -17,6 +17,7 @@ import { } from '../../../util/test/accountsControllerTestUtils'; import { SwapsViewSelectors } from '../../../../e2e/selectors/swaps/SwapsView.selectors'; import Engine from '../../../core/Engine'; +import { RpcEndpointType } from '@metamask/network-controller'; jest.mock('../../../core/Engine', () => ({ context: { @@ -203,7 +204,14 @@ const mockInitialState: DeepPartial = { defaultRpcEndpointIndex: 0, name: 'Ethereum Mainnet', nativeCurrency: 'ETH', - rpcEndpoints: [], + rpcEndpoints: [ + { + name: 'Ethereum Mainnet', + networkClientId: 'mainnet', + type: RpcEndpointType.Infura, + url: 'https://mainnet.infura.io/v3/{infuraProjectId}', + }, + ], }, }, }, diff --git a/app/components/UI/Swaps/__snapshots__/QuotesView.test.ts.snap b/app/components/UI/Swaps/__snapshots__/QuotesView.test.ts.snap index 658bd7b1498..b7df5af1c32 100644 --- a/app/components/UI/Swaps/__snapshots__/QuotesView.test.ts.snap +++ b/app/components/UI/Swaps/__snapshots__/QuotesView.test.ts.snap @@ -197,7 +197,7 @@ exports[`QuotesView should render quote screen 1`] = ` } } > - Private Network + Ethereum Main Network diff --git a/app/components/UI/Swaps/components/LoadingAnimation/__snapshots__/index.test.tsx.snap b/app/components/UI/Swaps/components/LoadingAnimation/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..76d124104e2 --- /dev/null +++ b/app/components/UI/Swaps/components/LoadingAnimation/__snapshots__/index.test.tsx.snap @@ -0,0 +1,1719 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LoadingAnimation renders 1`] = ` + + + + + Starting... + + + + + + + + + + + + --- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ", + } + } + style={ + [ + { + "backgroundColor": "#ffffff", + "flex": 1, + }, + undefined, + ] + } + /> + + + +`; diff --git a/app/components/UI/Swaps/components/LoadingAnimation/index.js b/app/components/UI/Swaps/components/LoadingAnimation/index.js index ec6a1363364..9b282d42517 100644 --- a/app/components/UI/Swaps/components/LoadingAnimation/index.js +++ b/app/components/UI/Swaps/components/LoadingAnimation/index.js @@ -5,8 +5,10 @@ import React, { useRef, useState, } from 'react'; +import { useSelector } from 'react-redux'; import { Animated, View, StyleSheet, Image } from 'react-native'; import PropTypes from 'prop-types'; +import { selectSelectedNetworkClientId } from '../../../../../selectors/networkController'; import Engine from '../../../../../core/Engine'; import Logger from '../../../../../util/Logger'; import Device from '../../../../../util/device'; @@ -129,6 +131,8 @@ function LoadingAnimation({ const [renderLogos, setRenderLogos] = useState(false); const [currentQuoteIndex, setCurrentQuoteIndex] = useState(0); + const selectedNetworkClientId = useSelector(selectSelectedNetworkClientId); + /* References */ const foxRef = useRef(); const foxHeadPan = useRef(new Animated.ValueXY(0, 0)).current; @@ -317,9 +321,11 @@ function LoadingAnimation({ return; } if (!aggregatorMetadata) { + const { SwapsController } = Engine.context; try { - const { SwapsController } = Engine.context; - await SwapsController.fetchAggregatorMetadataWithCache(); + await SwapsController.fetchAggregatorMetadataWithCache({ + networkClientId: selectedNetworkClientId, + }); } catch (error) { Logger.error( error, @@ -337,7 +343,7 @@ function LoadingAnimation({ setShouldStart(true); } })(); - }, [aggregatorMetadata, hasStarted]); + }, [aggregatorMetadata, hasStarted, selectedNetworkClientId]); /* Delay the logos rendering to avoid navigation transition lag */ useEffect(() => { diff --git a/app/components/UI/Swaps/components/LoadingAnimation/index.test.tsx b/app/components/UI/Swaps/components/LoadingAnimation/index.test.tsx new file mode 100644 index 00000000000..d0f72eb0ad1 --- /dev/null +++ b/app/components/UI/Swaps/components/LoadingAnimation/index.test.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import renderWithProvider, { + DeepPartial, +} from '../../../../../util/test/renderWithProvider'; +import LoadingAnimation from './'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; +import { RootState } from '../../../../../reducers'; + +const mockInitialState: DeepPartial = { + engine: { + backgroundState: { + ...backgroundState, + }, + }, +}; + +describe('LoadingAnimation', () => { + it('renders', () => { + const wrapper = renderWithProvider(, { + state: mockInitialState, + }); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/Swaps/index.js b/app/components/UI/Swaps/index.js index 8d189c21d8f..5808688e3eb 100644 --- a/app/components/UI/Swaps/index.js +++ b/app/components/UI/Swaps/index.js @@ -70,6 +70,7 @@ import { selectChainId, selectNetworkConfigurations, selectProviderConfig, + selectSelectedNetworkClientId, } from '../../../selectors/networkController'; import { selectConversionRate, @@ -184,6 +185,7 @@ function SwapsAmountView({ accounts, selectedAddress, chainId, + selectedNetworkClientId, providerConfig, networkConfigurations, balances, @@ -297,8 +299,12 @@ function SwapsAmountView({ (async () => { const { SwapsController } = Engine.context; try { - await SwapsController.fetchAggregatorMetadataWithCache(); - await SwapsController.fetchTopAssetsWithCache(); + await SwapsController.fetchAggregatorMetadataWithCache({ + networkClientId: selectedNetworkClientId, + }); + await SwapsController.fetchTopAssetsWithCache({ + networkClientId: selectedNetworkClientId, + }); } catch (error) { Logger.error( error, @@ -306,7 +312,7 @@ function SwapsAmountView({ ); } })(); - }, []); + }, [selectedNetworkClientId]); useEffect(() => { (async () => { @@ -320,7 +326,9 @@ function SwapsAmountView({ setInitialLoadingTokens(true); } setLoadingTokens(true); - await SwapsController.fetchTokenWithCache(); + await SwapsController.fetchTokenWithCache({ + networkClientId: selectedNetworkClientId, + }); setLoadingTokens(false); setInitialLoadingTokens(false); } catch (error) { @@ -333,7 +341,7 @@ function SwapsAmountView({ setInitialLoadingTokens(false); } })(); - }, [swapsControllerTokens, swapsTokens]); + }, [swapsControllerTokens, swapsTokens, selectedNetworkClientId]); const canSetAnInitialSourceToken = !isSourceSet && @@ -986,6 +994,10 @@ SwapsAmountView.propTypes = { * Chain Id */ chainId: PropTypes.string, + /** + * Selected network client ID + */ + selectedNetworkClientId: PropTypes.string, /** * Network configurations */ @@ -1008,6 +1020,7 @@ const mapStateToProps = (state) => ({ providerConfig: selectProviderConfig(state), networkConfigurations: selectNetworkConfigurations(state), chainId: selectChainId(state), + selectedNetworkClientId: selectSelectedNetworkClientId(state), tokensWithBalance: swapsTokensWithBalanceSelector(state), tokensTopAssets: swapsTopAssetsSelector(state), }); diff --git a/app/components/UI/Swaps/index.test.tsx b/app/components/UI/Swaps/index.test.tsx new file mode 100644 index 00000000000..d7ccfc1b9bd --- /dev/null +++ b/app/components/UI/Swaps/index.test.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import renderWithProvider, { + DeepPartial, +} from '../../../util/test/renderWithProvider'; +import SwapsAmountView from './'; +import { backgroundState } from '../../../util/test/initial-root-state'; +import { RootState } from '../../../reducers'; +import { QuoteViewSelectorIDs } from '../../../../e2e/selectors/swaps/QuoteView.selectors'; + +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useNavigation: () => ({ + setOptions: jest.fn(), + pop: jest.fn(), + navigate: jest.fn(), + }), + useRoute: () => ({}), + }; +}); + +jest.mock('../../../core/Engine', () => ({ + context: { + SwapsController: { + fetchAggregatorMetadataWithCache: jest.fn(), + fetchTopAssetsWithCache: jest.fn(), + fetchTokenWithCache: jest.fn(), + }, + }, +})); + +const mockInitialState: DeepPartial = { + engine: { + backgroundState: { + ...backgroundState, + }, + }, +}; + +describe('SwapsAmountView', () => { + it('renders', async () => { + const { getByTestId } = renderWithProvider(, { + state: mockInitialState, + }); + expect(getByTestId(QuoteViewSelectorIDs.SOURCE_TOKEN)).toBeDefined(); + }); +}); diff --git a/app/components/UI/Swaps/utils/index.js b/app/components/UI/Swaps/utils/index.js index 665562c3f55..d9d41edd236 100644 --- a/app/components/UI/Swaps/utils/index.js +++ b/app/components/UI/Swaps/utils/index.js @@ -118,6 +118,7 @@ export function getQuotesNavigationsParams(route) { * @param {object} options.destinationToken destinationToken object from tokens API * @param {string} sourceAmount Amount in minimal token units of sourceToken to be swapped * @param {string} fromAddress Current address attempting to swap + * @param {string} networkClientId Current network client ID */ export function getFetchParams({ slippage = 1, @@ -125,6 +126,7 @@ export function getFetchParams({ destinationToken, sourceAmount, walletAddress, + networkClientId, }) { return { slippage, @@ -135,6 +137,7 @@ export function getFetchParams({ metaData: { sourceTokenInfo: sourceToken, destinationTokenInfo: destinationToken, + networkClientId, }, }; } diff --git a/app/core/Engine/Engine.test.ts b/app/core/Engine/Engine.test.ts index 5af5d8965c8..a261e484232 100644 --- a/app/core/Engine/Engine.test.ts +++ b/app/core/Engine/Engine.test.ts @@ -8,7 +8,6 @@ import { mockNetworkState } from '../../util/test/network'; import MetaMetrics from '../Analytics/MetaMetrics'; import { store } from '../../store'; import { MetaMetricsEvents } from '../Analytics'; -import { NetworkState } from '@metamask/network-controller'; import { Hex } from '@metamask/utils'; import { TransactionMeta } from '@metamask/transaction-controller'; import { RootState } from '../../reducers'; @@ -132,18 +131,12 @@ describe('Engine', () => { [selectedAddress]: { balance: (ethBalance * 1e18).toString() }, }, }, - NetworkController: { - state: { - ...mockNetworkState({ - chainId: '0x1', - id: '0x1', - nickname: 'mainnet', - ticker: 'ETH', - }), - }, - // TODO(dbrans): Investigate why the shape of the NetworkController state in this - // test is {state: NetworkState} instead of just NetworkState. - } as unknown as NetworkState, + NetworkController: mockNetworkState({ + chainId: '0x1', + id: '0x1', + nickname: 'mainnet', + ticker: 'ETH', + }), CurrencyRateController: { currencyRates: { [ticker]: { diff --git a/app/core/Engine/Engine.ts b/app/core/Engine/Engine.ts index 95c23de7ba2..a586b2c2393 100644 --- a/app/core/Engine/Engine.ts +++ b/app/core/Engine/Engine.ts @@ -1404,16 +1404,12 @@ export class Engine { // allowedActions: [ // 'GasFeeController:getEIP1559GasFeeEstimates', // ], - allowedActions: [ - 'NetworkController:findNetworkClientIdByChainId', - 'NetworkController:getNetworkClientById', - ], - allowedEvents: [], + allowedActions: ['NetworkController:getNetworkClientById'], + allowedEvents: ['NetworkController:networkDidChange'], }), pollCountLimit: AppConstants.SWAPS.POLL_COUNT_LIMIT, // TODO: Remove once GasFeeController exports this action type fetchGasFeeEstimates: () => gasFeeController.fetchGasFeeEstimates(), - // @ts-expect-error TODO: Resolve mismatch between gas fee and swaps controller types fetchEstimatedMultiLayerL1Fee, }), GasFeeController: gasFeeController, @@ -1675,7 +1671,7 @@ export class Engine { } configureControllersOnNetworkChange() { - const { AccountTrackerController, NetworkController, SwapsController } = + const { AccountTrackerController, NetworkController } = this.context; const { provider } = NetworkController.getProviderAndBlockTracker(); @@ -1685,10 +1681,6 @@ export class Engine { } provider.sendAsync = provider.sendAsync.bind(provider); - SwapsController.setProvider(provider, { - chainId: getGlobalChainId(NetworkController), - pollCountLimit: AppConstants.SWAPS.POLL_COUNT_LIMIT, - }); AccountTrackerController.refresh(); } diff --git a/app/reducers/swaps/index.js b/app/reducers/swaps/index.js index 04a57225652..09cfc620d24 100644 --- a/app/reducers/swaps/index.js +++ b/app/reducers/swaps/index.js @@ -2,13 +2,13 @@ import { createSelector } from 'reselect'; import { isMainnetByChainId } from '../../util/networks'; import { safeToChecksumAddress } from '../../util/address'; import { toLowerCaseEquals } from '../../util/general'; -import Engine from '../../core/Engine'; import { lte } from '../../util/lodash'; import { selectChainId } from '../../selectors/networkController'; import { selectAllTokens, selectTokens, } from '../../selectors/tokensController'; +import { selectTokenList } from '../../selectors/tokenListController'; import { selectContractBalances } from '../../selectors/tokenBalancesController'; import { getChainFeatureFlags, getSwapsLiveness } from './utils'; import { allowedTestnetChainIds } from '../../components/UI/Swaps/utils'; @@ -39,15 +39,12 @@ export const setSwapsHasOnboarded = (hasOnboarded) => ({ // * Functions -function addMetadata(chainId, tokens) { +function addMetadata(chainId, tokens, tokenList) { if (!isMainnetByChainId(chainId)) { return tokens; } return tokens.map((token) => { - const tokenMetadata = - Engine.context.TokenListController.state.tokenList[ - safeToChecksumAddress(token.address) - ]; + const tokenMetadata = tokenList[safeToChecksumAddress(token.address)]; if (tokenMetadata) { return { ...token, name: tokenMetadata.name }; } @@ -235,12 +232,13 @@ const swapsControllerAndUserTokensMultichain = createSelector( export const swapsTokensSelector = createSelector( chainIdSelector, swapsControllerAndUserTokens, - (chainId, tokens) => { + selectTokenList, + (chainId, tokens, tokenList) => { if (!tokens) { return []; } - return addMetadata(chainId, tokens); + return addMetadata(chainId, tokens, tokenList); }, ); @@ -253,13 +251,17 @@ const topAssets = (state) => */ export const swapsTokensObjectSelector = createSelector( swapsControllerAndUserTokens, - (tokens) => - tokens?.length > 0 - ? tokens.reduce( - (acc, token) => ({ ...acc, [token.address]: undefined }), - {}, - ) - : {}, + (tokens) => { + if (!tokens || tokens.length === 0) { + return {}; + } + + const result = {}; + for (const token of tokens) { + result[token.address] = undefined; + } + return result; + } ); /** @@ -288,8 +290,9 @@ export const swapsTokensMultiChainObjectSelector = createSelector( export const swapsTokensWithBalanceSelector = createSelector( chainIdSelector, swapsControllerAndUserTokens, + selectTokenList, selectContractBalances, - (chainId, tokens, balances) => { + (chainId, tokens, tokenList, balances) => { if (!tokens) { return []; } @@ -321,7 +324,7 @@ export const swapsTokensWithBalanceSelector = createSelector( 0, Math.max(tokensWithBalance.length, MAX_TOKENS_WITH_BALANCE), ); - return addMetadata(chainId, result); + return addMetadata(chainId, result, tokenList); }, ); @@ -332,8 +335,9 @@ export const swapsTokensWithBalanceSelector = createSelector( export const swapsTopAssetsSelector = createSelector( chainIdSelector, swapsControllerAndUserTokens, + selectTokenList, topAssets, - (chainId, tokens, topAssets) => { + (chainId, tokens, tokenList, topAssets) => { if (!topAssets || !tokens) { return []; } @@ -342,7 +346,7 @@ export const swapsTopAssetsSelector = createSelector( tokens?.find((token) => toLowerCaseEquals(token.address, address)), ) .filter(Boolean); - return addMetadata(chainId, result); + return addMetadata(chainId, result, tokenList); }, ); diff --git a/app/reducers/swaps/swaps.test.ts b/app/reducers/swaps/swaps.test.ts index f8bfdeae4e9..be4c7292d96 100644 --- a/app/reducers/swaps/swaps.test.ts +++ b/app/reducers/swaps/swaps.test.ts @@ -6,8 +6,13 @@ import reducer, { SWAPS_SET_LIVENESS, SWAPS_SET_HAS_ONBOARDED, swapsSmartTxFlagEnabled, + swapsTokensObjectSelector, } from './index'; import { NetworkClientType } from '@metamask/network-controller'; +// eslint-disable-next-line import/no-namespace +import * as tokensControllerSelectors from '../../selectors/tokensController'; + +jest.mock('../../selectors/tokensController'); const emptyAction = { type: null }; @@ -321,6 +326,69 @@ describe('swaps reducer', () => { }); }); + describe('swapsTokensObjectSelector', () => { + it('should return a object that returns an object combining TokensController and SwapsController tokens where each key is an address and each value is undefined', () => { + jest.spyOn(tokensControllerSelectors, 'selectTokens').mockReturnValue([ + { + address: '0x0000000000000000000000000000000000000010', + symbol: 'TOKEN1', + decimals: 1, + aggregators: [], + }, + { + address: '0x0000000000000000000000000000000000000011', + symbol: 'TOKEN2', + decimals: 2, + aggregators: [], + }, + ]); + const state = { + engine: { + backgroundState: { + SwapsController: { + tokens: [ + { + address: '0x0000000000000000000000000000000000000000', + symbol: 'SWAPS-TOKEN1', + decimals: 1, + occurrences: 10, + iconUrl: 'https://some.token.icon.url/1', + }, + { + address: '0x0000000000000000000000000000000000000001', + symbol: 'SWAPS-TOKEN2', + decimals: 2, + occurrences: 20, + iconUrl: 'https://some.token.icon.url/2', + }, + ], + }, + }, + }, + }; + expect(swapsTokensObjectSelector(state)).toStrictEqual({ + '0x0000000000000000000000000000000000000000': undefined, + '0x0000000000000000000000000000000000000001': undefined, + '0x0000000000000000000000000000000000000010': undefined, + '0x0000000000000000000000000000000000000011': undefined, + }); + }); + + it('should return an empty object if there are no Swaps tokens or user tokens', () => { + jest.spyOn(tokensControllerSelectors, 'selectTokens').mockReturnValue([]); + const state = { + engine: { + backgroundState: { + SwapsController: { + tokens: [], + }, + }, + }, + }; + expect(swapsTokensObjectSelector(state)).toStrictEqual({}); + }); + }); + it('should set has onboarded', () => { const initalState = reducer(undefined, emptyAction); // @ts-ignore diff --git a/app/util/logs/__snapshots__/index.test.ts.snap b/app/util/logs/__snapshots__/index.test.ts.snap index 197b81ca95f..6ee35eccd2d 100644 --- a/app/util/logs/__snapshots__/index.test.ts.snap +++ b/app/util/logs/__snapshots__/index.test.ts.snap @@ -305,6 +305,7 @@ exports[`logs :: generateStateLogs generates a valid json export 1`] = ` "decimals": 0, "symbol": "", }, + "networkClientId": "mainnet", "sourceTokenInfo": { "address": "", "decimals": 0, diff --git a/app/util/networks/engineNetworkUtils.ts b/app/util/networks/engineNetworkUtils.ts index e093fd80e5a..65391429710 100644 --- a/app/util/networks/engineNetworkUtils.ts +++ b/app/util/networks/engineNetworkUtils.ts @@ -1,6 +1,7 @@ import Engine from '../../core/Engine'; import { convertHexToDecimal } from '@metamask/controller-utils'; -import { TransactionMeta } from '@metamask/transaction-controller'; +import { NetworkClientId } from '@metamask/network-controller'; +import { TransactionParams } from '@metamask/transaction-controller'; import { isStrictHexString } from '@metamask/utils'; /** @@ -42,14 +43,15 @@ export function toggleUseSafeChainsListValidation(value: boolean): void { */ export const fetchEstimatedMultiLayerL1Fee = async ( _: unknown, - txMeta: TransactionMeta, + { txParams, networkClientId }: { + txParams: TransactionParams, + networkClientId: NetworkClientId, + } ) => { - const chainId = txMeta.chainId; - const layer1GasFee = await Engine.context.TransactionController.getLayer1GasFee({ - transactionParams: txMeta.txParams, - chainId, + transactionParams: txParams, + networkClientId, }); const layer1GasFeeNoPrefix = layer1GasFee?.startsWith('0x') diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json index 0191af34f02..b55fbd59d02 100644 --- a/app/util/test/initial-background-state.json +++ b/app/util/test/initial-background-state.json @@ -270,7 +270,8 @@ "decimals": 0, "address": "", "symbol": "" - } + }, + "networkClientId": "mainnet" }, "topAggSavings": null, "aggregatorMetadata": null, diff --git a/package.json b/package.json index 0c94bf43d3d..fb288afa5cb 100644 --- a/package.json +++ b/package.json @@ -206,7 +206,7 @@ "@metamask/snaps-utils": "^8.6.1", "@metamask/stake-sdk": "^0.3.0", "@metamask/swappable-obj-proxy": "^2.1.0", - "@metamask/swaps-controller": "^11.0.0", + "@metamask/swaps-controller": "^12.0.0", "@metamask/transaction-controller": "^42.0.0", "@metamask/utils": "^10.0.1", "@ngraveio/bc-ur": "^1.1.6", diff --git a/yarn.lock b/yarn.lock index b2aa515ace3..3825d237ad7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5369,10 +5369,10 @@ resolved "https://registry.yarnpkg.com/@metamask/swappable-obj-proxy/-/swappable-obj-proxy-2.2.0.tgz#31b8e0ce57e28bf9847b3b24b214996f7748cc99" integrity sha512-0OjVwQtrrPFRGipw64yDUQA0CUXCK161LWCv2KlTTDZD8BKeWSNb0gbnpDI7HvhsJ0gki5gScZj1hF3ShDnBzA== -"@metamask/swaps-controller@^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@metamask/swaps-controller/-/swaps-controller-11.0.0.tgz#7f77e4c65addb5f03bcad8c651ca6a5d74f94fd5" - integrity sha512-SdFEIiHWRZcHrkWkyhNUO5/Cr1GjOYv7RFS3D0jaCVhgCz/0yvHlgKAq5iOgS87DVHh1Iv/uFrFswwJyRin6gQ== +"@metamask/swaps-controller@^12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@metamask/swaps-controller/-/swaps-controller-12.0.0.tgz#60f256c10906f417225ebc7c99b587cafe6808a6" + integrity sha512-sjpN1iZnKu4BzWeIi7wUiZAa+rX+EA+SCLAmkrBcACrJQhjsKqBA4Wtrw8gI8uZrwwSfTIRFeMB6ksSiHFPzhQ== dependencies: "@ethersproject/contracts" "^5.7.0" "@ethersproject/providers" "^5.7.0" From 8dc1c9fcfbbcfa892deeee6959f7f7088df7e1cf Mon Sep 17 00:00:00 2001 From: Vince Howard Date: Wed, 8 Jan 2025 09:01:56 -0700 Subject: [PATCH 3/9] fix(12849): alert toast blocking tab navigation (#12853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** We have received several complaints about our toast blocking our tab navigation slowing down user experience ## **Related issues** Fixes: [#12849](https://github.com/MetaMask/metamask-mobile/issues/12849) ## **Manual testing steps** 1. On the wallet home page, click on the network selector on the top left 2. Click on any network 3. The toast should pop up ## **Screenshots/Recordings** ### Android | Before | After | |:---:|:---:| |![before](https://github.com/user-attachments/assets/d4d28558-3e94-49ce-8a76-e2a0258d5e21)|![after](https://github.com/user-attachments/assets/e439f189-f4fc-4db9-b167-b17405e35205)| ### iOS | Before | After | |:---:|:---:| |![before](https://github.com/user-attachments/assets/bb0bb5ad-f5e1-4ffa-82bb-80454f2eb09c)|![after](https://github.com/user-attachments/assets/1ec31b56-2e40-4897-abfd-58fb4a573b69)| ### **Before** NA ### **After** NA ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../components/Navigation/TabBar/TabBar.constants.ts | 4 +++- .../components/Navigation/TabBar/TabBar.styles.ts | 4 ++-- app/component-library/components/Toast/Toast.tsx | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/component-library/components/Navigation/TabBar/TabBar.constants.ts b/app/component-library/components/Navigation/TabBar/TabBar.constants.ts index 38e96d6d39e..3b389a7b7b2 100644 --- a/app/component-library/components/Navigation/TabBar/TabBar.constants.ts +++ b/app/component-library/components/Navigation/TabBar/TabBar.constants.ts @@ -2,7 +2,7 @@ // Third party dependencies. import { IconName } from '../../Icons/Icon'; - +import Device from '../../../../util/device'; // Internal dependencies. import { IconByTabBarIconKey, TabBarIconKey } from './TabBar.types'; @@ -13,3 +13,5 @@ export const ICON_BY_TAB_BAR_ICON_KEY: IconByTabBarIconKey = { [TabBarIconKey.Activity]: IconName.Activity, [TabBarIconKey.Setting]: IconName.Setting, }; + +export const TAB_BAR_HEIGHT = Device.isAndroid() ? 62 : 48; diff --git a/app/component-library/components/Navigation/TabBar/TabBar.styles.ts b/app/component-library/components/Navigation/TabBar/TabBar.styles.ts index f74fed8b87c..bbfbad0763b 100644 --- a/app/component-library/components/Navigation/TabBar/TabBar.styles.ts +++ b/app/component-library/components/Navigation/TabBar/TabBar.styles.ts @@ -7,7 +7,7 @@ import Device from '../../../../util/device'; // Internal dependencies. import { TabBarStyleSheetVars } from './TabBar.types'; - +import { TAB_BAR_HEIGHT } from './TabBar.constants'; /** * Style sheet function for TabBar component. * @@ -39,7 +39,7 @@ const styleSheet = (params: { vars: TabBarStyleSheetVars; theme: Theme }) => { base: { flexDirection: 'row', alignItems: 'center', - height: Device.isAndroid() ? 62 : 48, + height: TAB_BAR_HEIGHT, paddingHorizontal: 16, marginBottom: bottomInset, backgroundColor: colors.background.default, diff --git a/app/component-library/components/Toast/Toast.tsx b/app/component-library/components/Toast/Toast.tsx index 170b7543897..8c23d38b46d 100644 --- a/app/component-library/components/Toast/Toast.tsx +++ b/app/component-library/components/Toast/Toast.tsx @@ -40,6 +40,7 @@ import { import styles from './Toast.styles'; import { ToastSelectorsIDs } from '../../../../e2e/selectors/wallet/ToastModal.selectors'; import { ButtonProps } from '../Buttons/Button/Button.types'; +import { TAB_BAR_HEIGHT } from '../Navigation/TabBar/TabBar.constants'; const visibilityDuration = 2750; const animationDuration = 250; @@ -53,7 +54,7 @@ const Toast = forwardRef((_, ref: React.ForwardedRef) => { const { bottom: bottomNotchSpacing } = useSafeAreaInsets(); const translateYProgress = useSharedValue(screenHeight); const animatedStyle = useAnimatedStyle(() => ({ - transform: [{ translateY: translateYProgress.value }], + transform: [{ translateY: translateYProgress.value - TAB_BAR_HEIGHT }], })); const baseStyle: StyleProp>> = useMemo( From e0864ad83afcaf74c535efa4006e773564e31f1c Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Wed, 8 Jan 2025 17:24:06 +0100 Subject: [PATCH 4/9] fix: fix asset options sheet (#12859) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** PR to fix options displayed in Asset Options bottom sheet. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/12660#issuecomment-2576418131 ## **Manual testing steps** 1. Click on a native token on any network 2. Click on asset options button on top right 3. Notice that you should not see "Token Details" option ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/db23c1df-4e2b-46cb-a0f4-ea976005d4ea ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/components/Views/Asset/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/Views/Asset/index.js b/app/components/Views/Asset/index.js index e2a7b59a7b2..531b8c0ec35 100644 --- a/app/components/Views/Asset/index.js +++ b/app/components/Views/Asset/index.js @@ -208,7 +208,7 @@ class Asset extends PureComponent { networkConfigurations, } = this.props; const colors = this.context.colors || mockTheme.colors; - const isNativeToken = route.params.isETH; + const isNativeToken = route.params.isNative ?? route.params.isETH; const isMainnet = isMainnetByChainId(chainId); const blockExplorer = findBlockExplorerForRpc( rpcUrl, From 60667f7f6651b18a6526e97f4615da22f964d202 Mon Sep 17 00:00:00 2001 From: Curtis David Date: Wed, 8 Jan 2025 17:18:22 -0500 Subject: [PATCH 5/9] test: Disable flakey chain permissions e2e (#12906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The purpose of this PR is to disable a couple of chain permission tests that are flaking as of recently. We aim to renable these tests in https://github.com/MetaMask/metamask-mobile/issues/12872 ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../chains/permission-system-add-non-permitted.spec.js | 2 +- .../chains/permission-system-update-permissions.spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/specs/multichain/permissions/chains/permission-system-add-non-permitted.spec.js b/e2e/specs/multichain/permissions/chains/permission-system-add-non-permitted.spec.js index 36af037bba8..a33b641f342 100644 --- a/e2e/specs/multichain/permissions/chains/permission-system-add-non-permitted.spec.js +++ b/e2e/specs/multichain/permissions/chains/permission-system-add-non-permitted.spec.js @@ -102,7 +102,7 @@ describe( ); }); - it('should add network permission when requested', async () => { + it.skip('should add network permission when requested', async () => { await withFixtures( { dapp: true, diff --git a/e2e/specs/multichain/permissions/chains/permission-system-update-permissions.spec.js b/e2e/specs/multichain/permissions/chains/permission-system-update-permissions.spec.js index 53ebea8166b..ebd1c583dd3 100644 --- a/e2e/specs/multichain/permissions/chains/permission-system-update-permissions.spec.js +++ b/e2e/specs/multichain/permissions/chains/permission-system-update-permissions.spec.js @@ -21,7 +21,7 @@ describe(SmokeMultiChainPermissions('Chain Permission Management'), () => { jest.setTimeout(150000); await TestHelpers.reverseServerPort(); }); - it('allows simultaneous granting and revoking of multiple chain permissions', async () => { + it.skip('allows simultaneous granting and revoking of multiple chain permissions', async () => { await withFixtures( { dapp: true, From 8cc7e0f4b90f798b64358adb9c9361f27dc0fe62 Mon Sep 17 00:00:00 2001 From: Matthew Grainger <46547583+Matt561@users.noreply.github.com> Date: Wed, 8 Jan 2025 19:43:16 -0500 Subject: [PATCH 6/9] feat: STAKE-884 add more mobile pooled staking events (#12651) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds more events related to the pooled-staking flows. ### Events Added #### Stake Flow - `Stake Cancel Clicked` - `Stake Gas Cost Impact Warning Triggered` - `Stake Gas Cost Impact Cancel Clicked`, - `Stake Gas Cost Impact Proceeded Clicked` - `Stake Confirmation Back Clicked` - `Stake Transaction Initiated` - `Stake Transaction Approved` - `Stake Transaction Rejected` - `Stake Transaction Confirmed` - `Stake Transaction Failed` - `Stake Transaction Submitted` #### Unstake Flow - `Unstake Cancel Clicked` - `Unstake Confirmation Back Clicked` - `Unstake Transaction Initiated` - `Unstake Transaction Approved` - `Unstake Transaction Rejected` - `Unstake Transaction Confirmed` - `Unstake Transaction Failed` - `Unstake Transaction Submitted` #### Misc - `VISITED_ETH_OVERVIEW_WITH_STAKED_POSITIONS` ## **Related issues** Fixes: [STAKE-884: Add more mobile pooled staking events](https://consensyssoftware.atlassian.net/browse/STAKE-884) ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/components/UI/Navbar/index.js | 42 +++++- .../StakeConfirmationView.tsx | 24 +++- .../StakeInputView/StakeInputView.test.tsx | 2 + .../Views/StakeInputView/StakeInputView.tsx | 57 ++++++-- .../UnstakeConfirmationView.tsx | 24 +++- .../UnstakeInputView/UnstakeInputView.tsx | 29 +++- .../GasImpactModal/GasImpactModal.test.tsx | 2 + .../GasImpactModal/GasImpactModal.types.ts | 2 + .../Stake/components/GasImpactModal/index.tsx | 56 ++++++-- .../Stake/components/LearnMoreModal/index.tsx | 5 +- .../UI/Stake/components/StakeButton/index.tsx | 3 +- .../StakingBalance/StakingBalance.tsx | 34 ++++- .../ClaimBanner/ClaimBanner.tsx | 3 +- .../StakingButtons/StakingButtons.tsx | 5 +- .../StakingBalance/StakingCta/StakingCta.tsx | 5 +- .../FooterButtonGroup/FooterButtonGroup.tsx | 124 ++++++++++++++++-- app/components/UI/Stake/constants/events.ts | 16 +++ .../UI/Stake/hooks/useStakingInput.ts | 1 + .../metaMetrics/tooltipMetaMetricsUtils.ts | 3 +- .../utils/metaMetrics/withMetaMetrics.test.ts | 17 +-- app/core/Analytics/MetaMetrics.events.ts | 74 +++++++++++ 21 files changed, 462 insertions(+), 66 deletions(-) create mode 100644 app/components/UI/Stake/constants/events.ts diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js index 8c8d941b7bb..435f30d5fe3 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -63,6 +63,7 @@ import { toChecksumHexAddress } from '@metamask/controller-utils'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) import { isBtcAccount } from '../../../core/Multichain/utils'; ///: END:ONLY_INCLUDE_IF +import { withMetaMetrics } from '../Stake/utils/metaMetrics/withMetaMetrics'; const trackEvent = (event, params = {}) => { MetaMetrics.getInstance().trackEvent(event); @@ -1949,16 +1950,23 @@ export const getSettingsNavigationOptions = (title, themeColors) => { * @param {String} title - Navbar Title. * @param {NavigationProp} navigation Navigation object returned from useNavigation hook. * @param {ThemeColors} themeColors theme.colors returned from useStyles hook. - * @param {{ backgroundColor?: string, hasCancelButton?: boolean, hasBackButton?: boolean }} [options] - Optional options for navbar. + * @param {{ backgroundColor?: string, hasCancelButton?: boolean, hasBackButton?: boolean }} [navBarOptions] - Optional navbar options. + * @param {{ cancelButtonEvent?: { event: IMetaMetricsEvent, properties: Record }, backButtonEvent?: { event: IMetaMetricsEvent, properties: Record} }} [metricsOptions] - Optional metrics options. * @returns Staking Navbar Component. */ -export function getStakingNavbar(title, navigation, themeColors, options) { - const { hasBackButton = true, hasCancelButton = true } = options ?? {}; +export function getStakingNavbar( + title, + navigation, + themeColors, + navBarOptions, + metricsOptions, +) { + const { hasBackButton = true, hasCancelButton = true } = navBarOptions ?? {}; const innerStyles = StyleSheet.create({ headerStyle: { backgroundColor: - options?.backgroundColor ?? themeColors.background.default, + navBarOptions?.backgroundColor ?? themeColors.background.default, shadowOffset: null, }, headerLeft: { @@ -1978,6 +1986,28 @@ export function getStakingNavbar(title, navigation, themeColors, options) { navigation.goBack(); } + function handleBackPress() { + if (metricsOptions?.backButtonEvent) { + withMetaMetrics(navigationPop, { + event: metricsOptions.backButtonEvent.event, + properties: metricsOptions.backButtonEvent.properties, + }); + } else { + navigationPop(); + } + } + + function handleCancelPress() { + if (metricsOptions?.cancelButtonEvent) { + withMetaMetrics(navigationPop, { + event: metricsOptions.cancelButtonEvent.event, + properties: metricsOptions.cancelButtonEvent.properties, + }); + } else { + navigationPop(); + } + } + return { headerTitle: () => ( @@ -1990,7 +2020,7 @@ export function getStakingNavbar(title, navigation, themeColors, options) { ) : ( @@ -1999,7 +2029,7 @@ export function getStakingNavbar(title, navigation, themeColors, options) { headerRight: () => hasCancelButton ? ( navigation.dangerouslyGetParent()?.pop()} + onPress={handleCancelPress} style={styles.closeButton} > diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx index 0eb0e2ba8de..8a86705d0e8 100644 --- a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx +++ b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx @@ -13,6 +13,8 @@ import { strings } from '../../../../../../locales/i18n'; import { FooterButtonGroupActions } from '../../components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.types'; import UnstakingTimeCard from '../../components/StakingConfirmation/UnstakeTimeCard/UnstakeTimeCard'; import { ScrollView } from 'react-native-gesture-handler'; +import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events'; const MOCK_STAKING_CONTRACT_NAME = 'MM Pooled Staking'; @@ -23,10 +25,24 @@ const StakeConfirmationView = ({ route }: StakeConfirmationViewProps) => { useEffect(() => { navigation.setOptions( - getStakingNavbar(strings('stake.stake'), navigation, theme.colors, { - backgroundColor: theme.colors.background.alternative, - hasCancelButton: false, - }), + getStakingNavbar( + strings('stake.stake'), + navigation, + theme.colors, + { + backgroundColor: theme.colors.background.alternative, + hasCancelButton: false, + }, + { + backButtonEvent: { + event: MetaMetricsEvents.STAKE_CONFIRMATION_BACK_CLICKED, + properties: { + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: EVENT_LOCATIONS.STAKE_CONFIRMATION_VIEW, + }, + }, + }, + ), ); }, [navigation, theme.colors]); diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx index 650343c3958..559c068e427 100644 --- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx +++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx @@ -246,6 +246,8 @@ describe('StakeInputView', () => { annualRewardRate: '2.5%', annualRewardsETH: '0.00938 ETH', annualRewardsFiat: '18.75 USD', + estimatedGasFee: '0.25', + estimatedGasFeePercentage: '66%', }, }); }); diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx index ead9d7016c8..3ae652a718b 100644 --- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx +++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx @@ -20,6 +20,8 @@ import useStakingInputHandlers from '../../hooks/useStakingInput'; import InputDisplay from '../../components/InputDisplay'; import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; import { withMetaMetrics } from '../../utils/metaMetrics/withMetaMetrics'; +import { formatEther } from 'ethers/lib/utils'; +import { EVENT_PROVIDERS, EVENT_LOCATIONS } from '../../constants/events'; const StakeInputView = () => { const title = strings('stake.stake_eth'); @@ -49,6 +51,8 @@ const StakeInputView = () => { handleMax, balanceValue, isHighGasCostImpact, + getDepositTxGasPercentage, + estimatedGasFeeWei, isLoadingStakingGasFee, } = useStakingInputHandlers(); @@ -60,6 +64,21 @@ const StakeInputView = () => { const handleStakePress = useCallback(() => { if (isHighGasCostImpact()) { + trackEvent( + createEventBuilder( + MetaMetricsEvents.STAKE_GAS_COST_IMPACT_WARNING_TRIGGERED, + ) + .addProperties({ + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: EVENT_LOCATIONS.STAKE_INPUT_VIEW, + tokens_to_stake_native_value: amountEth, + tokens_to_stake_usd_value: fiatAmount, + estimated_gas_fee: formatEther(estimatedGasFeeWei.toString()), + estimated_gas_percentage_of_deposit: `${getDepositTxGasPercentage()}%`, + }) + .build(), + ); + navigation.navigate('StakeModals', { screen: Routes.STAKING.MODALS.GAS_IMPACT, params: { @@ -68,6 +87,8 @@ const StakeInputView = () => { annualRewardsETH, annualRewardsFiat, annualRewardRate, + estimatedGasFee: formatEther(estimatedGasFeeWei.toString()), + estimatedGasFeePercentage: `${getDepositTxGasPercentage()}%`, }, }); return; @@ -86,7 +107,7 @@ const StakeInputView = () => { trackEvent( createEventBuilder(MetaMetricsEvents.REVIEW_STAKE_BUTTON_CLICKED) .addProperties({ - selected_provider: 'consensys', + selected_provider: EVENT_PROVIDERS.CONSENSYS, tokens_to_stake_native_value: amountEth, tokens_to_stake_usd_value: fiatAmount, }) @@ -103,6 +124,8 @@ const StakeInputView = () => { trackEvent, createEventBuilder, amountEth, + estimatedGasFeeWei, + getDepositTxGasPercentage, ]); const handleMaxButtonPress = () => { @@ -124,9 +147,23 @@ const StakeInputView = () => { useEffect(() => { navigation.setOptions( - getStakingNavbar(title, navigation, theme.colors, { - hasBackButton: false, - }), + getStakingNavbar( + title, + navigation, + theme.colors, + { + hasBackButton: false, + }, + { + cancelButtonEvent: { + event: MetaMetricsEvents.STAKE_CANCEL_CLICKED, + properties: { + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: EVENT_LOCATIONS.STAKE_INPUT_VIEW, + }, + }, + }, + ), ); }, [navigation, theme.colors, title]); @@ -148,9 +185,9 @@ const StakeInputView = () => { handleCurrencySwitch={withMetaMetrics(handleCurrencySwitch, { event: MetaMetricsEvents.STAKE_INPUT_CURRENCY_SWITCH_CLICKED, properties: { - selected_provider: 'consensys', + selected_provider: EVENT_PROVIDERS.CONSENSYS, text: 'Currency Switch Trigger', - location: 'Stake Input View', + location: EVENT_LOCATIONS.STAKE_INPUT_VIEW, // We want to track the currency switching to. Not the current currency. currency_type: isEth ? 'fiat' : 'native', }, @@ -163,9 +200,9 @@ const StakeInputView = () => { onIconPress={withMetaMetrics(navigateToLearnMoreModal, { event: MetaMetricsEvents.TOOLTIP_OPENED, properties: { - selected_provider: 'consensys', + selected_provider: EVENT_PROVIDERS.CONSENSYS, text: 'Tooltip Opened', - location: 'Stake Input View', + location: EVENT_LOCATIONS.STAKE_INPUT_VIEW, tooltip_name: 'MetaMask Pool Estimated Rewards', }, })} @@ -178,7 +215,7 @@ const StakeInputView = () => { withMetaMetrics(handleQuickAmountPress, { event: MetaMetricsEvents.STAKE_INPUT_QUICK_AMOUNT_CLICKED, properties: { - location: 'StakeInputView', + location: EVENT_LOCATIONS.STAKE_INPUT_VIEW, amount: value, // onMaxPress is called instead when it's defined and the max is clicked. is_max: false, @@ -189,7 +226,7 @@ const StakeInputView = () => { onMaxPress={withMetaMetrics(handleMaxButtonPress, { event: MetaMetricsEvents.STAKE_INPUT_QUICK_AMOUNT_CLICKED, properties: { - location: 'StakeInputView', + location: EVENT_LOCATIONS.STAKE_INPUT_VIEW, is_max: true, mode: isEth ? 'native' : 'fiat', }, diff --git a/app/components/UI/Stake/Views/UnstakeConfirmationView/UnstakeConfirmationView.tsx b/app/components/UI/Stake/Views/UnstakeConfirmationView/UnstakeConfirmationView.tsx index 1a78eab35e5..4c3a94084a3 100644 --- a/app/components/UI/Stake/Views/UnstakeConfirmationView/UnstakeConfirmationView.tsx +++ b/app/components/UI/Stake/Views/UnstakeConfirmationView/UnstakeConfirmationView.tsx @@ -11,6 +11,8 @@ import TokenValueStack from '../../components/StakingConfirmation/TokenValueStac import AccountCard from '../../components/StakingConfirmation/AccountCard/AccountCard'; import ConfirmationFooter from '../../components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter'; import { FooterButtonGroupActions } from '../../components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.types'; +import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events'; const MOCK_STAKING_CONTRACT_NAME = 'MM Pooled Staking'; @@ -21,10 +23,24 @@ const UnstakeConfirmationView = ({ route }: UnstakeConfirmationViewProps) => { useEffect(() => { navigation.setOptions( - getStakingNavbar(strings('stake.unstake'), navigation, theme.colors, { - backgroundColor: theme.colors.background.alternative, - hasCancelButton: false, - }), + getStakingNavbar( + strings('stake.unstake'), + navigation, + theme.colors, + { + backgroundColor: theme.colors.background.alternative, + hasCancelButton: false, + }, + { + backButtonEvent: { + event: MetaMetricsEvents.UNSTAKE_CONFIRMATION_BACK_CLICKED, + properties: { + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: EVENT_LOCATIONS.UNSTAKE_CONFIRMATION_VIEW, + }, + }, + }, + ), ); }, [navigation, theme.colors]); diff --git a/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx index b27bbbe07e0..1786301fb0e 100644 --- a/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx +++ b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx @@ -20,6 +20,7 @@ import Routes from '../../../../../constants/navigation/Routes'; import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; import useUnstakingInputHandlers from '../../hooks/useUnstakingInput'; import { withMetaMetrics } from '../../utils/metaMetrics/withMetaMetrics'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events'; const UnstakeInputView = () => { const title = strings('stake.unstake_eth'); @@ -54,9 +55,23 @@ const UnstakeInputView = () => { useEffect(() => { navigation.setOptions( - getStakingNavbar(title, navigation, theme.colors, { - hasBackButton: false, - }), + getStakingNavbar( + title, + navigation, + theme.colors, + { + hasBackButton: false, + }, + { + cancelButtonEvent: { + event: MetaMetricsEvents.UNSTAKE_CANCEL_CLICKED, + properties: { + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: EVENT_LOCATIONS.UNSTAKE_INPUT_VIEW, + }, + }, + }, + ), ); }, [navigation, theme.colors, title]); @@ -71,7 +86,7 @@ const UnstakeInputView = () => { trackEvent( createEventBuilder(MetaMetricsEvents.REVIEW_UNSTAKE_BUTTON_CLICKED) .addProperties({ - selected_provider: 'consensys', + selected_provider: EVENT_PROVIDERS.CONSENSYS, tokens_to_stake_native_value: amountEth, tokens_to_stake_usd_value: fiatAmount, }) @@ -100,9 +115,9 @@ const UnstakeInputView = () => { handleCurrencySwitch={withMetaMetrics(handleCurrencySwitch, { event: MetaMetricsEvents.UNSTAKE_INPUT_CURRENCY_SWITCH_CLICKED, properties: { - selected_provider: 'consensys', + selected_provider: EVENT_PROVIDERS.CONSENSYS, text: 'Currency Switch Trigger', - location: 'Unstake Input View', + location: EVENT_LOCATIONS.UNSTAKE_INPUT_VIEW, // We want to track the currency switching to. Not the current currency. currency_type: isEth ? 'fiat' : 'native', }, @@ -116,7 +131,7 @@ const UnstakeInputView = () => { withMetaMetrics(handleQuickAmountPress, { event: MetaMetricsEvents.UNSTAKE_INPUT_QUICK_AMOUNT_CLICKED, properties: { - location: 'UnstakeInputView', + location: EVENT_LOCATIONS.UNSTAKE_INPUT_VIEW, amount: value, is_max: value === 1, mode: isEth ? 'native' : 'fiat', diff --git a/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx index 7018981373c..f6eca2e2f22 100644 --- a/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx +++ b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx @@ -29,6 +29,8 @@ const props: GasImpactModalProps = { annualRewardRate: '2.5%', annualRewardsETH: '2.5 ETH', annualRewardsFiat: '$5000', + estimatedGasFee: '0.009171428571428572', + estimatedGasFeePercentage: '35%', }, name: 'params', }, diff --git a/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts index a00204cfbee..7a1d9441615 100644 --- a/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts +++ b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts @@ -6,6 +6,8 @@ interface GasImpactModalRouteParams { annualRewardsETH: string; annualRewardsFiat: string; annualRewardRate: string; + estimatedGasFee: string; + estimatedGasFeePercentage: string; } export interface GasImpactModalProps { diff --git a/app/components/UI/Stake/components/GasImpactModal/index.tsx b/app/components/UI/Stake/components/GasImpactModal/index.tsx index 4e348f75426..dabbaca7c5d 100644 --- a/app/components/UI/Stake/components/GasImpactModal/index.tsx +++ b/app/components/UI/Stake/components/GasImpactModal/index.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useCallback, useRef } from 'react'; import BottomSheet, { BottomSheetRef, } from '../../../../../component-library/components/BottomSheets/BottomSheet'; @@ -22,27 +22,65 @@ import { useNavigation } from '@react-navigation/native'; import Routes from '../../../../../constants/navigation/Routes'; import { GasImpactModalProps } from './GasImpactModal.types'; import { strings } from '../../../../../../locales/i18n'; +import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; +import { formatEther } from 'ethers/lib/utils'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events'; const GasImpactModal = ({ route }: GasImpactModalProps) => { const { styles } = useStyles(styleSheet, {}); const { navigate } = useNavigation(); + const { trackEvent, createEventBuilder } = useMetrics(); + const sheetRef = useRef(null); + const { + amountWei, + annualRewardRate, + annualRewardsFiat, + annualRewardsETH, + amountFiat, + estimatedGasFee, + estimatedGasFeePercentage, + } = route.params; + + const metricsEvent = useCallback( + ( + eventName: + | typeof MetaMetricsEvents.STAKE_GAS_COST_IMPACT_CANCEL_CLICKED + | typeof MetaMetricsEvents.STAKE_GAS_COST_IMPACT_PROCEEDED_CLICKED, + ) => { + trackEvent( + createEventBuilder(eventName) + .addProperties({ + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: EVENT_LOCATIONS.GAS_IMPACT_MODAL, + tokens_to_stake_native_value: formatEther(amountWei), + tokens_to_stake_usd_value: amountFiat, + estimated_gas_fee: estimatedGasFee, + estimated_gas_percentage_of_deposit: estimatedGasFeePercentage, + }) + .build(), + ); + }, + [ + amountFiat, + amountWei, + createEventBuilder, + estimatedGasFee, + estimatedGasFeePercentage, + trackEvent, + ], + ); + const handleClose = () => { + metricsEvent(MetaMetricsEvents.STAKE_GAS_COST_IMPACT_CANCEL_CLICKED); sheetRef.current?.onCloseBottomSheet(); }; const handleNavigateToStakeReviewScreen = () => { - const { - amountWei, - annualRewardRate, - annualRewardsFiat, - annualRewardsETH, - amountFiat, - } = route.params; - + metricsEvent(MetaMetricsEvents.STAKE_GAS_COST_IMPACT_PROCEEDED_CLICKED); navigate('StakeScreens', { screen: Routes.STAKING.STAKE_CONFIRMATION, params: { diff --git a/app/components/UI/Stake/components/LearnMoreModal/index.tsx b/app/components/UI/Stake/components/LearnMoreModal/index.tsx index eb321f8d238..5d6d4b6677d 100644 --- a/app/components/UI/Stake/components/LearnMoreModal/index.tsx +++ b/app/components/UI/Stake/components/LearnMoreModal/index.tsx @@ -19,6 +19,7 @@ import { POOLED_STAKING_FAQ_URL } from '../../constants'; import createLearnMoreModalStyles from './LearnMoreModal.styles'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { withMetaMetrics } from '../../utils/metaMetrics/withMetaMetrics'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events'; const styles = createLearnMoreModalStyles(); @@ -99,9 +100,9 @@ const LearnMoreModal = () => { onPress={withMetaMetrics(handleLearnMoreBrowserRedirect, { event: MetaMetricsEvents.STAKE_LEARN_MORE_CLICKED, properties: { - selected_provider: 'consensys', + selected_provider: EVENT_PROVIDERS.CONSENSYS, text: 'Learn More', - location: 'Learn More Modal', + location: EVENT_LOCATIONS.LEARN_MORE_MODAL, }, })} label={strings('stake.learn_more')} diff --git a/app/components/UI/Stake/components/StakeButton/index.tsx b/app/components/UI/Stake/components/StakeButton/index.tsx index ca578bef3bd..f197ed15dfc 100644 --- a/app/components/UI/Stake/components/StakeButton/index.tsx +++ b/app/components/UI/Stake/components/StakeButton/index.tsx @@ -25,6 +25,7 @@ import { strings } from '../../../../../../locales/i18n'; import { RootState } from '../../../../../reducers'; import useStakingEligibility from '../../hooks/useStakingEligibility'; import { StakeSDKProvider } from '../../sdk/stakeSdkProvider'; +import { EVENT_LOCATIONS } from '../../constants/events'; interface StakeButtonProps { asset: TokenI; @@ -69,7 +70,7 @@ const StakeButtonContent = ({ asset }: StakeButtonProps) => { createEventBuilder(MetaMetricsEvents.STAKE_BUTTON_CLICKED) .addProperties({ chain_id: getDecimalChainId(chainId), - location: 'Home Screen', + location: EVENT_LOCATIONS.HOME_SCREEN, text: 'Stake', token_symbol: asset.symbol, url: AppConstants.STAKE.URL, diff --git a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx index 1d41646bc3c..ce12aec48e7 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import Badge, { BadgeVariant, } from '../../../../../component-library/components/Badges/Badge'; @@ -44,6 +44,8 @@ import useBalance from '../../hooks/useBalance'; import { NetworkBadgeSource } from '../../../AssetOverview/Balance/Balance'; import { selectChainId } from '../../../../../selectors/networkController'; import SkeletonPlaceholder from 'react-native-skeleton-placeholder'; +import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events'; export interface StakingBalanceProps { asset: TokenI; @@ -51,6 +53,12 @@ export interface StakingBalanceProps { const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { const { styles } = useStyles(styleSheet, {}); + + const [ + hasSentViewingStakingRewardsMetric, + setHasSentViewingStakingRewardsMetric, + ] = useState(false); + const chainId = useSelector(selectChainId); const networkName = useSelector(selectNetworkName); @@ -58,6 +66,8 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { const { isStakingSupportedChain } = useStakingChain(); + const { trackEvent, createEventBuilder } = useMetrics(); + const { pooledStakesData, exchangeRate, @@ -92,6 +102,28 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { const hasClaimableEth = !!Number(claimableEth); + useEffect(() => { + if (hasStakedPositions && !hasSentViewingStakingRewardsMetric) { + trackEvent( + createEventBuilder( + MetaMetricsEvents.VISITED_ETH_OVERVIEW_WITH_STAKED_POSITIONS, + ) + .addProperties({ + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: EVENT_LOCATIONS.STAKING_BALANCE, + }) + .build(), + ); + + setHasSentViewingStakingRewardsMetric(true); + } + }, [ + createEventBuilder, + hasSentViewingStakingRewardsMetric, + hasStakedPositions, + trackEvent, + ]); + if (!isStakingSupportedChain) { return <>; } diff --git a/app/components/UI/Stake/components/StakingBalance/StakingBanners/ClaimBanner/ClaimBanner.tsx b/app/components/UI/Stake/components/StakingBalance/StakingBanners/ClaimBanner/ClaimBanner.tsx index 3a13ee588ca..e99f93880c0 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingBanners/ClaimBanner/ClaimBanner.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingBanners/ClaimBanner/ClaimBanner.tsx @@ -23,6 +23,7 @@ import { MetaMetricsEvents, useMetrics, } from '../../../../../../hooks/useMetrics'; +import { EVENT_LOCATIONS } from '../../../../constants/events'; type StakeBannerProps = Pick & { claimableAmount: string; @@ -48,7 +49,7 @@ const ClaimBanner = ({ claimableAmount, style }: StakeBannerProps) => { trackEvent( createEventBuilder(MetaMetricsEvents.STAKE_CLAIM_BUTTON_CLICKED) .addProperties({ - location: 'Token Details', + location: EVENT_LOCATIONS.TOKEN_DETAILS, }) .build(), ); diff --git a/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx b/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx index e6245f6506a..a87ea82fc06 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx @@ -11,6 +11,7 @@ import Routes from '../../../../../../constants/navigation/Routes'; import { useMetrics, MetaMetricsEvents } from '../../../../../hooks/useMetrics'; import { useSelector } from 'react-redux'; import { selectChainId } from '../../../../../../selectors/networkController'; +import { EVENT_LOCATIONS } from '../../../constants/events'; interface StakingButtonsProps extends Pick { hasStakedPositions: boolean; @@ -34,7 +35,7 @@ const StakingButtons = ({ trackEvent( createEventBuilder(MetaMetricsEvents.STAKE_WITHDRAW_BUTTON_CLICKED) .addProperties({ - location: 'Token Details', + location: EVENT_LOCATIONS.TOKEN_DETAILS, text: 'Unstake', token_symbol: 'ETH', chain_id: chainId, @@ -48,7 +49,7 @@ const StakingButtons = ({ trackEvent( createEventBuilder(MetaMetricsEvents.STAKE_BUTTON_CLICKED) .addProperties({ - location: 'Token Details', + location: EVENT_LOCATIONS.TOKEN_DETAILS, text: 'Stake', token_symbol: 'ETH', chain_id: chainId, diff --git a/app/components/UI/Stake/components/StakingBalance/StakingCta/StakingCta.tsx b/app/components/UI/Stake/components/StakingBalance/StakingCta/StakingCta.tsx index a6e2f4efca0..d292873ef5e 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingCta/StakingCta.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingCta/StakingCta.tsx @@ -13,6 +13,7 @@ import { strings } from '../../../../../../../locales/i18n'; import { useNavigation } from '@react-navigation/native'; import Routes from '../../../../../../constants/navigation/Routes'; import { MetaMetricsEvents, useMetrics } from '../../../../../hooks/useMetrics'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../../constants/events'; interface StakingCtaProps extends Pick { estimatedRewardRate: string; @@ -30,9 +31,9 @@ const StakingCta = ({ estimatedRewardRate, style }: StakingCtaProps) => { trackEvent( createEventBuilder(MetaMetricsEvents.STAKE_LEARN_MORE_CLICKED) .addProperties({ - selected_provider: 'consensys', + selected_provider: EVENT_PROVIDERS.CONSENSYS, text: 'Learn More', - location: 'Token Details', + location: EVENT_LOCATIONS.TOKEN_DETAILS, }) .build(), ); diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx index c60c393fb18..98c3e8fc987 100644 --- a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx +++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useNavigation } from '@react-navigation/native'; import { View } from 'react-native'; import { strings } from '../../../../../../../../locales/i18n'; @@ -24,6 +24,36 @@ import { import Routes from '../../../../../../../constants/navigation/Routes'; import usePoolStakedUnstake from '../../../../hooks/usePoolStakedUnstake'; import usePooledStakes from '../../../../hooks/usePooledStakes'; +import { + MetaMetricsEvents, + useMetrics, +} from '../../../../../../hooks/useMetrics'; +import { IMetaMetricsEvent } from '../../../../../../../core/Analytics'; +import { formatEther } from 'ethers/lib/utils'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../../../constants/events'; + +const STAKING_TX_METRIC_EVENTS: Record< + FooterButtonGroupActions, + Record< + 'APPROVED' | 'REJECTED' | 'CONFIRMED' | 'FAILED' | 'SUBMITTED', + IMetaMetricsEvent + > +> = { + STAKE: { + APPROVED: MetaMetricsEvents.STAKE_TRANSACTION_APPROVED, + REJECTED: MetaMetricsEvents.STAKE_TRANSACTION_REJECTED, + CONFIRMED: MetaMetricsEvents.STAKE_TRANSACTION_CONFIRMED, + FAILED: MetaMetricsEvents.STAKE_TRANSACTION_FAILED, + SUBMITTED: MetaMetricsEvents.STAKE_TRANSACTION_SUBMITTED, + }, + UNSTAKE: { + APPROVED: MetaMetricsEvents.UNSTAKE_TRANSACTION_APPROVED, + REJECTED: MetaMetricsEvents.UNSTAKE_TRANSACTION_REJECTED, + CONFIRMED: MetaMetricsEvents.UNSTAKE_TRANSACTION_CONFIRMED, + FAILED: MetaMetricsEvents.UNSTAKE_TRANSACTION_FAILED, + SUBMITTED: MetaMetricsEvents.UNSTAKE_TRANSACTION_SUBMITTED, + }, +}; const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { const { styles } = useStyles(styleSheet, {}); @@ -31,6 +61,8 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { const navigation = useNavigation(); const { navigate } = navigation; + const { trackEvent, createEventBuilder } = useMetrics(); + const activeAccount = useSelector(selectSelectedInternalAccount); const { attemptDepositTransaction } = usePoolStakedDeposit(); @@ -40,13 +72,49 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { const [didSubmitTransaction, setDidSubmitTransaction] = useState(false); + const isStaking = useMemo( + () => action === FooterButtonGroupActions.STAKE, + [action], + ); + + const submitTxMetaMetric = useCallback( + (txEventName: IMetaMetricsEvent) => { + const { STAKE_CONFIRMATION_VIEW, UNSTAKE_CONFIRMATION_VIEW } = + EVENT_LOCATIONS; + + const location = isStaking + ? STAKE_CONFIRMATION_VIEW + : UNSTAKE_CONFIRMATION_VIEW; + + return trackEvent( + createEventBuilder(txEventName) + .addProperties({ + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location, + transaction_amount_eth: formatEther(valueWei), + }) + .build(), + ); + }, + [createEventBuilder, isStaking, trackEvent, valueWei], + ); + const listenForTransactionEvents = useCallback( (transactionId?: string) => { if (!transactionId) return; + Engine.controllerMessenger.subscribeOnceIf( + 'TransactionController:transactionApproved', + () => { + submitTxMetaMetric(STAKING_TX_METRIC_EVENTS[action].APPROVED); + }, + ({ transactionMeta }) => transactionMeta.id === transactionId, + ); + Engine.controllerMessenger.subscribeOnceIf( 'TransactionController:transactionSubmitted', () => { + submitTxMetaMetric(STAKING_TX_METRIC_EVENTS[action].SUBMITTED); setDidSubmitTransaction(false); navigate(Routes.TRANSACTIONS_VIEW); }, @@ -56,6 +124,7 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { Engine.controllerMessenger.subscribeOnceIf( 'TransactionController:transactionFailed', () => { + submitTxMetaMetric(STAKING_TX_METRIC_EVENTS[action].FAILED); setDidSubmitTransaction(false); }, ({ transactionMeta }) => transactionMeta.id === transactionId, @@ -64,6 +133,7 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { Engine.controllerMessenger.subscribeOnceIf( 'TransactionController:transactionRejected', () => { + submitTxMetaMetric(STAKING_TX_METRIC_EVENTS[action].REJECTED); setDidSubmitTransaction(false); }, ({ transactionMeta }) => transactionMeta.id === transactionId, @@ -72,12 +142,13 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { Engine.controllerMessenger.subscribeOnceIf( 'TransactionController:transactionConfirmed', () => { + submitTxMetaMetric(STAKING_TX_METRIC_EVENTS[action].CONFIRMED); refreshPooledStakes(); }, (transactionMeta) => transactionMeta.id === transactionId, ); }, - [navigate, refreshPooledStakes], + [action, navigate, refreshPooledStakes, submitTxMetaMetric], ); const handleConfirmation = async () => { @@ -86,17 +157,36 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { setDidSubmitTransaction(true); + const metricsEvent = { + name: isStaking + ? MetaMetricsEvents.STAKE_TRANSACTION_INITIATED + : MetaMetricsEvents.UNSTAKE_TRANSACTION_INITIATED, + location: isStaking + ? 'StakeConfirmationView' + : 'UnstakeConfirmationView', + }; + + trackEvent( + createEventBuilder(metricsEvent.name) + .addProperties({ + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: metricsEvent.location, + transaction_amount_eth: formatEther(valueWei), + }) + .build(), + ); + let transactionId: string | undefined; - if (action === FooterButtonGroupActions.STAKE) { + if (isStaking) { const txRes = await attemptDepositTransaction( valueWei, activeAccount.address, ); transactionId = txRes?.transactionMeta?.id; } - - if (action === FooterButtonGroupActions.UNSTAKE) { + // Unstaking + else { const txRes = await attemptUnstakeTransaction( valueWei, activeAccount.address, @@ -110,6 +200,26 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { } }; + const handleCancelPress = () => { + const metricsEvent = { + name: isStaking + ? MetaMetricsEvents.STAKE_CANCEL_CLICKED + : MetaMetricsEvents.UNSTAKE_CANCEL_CLICKED, + location: isStaking ? 'StakeConfirmationView' : 'UnstakeConfirmationView', + }; + + trackEvent( + createEventBuilder(metricsEvent.name) + .addProperties({ + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: metricsEvent.location, + }) + .build(), + ); + + navigation.goBack(); + }; + return (