diff --git a/.yarn/patches/@metamask-transaction-controller-npm-62.9.0-5c8f871530.patch b/.yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch similarity index 80% rename from .yarn/patches/@metamask-transaction-controller-npm-62.9.0-5c8f871530.patch rename to .yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch index 574f1b30b9b..df945d9e622 100644 --- a/.yarn/patches/@metamask-transaction-controller-npm-62.9.0-5c8f871530.patch +++ b/.yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch @@ -1,37 +1,37 @@ diff --git a/dist/TransactionController.cjs b/dist/TransactionController.cjs -index bcccfac963c3b13e121085436dbcdb6dc4d1b41e..27860c2c785fb14dba09cb2c0b40247cdd78f79a 100644 +index 88203b572578477512d6c474149e05c3f0275f48..ec2f692a3fdf695c674f9489d140847d8c6fcedb 100644 --- a/dist/TransactionController.cjs +++ b/dist/TransactionController.cjs -@@ -96,6 +96,12 @@ const metadata = { +@@ -97,6 +97,12 @@ const metadata = { includeInDebugSnapshot: false, usedInUi: false, }, -+ swapsTransactions: { ++ swapsTransactions: { + includeInStateLogs: true, + persist: true, + includeInDebugSnapshot: false, + usedInUi: true, + }, }; - const SUBMIT_HISTORY_LIMIT = 100; /** + * Multiplier used to determine a transaction's increased gas fee during cancellation diff --git a/dist/TransactionController.mjs b/dist/TransactionController.mjs -index 854d4698c1821578ec36e6ba2a3a5efb475367d3..6a3799d47e1f5b6ea070063e8efe37ae70d84a2d 100644 +index 0ea5bf2c4f54d770e5891369f069af3e294db48d..984dba93e8d1f5682f4954db8492e101fa29dbf5 100644 --- a/dist/TransactionController.mjs +++ b/dist/TransactionController.mjs -@@ -98,6 +98,12 @@ const metadata = { +@@ -99,6 +99,12 @@ const metadata = { includeInDebugSnapshot: false, usedInUi: false, }, -+ swapsTransactions: { ++ swapsTransactions: { + includeInStateLogs: true, + persist: true, + includeInDebugSnapshot: false, + usedInUi: true, + }, }; - const SUBMIT_HISTORY_LIMIT = 100; /** + * Multiplier used to determine a transaction's increased gas fee during cancellation diff --git a/dist/types.cjs b/dist/types.cjs index 0a83050978f13f7ed4e76ccdc250826e0ed0c6ab..2870d525100e99824b3705146a25933992df4d02 100644 --- a/dist/types.cjs @@ -48,7 +48,7 @@ index 0a83050978f13f7ed4e76ccdc250826e0ed0c6ab..2870d525100e99824b3705146a259339 * A transaction for personal sign. */ diff --git a/dist/types.d.cts b/dist/types.d.cts -index 46c36be04420b4df5ba297bbe594a63a931552a3..6170d8b5319b61cc9f9d1513ef7e3b61b5739dc4 100644 +index 2f1660ce03f2aea99a9202edb06ffb0e21072d0c..92fbc94f1490bb4867b1de90494ce028aa094ea5 100644 --- a/dist/types.d.cts +++ b/dist/types.d.cts @@ -631,6 +631,10 @@ export declare enum TransactionType { @@ -63,7 +63,7 @@ index 46c36be04420b4df5ba297bbe594a63a931552a3..6170d8b5319b61cc9f9d1513ef7e3b61 * A transaction for personal sign. */ diff --git a/dist/types.d.mts b/dist/types.d.mts -index adc6362d66c8875ed2bfe802c018bf6546dd7ba9..9239433863008b6a2d8b39cea1044cfcb23da0e3 100644 +index 8ca4cbfc5f93ae0366ec8a3a88cead6e5c5e7d47..cd3e8b221d0b083d3d27c78b9174f9eaa5e8b359 100644 --- a/dist/types.d.mts +++ b/dist/types.d.mts @@ -631,6 +631,10 @@ export declare enum TransactionType { diff --git a/CHANGELOG.md b/CHANGELOG.md index 679adc91354..c7800fee170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,299 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.63.1] + +### Fixed + +- fix: Android ANR bug (#25596) +- fix(analytics): cp-7.63.1 correct capitalization in Deep link event name (#25599) +- feat(perps): sdk reconnect on native socket event (#25022) (#25573) + +## [7.63.0] + +### Added + +- Added "Claim bonus" CTA on token list items for tokens with claimable mUSD bonuses, with automatic scroll to claim section on (#24982) + asset details page +- Removed unnecessary security alerts when revoking token permissions from malicious addresses (#24592) +- Update MegaETH RPC (Infura) and explorer (Blockscout) URLs (#24939) + add migration (113) for MegaETH RPC (Infura) and + explorer (Blockscout) URLs +- Added ability to view card details (card number, expiration, and CVV) as a secure image. Improved card onboarding (#25021) + experience on Android with better keyboard handling. Added card + provisioning status message. +- Added new `network-fee-row` component and conditionally render it for mUSD conversion transactions. (#24943) +- Added smooth slide animation when selecting regions with states in buy/sell flows (#24911) +- Upgrade smart-transactions-controller and replace the legacy smart transactions swaps flags with smart transactions flags from (#23847) + remote config API. +- Redesigned Card Home screen with improved balance display layout and simplified KYC verification flow (#24954) +- Added deeplink support to navigate directly to the Trending/Explore screen (#24952) +- Added geo-blocking for mUSD conversion feature to restrict access in non-compliant countries (#24501) +- Add Merkl Rewards Claim Functionality (#24487) +- Added per-token dismissal for mUSD conversion CTA on asset detail page (#24590) +- Added mUSD developer options section with button to reset education screen seen state (#24949) +- Updated copy for the mUSD conversion education screen. (#24948) +- Adds settings page for changing ramp region (#24856) +- Added optional quickActionsHint to custom-amount-info (#24914) +- Improved readability of market data on Token Details page by shortening large numbers with abbreviations (K/M/B/T) and (#24560) + increasing font size +- Added a check to make the buy button invisible for unsupported tokens (#24924) +- Updated the copy for the mUSD conversion claimable bonus tooltip. (#24912) +- - Add change utxo dropped when full swap use case ([#572](https://github.com/MetaMask/snap-bitcoin-wallet/pull/572)) (#24922) +- Update p2wsh, p2tr and p2sh dust minimum value + ([#570](https://github.com/MetaMask/snap-bitcoin-wallet/pull/570)) +- Refresh smart-transaction feature liveness in bridge and transaction flows. (#24087) +- Fixed font rendering on Android Card welcome screen, improved error messages for incorrect SMS codes, and enhanced keyboard (#24860) + handling during Card onboarding +- Add support for `InsufficientBalanceToCoverFee` error response from Snaps (#24747) +- (Behind feature flag) Fixed UI inconsistency when adding accounts in full-page account list mode - actions now appear as a (#24468) + bottom sheet overlay +- Added replaces active tab if max tabs are open and request comes from trending (#24555) +- Improved browser experience by hiding the app navigation bar when browsing and redesigned the browser controls with (#24444) + quick-access buttons for reloading pages, managing bookmarks, and + opening new tabs, plus added close and tabs buttons to the URL bar +- Add first iteration of mUSD conversion segment events (#24457) +- Added Perps Discovery Banner on spot asset detail screens to help users discover perpetual trading options (#24512) +- Adopted new ramp controller variable names (#24280) +- Updated Networks bottom sheet to open at 50% screen height with improved header and close button (#24493) +- Refactored mUSD conversion CTA rendering conditions (#24335) +- UseTokenAsset to make use of `@assets-controller` `getNativeTokenAddress` implementation. (#24483) +- Added a "Give feedback" button to Perps home screen (#24511) +- Hide Earn product entry points for ineligible users (#24490) +- Add 5 min polling to trending tokens (#24462) +- Adds intent based swap support (#23776) +- Improved Card onboarding flow with better state handling and navigation after delegation setup (#24430) +- Added perps deeplinks for home view, market list with category filtering, and HIP-3 asset support (#24132) +- Set trending token as the dest token when navigating to swaps (#24373) +- Misc mUSD conversion flow design changes ahead of UAT (#24336) +- Perps icon fallback url mechanism (#24340) +- Added fill type indicators (Take Profit, Stop Loss, Liquidation, ADL) to perps transactions on Home and Market Details (#24259) + screens +- Allow metal card rewards claim when season 1 ends (#24314) +- Ai workflow with jira and simulator setup (#24283) +- feat: bip39 word bar suggestions in onboarding and import srp module. (#22927) +- Added a fallback for Perps users who have Basic Functionality disabled. (#23952) +- Added remotely configurable allowlist and blocklist for mUSD conversion tokens. (#24126) +- Use optimistic approach when updating TP/SL in perps (#24159) +- Added OTA updates modal (#24175) +- Updated token list item musd conversion cta position to secondary balance position. This replaces the price change percentage (#24249) + for specified stablecoins +- Added a verification step to the Card onboarding flow to validate Veriff KYC results before proceeding. (#24246) +- Added override functionality to remote feature flags with methods to set and clear overrides and access A/B test groups (#23487) +- Improved initial cross ecosystem connection flows by preselecting all supported chains (EVM + Solana) when connecting through (#24137) + injected providers. +- Implement modal for user already has a Card account (#24007) +- Added card-home deeplink handler to navigate users to MetaMask Card based on authentication and card-linked account status (#24118) +- Fixed HIP-3 market symbols displaying with DEX prefix in Order Book view (#24068) +- Added ability to view trade details when tapping on recent trades in Perps, with a "Trade again" button to quickly navigate (#24067) + back to trading +- Added new card-onboarding deeplink to navigate users to the MetaMask Card feature (#24042) +- Reveal SRP flow ui and text updates. (#24058) +- Fixed "See all" button visibility in Perps Recent Activity section (#24099) +- Updated limit price presets to include Mid and Bid/Ask buttons for quicker price selection (#24069) +- Re-enabled order book access from perps market details view (#24010) +- Changed address copy confirmation from modal to Toast notification (#23980) +- Add Tron Account Change detection support for multichain Api (#23639) +- Removed hardcoded mainnet output chainId for mUSD conversion flow (#23704) +- Add animation for importSRP and create password layout and vice-versa (#24011) +- Updated mUSD conversion education screen theming (#23949) +- Update optin metrics screen ui changes (#24008) +- NFTs allow fetching one page and aborting fetch when navigating away (#23962) +- Removed the notification badge from the card icon button (#23978) +- Improved Toast component visual alignment and positioning (#23928) +- Updated Deposit page transition to slide in from the right instead of bottom (#23913) +- Updated NFT details pages to slide in from right instead of from bottom (#23912) +- Changed DeFi protocol detail page to slide in from right instead of from bottom (#23911) +- Added QuickNode fallback RPC for Monad. (#23479) +- Added paste button for email verification code input on Deposit flow (#23939) +- Automatically change the destination asset to match source asset chain when users land on swaps. (#23822) +- Added useTronStakeApy hook (#23743) +- Added support for delegating Base assets in the MetaMask Card feature (#23509) +- Implement MM Travel and ToS links on Card Home (#23550) +- Disable token selection in Perps and Predict deposits if no native balance and gas station not supported (#23680) +- Updated the mUSD confirmation screen's header to display the mUSD token and target network icon. (#23691) +- Added mUSD CTA to asset overview screen (#23562) +- Add support for progressive rollout FF (#23670) +- Adds "Inaccurate fee" alert in Confirmations (#23588) +- Fixed Perps tab to display total balance instead of available balance (#23466) +- Add conditional rendering of bridge-time-row by tx type (#23552) +- Adds 1-click approval/deposit transaction confirmation flow for Earn lending (#23154) +- Add mUSD conversion CTA above token list (#23390) +- Fixed an issue where users could accidentally add burn addresses to their contacts, which could lead to sending funds to (#23358) + unrecoverable addresses +- Social Login user create wallet will always enable metametric flag (#22469) + SRP user create wallet will always disable metametric + flag by default and prompted metametric screen +- Add support for gas-included swaps with EIP-7702 (#22593) +- Added MegaETH Mainnet to Additional Networks (PopularList) (#23401) +- Added new simplified predict market row item for trending (#23388) +- Network connection banner now hides immediately when network becomes available again (#23425) +- Get marketData on trending search request (#23197) +- Added automatic saving and restoration of trade form configuration when navigating away from the perps trading screen (#23321) +- Add mUSD conversion education screen on first visit to conversion flow (#22972) +- Added price deviation warning to Perps trading interface to prevent opening positions when perps price deviates (#23242) + significantly from spot price + +### Fixed + +- Fixed "Get 3% Stablecoins" heading being rendered on 3 lines. (#25052) +- Fixed `Stake` button showing for assets in the Tron network that were not native TRX (#25043) +- Updated design of perps SortBy bottomSheet (#24970) +- Update SRP flow to display multichain accounts (#24906) +- Fixed TrendingTokenPriceChangeBottomSheet to discard uncommitted changes when reopened. (#24977) +- Fixed TRX token logo displaying incorrectly in swap token selector list (#24942) +- Align the trending tokens network selector UI with the standard network selector for consistency. (#24417) +- Updated secondary mUSD conversion CTA text to get 3% mUSD bonus (#24944) +- Biometric choice logic update (#24695) +- Ensure proper responses when requesting invalid RPC methods using the multichain API (#24887) +- Fixed insufficient balance alert incorrectly showing when using max amount in MetaMask Pay (#24903) +- Trending view search filtering improvement (#24891) +- Display custom msg for chart data when there is a single data point (#24917) +- Remove the network confirmation modal on trending flow (#24888) +- Updated address copy confirmation to show a toast notification instead of inline overlay (#24599) +- Updated get mUSD cta to respect network filter when creating mUSD conversion tx (#24907) +- Predict empty search screen items (#24892) +- Removes Non evm balance section in asset details page when zero (#24332) +- Trending tokens view safe area cleanup (#24883) +- Explore sites icons sizes and padding issues (#24877) +- Fallback to symbol if name is null on trending page (#24813) +- Network selector startup crash (#24872) +- Fixed UI copy casing to align with sentence case standards and corrected punctuation inconsistencies (#23296) +- Adds per network min value params for trending token (#24730) +- Improved price display for trending tokens with subscript notation for very small values (e.g., $0.0₆14) (#24441) +- Show custom error msg page when user searches for token not found on trending page (#24569) +- Fixed a bug where TextField components could wrap text to multiple lines even when multiline={false} (#24584) +- Adds configurable minimum fiat balance required for tokens to be eligible for mUSD conversion flow and CTAs (#24532) +- Fixed a bug where quotes would attempt to refresh even while a swap was being actively submitted (#24552) +- None (#24548) +- Adds predict sports primitives (#24506) +- Fixed QR code scanning in dark mode by adding white background (#24533) +- Fix incorrect skeleton used for predictions when searching in main explore page (#24519) +- Fix trending tokens search crashes on search (#24517) +- Explore browser tab open behaviour when opening empty vs populated browser (#24371) +- Use preferred search engine on trending (#24513) +- Add fallback for cardFeature flag using local value (#24428) +- Use custom values for liquidity and min14hvalue param for trending requests (#24481) +- fix: search token input box size (#24380) +- Fixed sentence case violations in locale strings (#24451) +- Add MetaMask Portfolio to explore sites (#24369) +- Reset token icon when source changes to recalculate fallback (#24334) +- Fixed a bug where total volume was displaying the wrong value in asset details page (#24331) +- Fix Predict feed animations, tab bar UI and search overlay (#24337) +- Using v3 api to get historical prices for asset details page instead of v1 (#24315) +- Bug fix where a user couldn't scroll through the SRP reveal bottom sheet on Android (#24338) +- Removes `Add account` in the account selector during the ramps flow. (#22114) +- Fixed an issue where Perps withdrawal indicators could remain stuck in "pending" state after the withdrawal completed (#24214) +- Fix trending search not getting market data (#24311) +- Token 2022 send (#24350) +- Fixed TP/SL price input validation to respect HyperLiquid's 5 significant figure limit (#24169) +- Fixes an issue where flipping source/dest assets do not update balances when user has zero balance on non-evm chains (#24329) +- Add conditional mounting to perps tab in Activity View (#24271) +- Add hybrid REST/websocket approach to building recent activity (#24212) +- Added ScreenshotDeterrent to the login page to prevent screenshots and recordings on the page. (#24285) +- Improved Card performance by reducing redundant authentication token refresh requests and ensuring CardHome updates (#24295) + correctly after changing the spending asset or spending limit. +- Updated Edit Account Name to display as a full-page drill-down with right-to-left transition and back button instead of a (#23579) + bottom sheet modal +- Fixed Swap button not showing for trending tokens on asset details page (#24299) +- Prevent filtering of transactions with 0x0 hash in activity view (#24289) +- Fixes a small localization key bug in the OTP code screen for Buy (deposit) (#24264) +- Modify the android manifest so that android 12+ will never ask for location permission. (#23759) +- Fixes issue where certain search queries in Predict would return no results (#24266) +- Fixed UI styling inconsistencies in Perps close positions and market balance views (#24234) +- Fixed inconsistent font size for TP/SL text in Perps position card (#24238) +- Fixed analytics `ramp_type` to correctly report `UNIFIED_BUY` when Unified Ramp V1 is enabled (#24245) +- Phishing screen UX background color papercut (#23836) +- Use cache headers on sentinel calls (#23429) +- Fixed mUSD conversion flow not rejecting transactions when navigating away (#24171) +- Fixed inconsistent price formatting on TradingView chart y-axis that showed unnecessary trailing zeros (#24184) +- Fixed rounded corners in Perps order screen when there are more than 2 rows displayed (#24162) +- Fixed payment method selector modal scroll not working (#24188) +- Fixed liquidation distance showing 0% instead of fallback when liquidation price is unavailable (#24128) +- Fixed sentence case violations in English locale strings lines 6001-7000 (#24056) +- Fixed market name text being truncated in Perps header for hip-3 markets (#24140) +- Fixed position size USD display showing 3 decimals instead of 2 (#24131) +- Added indicator name shortcuts in Perps asset pills to reflect sorting mechanism (#24130) +- Fixed sentence case violations in English locale strings lines 7001-7279 (#24127) +- Fixed bug where requesting quotes for successive non-EVM networks failed (#24095) +- Fixes header text in token details page (#24101) +- Fix balance updates using the new feature flags for balance fetching (#24156) +- Fixed sentence case violations in English locale strings lines 4001-5000 (#23996) +- Fixed sentence case violations in English locale strings lines 5001-6000 (#24049) +- Fixed delay between the live candlestick close price and current price line on the Perps TradingView chart (#24100) +- Fixed date format display on Perps price chart x-axis from "6 Nov" to "11/6" format (#24103) +- Fixed entry price decimal precision in Perps trade history (#24096) +- Fox animation raise up issue during keyboard open. (#23354) +- Fixed Perps activity view to show aggregated PnL values for stop loss and take profit orders that execute as multiple fills (#24050) +- Add excludeLabels to trending api. (#23988) +- Fix biometric sudden reset issue (#24018) +- fix: resolved minor send flow UI issues (#24034) +- Fixed sentence case violations in English locale strings lines 2001-3000 (#23957) +- Fix non-live current price in leverage slider (#24046) +- Fixed OHLC bar text overflow for tokens with small prices (#23471) +- Fixed an issue where pending approval requests could become permanently stuck in the queue, preventing users from approving (#24040) + or rejecting subsequent connection requests. +- Avoid re-rendering home when navigating back from trending (#24062) +- Improved the minimum received bridge label by rounding down (#23851) +- Fix TokenListController initialization. (#24020) +- Fixed sentence case violations in English locale strings lines 3001-4000 (#23994) +- Remove ens calls from account list to improve perf (#23920) +- Fixed an issue where switching back to the native token after automatic gas fee token selection did not trigger the insufficient (#23855) + balance alert. +- Fixed balance display in Perps to show proper decimal formatting with trailing zeros (#23898) +- Fixes empty quote card details issue when swapping on Solana network (#23915) +- Fixed a bug where alert modal styling would not update correctly when switching between different severity alerts. (#23893) +- Fixed a bug that caused Swap button to be blocked even though a quote was available (#23792) +- fix: update asset details transition to left to right (#23933) +- Token details block explorer correct urls (#23890) +- Google login fallback in android if no google account available on device. (#23520) +- Reduced number of calls to bulk-scan for NFT detection (#23803) +- Add default blockexplorer for linea and mainnet. (#23861) +- Resetpassword screen touch-id state (#23798) +- Remove emojis from gas options (#23907) +- fix: remove background from more button in multichain account selector (#23904) +- Change selected account styles (#22203) +- Remove scroll on click when clicking a tx in the asset details modal (#23885) +- Fixed "Report a detection problem" button to link to the correct GitHub repository for reporting phishing detection issues (#23824) +- fix: updated checkbox border to correct color (#23846) +- Fixed hiding nft flow. (#23833) +- Improved import assets UI (#23735) +- Fix nft auto detection not triggered on network switch (#23858) +- Fix token balances init (#23668) +- Fix issue where rejecting a MMConnect confirmation results in a connection failure toast (rather than a error toast) in the (#23806) + wallet. +- Fix stable coin chart (#23168) +- Fix re-rendering of nfts grid (#23768) +- Fixed QR scanner navigation by chain type and recipient address pre-population in send flow (#21498) +- Fixed leverage initialization when modifying existing perpetual positions (#23619) +- Fixed a bug where stale quote data remained momentarily when changing swap destination token (#23737) +- Fixed bug where the EVM addresses were not checksummed (#23703) +- Fixed chart jumping issues when font accessibility is turned on (#23732) +- Updated swaps network picker ordering (#23686) +- fix: update bg color for earn upsell banner (#23701) +- fix: unified keyboard actions background color (#23649) +- Fixed sentence case violations in English locale strings lines 1-1000 (#23499) +- Fixed a flicker where the insufficient balance alert appeared before gas-station checks completed (#23361) +- fix: update lend UI (#23633) +- fix: enlarge question mark icon in lend header (#23635) +- Added readme for using OTA updates on nightly builds (#23573) +- Fixes an issue where when users selected the swap button for an asset presented in browser URL bar, that asset was not (#23534) + selected as source when navigating to swap page. +- Hides trending section if empty (#23586) +- Fixed withdrawal progress component to display decimal amounts correctly (e.g., $1.30 instead of $1) (#23477) +- Truncate long titles in Predict claim confirmations (#23462) +- Fixed a bug where mainnet where showing after testnets in the send modal screen (#23492) +- Fixed sentence case violations in English locale strings lines 1001-2000 (#23516) +- Improved Portfolio integration by passing tracking consent from Mobile app (#22683) +- Removes usePopularNetworks hook and replace it with constant. (#23525) +- Google One Tap "user disabled the feature" errors are no longer sent to Sentry (#23101) +- Fixed an issue where "Fund your wallet" empty state briefly appeared when importing wallets with existing funds or switching (#23351) + between funded accounts +- Fixed internal transfers not appearing in perps transaction history (#23405) +- Filter and handle unsupported notifications (#23291) +- Fix trending tokens inconsistencies in search results (#23408) +- Prevent any dialogs for multichain wallet Snaps (Solana, Bitcoin, Tron) (#23218) + ## [7.62.2] ### Fixed @@ -9917,7 +10210,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#957](https://github.com/MetaMask/metamask-mobile/pull/957): fix timeouts (#957) - [#954](https://github.com/MetaMask/metamask-mobile/pull/954): Bugfix: onboarding navigation (#954) -[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.62.2...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.63.1...HEAD +[7.63.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.63.0...v7.63.1 +[7.63.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.62.2...v7.63.0 [7.62.2]: https://github.com/MetaMask/metamask-mobile/compare/v7.62.1...v7.62.2 [7.62.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.62.0...v7.62.1 [7.62.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.61.6...v7.62.0 diff --git a/app/components/Nav/App/App.tsx b/app/components/Nav/App/App.tsx index 9dfabc307c5..ede5ded4102 100644 --- a/app/components/Nav/App/App.tsx +++ b/app/components/Nav/App/App.tsx @@ -110,10 +110,7 @@ import { TraceOperation, } from '../../../util/trace'; import getUIStartupSpan from '../../../core/Performance/UIStartup'; -import { - selectUserLoggedIn, - selectExistingUser, -} from '../../../reducers/user/selectors'; +import { selectExistingUser } from '../../../reducers/user/selectors'; import { Confirm } from '../../Views/confirmations/components/confirm'; import ImportNewSecretRecoveryPhrase from '../../Views/ImportNewSecretRecoveryPhrase'; import { SelectSRPBottomSheet } from '../../Views/SelectSRP/SelectSRPBottomSheet'; @@ -884,210 +881,200 @@ const ModalSmartAccountOptIn = () => ( ); -const AppFlow = () => { - const userLoggedIn = useSelector(selectUserLoggedIn); - - return ( - <> - - {userLoggedIn && ( - // Render only if wallet is unlocked - // Note: This is probably not needed but nice to ensure that wallet isn't accessible when it is locked - - )} - - - - - - - - - - { - - } - - - - - ({ - cardStyle: { - transform: [ - { - translateX: current.progress.interpolate({ - inputRange: [0, 1], - outputRange: [layouts.screen.width, 0], - }), - }, - ], - }, - }), - }} - /> - - - - - ({ - overlayStyle: { - opacity: 0, - }, - }), - }} - name={Routes.LEDGER_TRANSACTION_MODAL} - component={LedgerTransactionModal} - /> - ({ - overlayStyle: { - opacity: 0, - }, - }), - }} - name={Routes.QR_SIGNING_TRANSACTION_MODAL} - component={QRSigningTransactionModal} - /> - ({ - overlayStyle: { - opacity: 0, +const AppFlow = () => ( + + + + + + + + + + + + { + + } + + + + + ({ + cardStyle: { + transform: [ + { + translateX: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [layouts.screen.width, 0], + }), }, - }), - }} - name={Routes.LEDGER_MESSAGE_SIGN_MODAL} - component={LedgerMessageSignModal} - /> - - - - {isNetworkUiRedesignEnabled() ? ( - - ) : null} - - - - - - - - ); -}; + ], + }, + }), + }} + /> + + + + + ({ + overlayStyle: { + opacity: 0, + }, + }), + }} + name={Routes.LEDGER_TRANSACTION_MODAL} + component={LedgerTransactionModal} + /> + ({ + overlayStyle: { + opacity: 0, + }, + }), + }} + name={Routes.QR_SIGNING_TRANSACTION_MODAL} + component={QRSigningTransactionModal} + /> + ({ + overlayStyle: { + opacity: 0, + }, + }), + }} + name={Routes.LEDGER_MESSAGE_SIGN_MODAL} + component={LedgerMessageSignModal} + /> + + + + {isNetworkUiRedesignEnabled() ? ( + + ) : null} + + + + + + +); const App: React.FC = () => { const { toastRef } = useContext(ToastContext); diff --git a/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap b/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap index 4c739a41fba..6200e60439e 100644 --- a/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap +++ b/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap @@ -260,6 +260,39 @@ exports[`MainNavigator Tab Bar Visibility hides tab bar when browser is active 1 } } /> + + + + + + + + + = ({ screen: Routes.PERPS.MARKET_DETAILS, params: { market: marketData, - source: PerpsEventValues.SOURCE.ASSET_DETAIL_SCREEN, + source: PERPS_EVENT_VALUE.SOURCE.ASSET_DETAIL_SCREEN, }, }); } diff --git a/app/components/UI/Perps/Debug/HIP3DebugView.tsx b/app/components/UI/Perps/Debug/HIP3DebugView.tsx index b32efdf94d2..91e15eb0043 100644 --- a/app/components/UI/Perps/Debug/HIP3DebugView.tsx +++ b/app/components/UI/Perps/Debug/HIP3DebugView.tsx @@ -11,6 +11,7 @@ import Text, { } from '../../../../component-library/components/Texts/Text'; import { useStyles } from '../../../../component-library/hooks'; import DevLogger from '../../../../core/SDKConnect/utils/DevLogger'; +import { ensureError } from '../../../../util/errorUtils'; import styleSheet from './HIP3DebugView.styles'; import Engine from '../../../../core/Engine'; import type { HyperLiquidProvider } from '../controllers/providers/HyperLiquidProvider'; @@ -96,10 +97,8 @@ const HIP3DebugView: React.FC = () => { DevLogger.log(' 3. API connectivity issues'); } } catch (error) { - const errorInfo = - error instanceof Error - ? { message: error.message, stack: error.stack } - : String(error); + const ensured = ensureError(error); + const errorInfo = { message: ensured.message, stack: ensured.stack }; DevLogger.log( '❌ Failed to load DEXs:\n' + JSON.stringify(errorInfo, null, 2), ); @@ -142,10 +141,8 @@ const HIP3DebugView: React.FC = () => { ); } } catch (error) { - const errorInfo = - error instanceof Error - ? { message: error.message, stack: error.stack } - : String(error); + const ensured = ensureError(error); + const errorInfo = { message: ensured.message, stack: ensured.stack }; DevLogger.log( '❌ Failed to load markets:\n' + JSON.stringify(errorInfo, null, 2), ); @@ -210,10 +207,8 @@ const HIP3DebugView: React.FC = () => { DevLogger.log('✅ Balance check complete'); } catch (error) { - const errorInfo = - error instanceof Error - ? { message: error.message, stack: error.stack } - : String(error); + const ensured = ensureError(error); + const errorInfo = { message: ensured.message, stack: ensured.stack }; DevLogger.log( '❌ Failed to check balances:\n' + JSON.stringify(errorInfo, null, 2), ); @@ -250,15 +245,12 @@ const HIP3DebugView: React.FC = () => { throw new Error(result.error || 'Transfer failed'); } } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - const errorInfo = - error instanceof Error - ? { message: error.message, stack: error.stack } - : String(error); + const ensured = ensureError(error); + const errorInfo = { message: ensured.message, stack: ensured.stack }; DevLogger.log( '❌ Transfer failed:\n' + JSON.stringify(errorInfo, null, 2), ); - setTransferResult({ status: 'error', error: errorMsg }); + setTransferResult({ status: 'error', error: ensured.message }); } }; @@ -311,15 +303,12 @@ const HIP3DebugView: React.FC = () => { throw new Error(result.error || 'Transfer failed'); } } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - const errorInfo = - error instanceof Error - ? { message: error.message, stack: error.stack } - : String(error); + const ensured = ensureError(error); + const errorInfo = { message: ensured.message, stack: ensured.stack }; DevLogger.log( '❌ Transfer failed:\n' + JSON.stringify(errorInfo, null, 2), ); - setTransferResult({ status: 'error', error: errorMsg }); + setTransferResult({ status: 'error', error: ensured.message }); } }; @@ -414,13 +403,10 @@ const HIP3DebugView: React.FC = () => { throw new Error(result.error || 'Order failed'); } } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - const errorInfo = - error instanceof Error - ? { message: error.message, stack: error.stack } - : String(error); + const ensured = ensureError(error); + const errorInfo = { message: ensured.message, stack: ensured.stack }; DevLogger.log('❌ Order failed:\n' + JSON.stringify(errorInfo, null, 2)); - setOrderResult({ status: 'error', error: errorMsg }); + setOrderResult({ status: 'error', error: ensured.message }); } }; @@ -467,13 +453,10 @@ const HIP3DebugView: React.FC = () => { throw new Error(result.error || 'Close failed'); } } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - const errorInfo = - error instanceof Error - ? { message: error.message, stack: error.stack } - : String(error); + const ensured = ensureError(error); + const errorInfo = { message: ensured.message, stack: ensured.stack }; DevLogger.log('❌ Close failed:\n' + JSON.stringify(errorInfo, null, 2)); - setCloseResult({ status: 'error', error: errorMsg }); + setCloseResult({ status: 'error', error: ensured.message }); } }; diff --git a/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.tsx b/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.tsx index 3fd7d96d1fb..b10e744961c 100644 --- a/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.tsx +++ b/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.tsx @@ -29,8 +29,8 @@ import { useTheme } from '../../../../../util/theme'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import type { CancelOrdersResult } from '../../controllers/types'; @@ -61,9 +61,9 @@ const PerpsCancelAllOrdersView: React.FC = ({ eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, conditions: [true], // Always track when component mounts (WebSocket data loads instantly) properties: { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.CANCEL_ALL_ORDERS, - [PerpsEventProperties.OPEN_POSITION]: orders?.length || 0, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.CANCEL_ALL_ORDERS, + [PERPS_EVENT_PROPERTY.OPEN_POSITION]: orders?.length || 0, }, }); diff --git a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.tsx b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.tsx index bdf5ab3c8d0..c92149cebdb 100644 --- a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.tsx +++ b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.tsx @@ -36,8 +36,8 @@ import { useTheme } from '../../../../../util/theme'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import type { ClosePositionsResult } from '../../controllers/types'; @@ -87,9 +87,9 @@ const PerpsCloseAllPositionsView: React.FC = ({ eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, conditions: [!isInitialLoading], properties: { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.CLOSE_ALL_POSITIONS, - [PerpsEventProperties.OPEN_POSITION]: positions?.length || 0, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.CLOSE_ALL_POSITIONS, + [PERPS_EVENT_PROPERTY.OPEN_POSITION]: positions?.length || 0, }, }); diff --git a/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx b/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx index 21a302dfc98..04bdf9d7a0c 100644 --- a/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx +++ b/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx @@ -69,8 +69,8 @@ import { } from '../../utils/positionCalculations'; import { createStyles } from './PerpsClosePositionView.styles'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { TraceName } from '../../../../../util/trace'; @@ -327,17 +327,17 @@ const PerpsClosePositionView: React.FC = () => { usePerpsEventTracking({ eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, properties: { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.POSITION_CLOSE, - [PerpsEventProperties.ASSET]: position.symbol, - [PerpsEventProperties.DIRECTION]: isLong - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.POSITION_SIZE]: absSize, - [PerpsEventProperties.UNREALIZED_PNL_DOLLAR]: pnl, - [PerpsEventProperties.UNREALIZED_PNL_PERCENT]: unrealizedPnlPercent, - [PerpsEventProperties.SOURCE]: PerpsEventValues.SOURCE.PERP_ASSET_SCREEN, - [PerpsEventProperties.RECEIVED_AMOUNT]: receiveAmount, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.POSITION_CLOSE, + [PERPS_EVENT_PROPERTY.ASSET]: position.symbol, + [PERPS_EVENT_PROPERTY.DIRECTION]: isLong + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.POSITION_SIZE]: absSize, + [PERPS_EVENT_PROPERTY.UNREALIZED_PNL_DOLLAR]: pnl, + [PERPS_EVENT_PROPERTY.UNREALIZED_PNL_PERCENT]: unrealizedPnlPercent, + [PERPS_EVENT_PROPERTY.SOURCE]: PERPS_EVENT_VALUE.SOURCE.PERP_ASSET_SCREEN, + [PERPS_EVENT_PROPERTY.RECEIVED_AMOUNT]: receiveAmount, }, }); diff --git a/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.test.tsx b/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.test.tsx index b5ba8877035..fefa985b27b 100644 --- a/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.test.tsx @@ -11,8 +11,8 @@ import Share from 'react-native-share'; import Logger from '../../../../../util/Logger'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { PerpsHeroCardViewSelectorsIDs, @@ -425,9 +425,9 @@ describe('PerpsHeroCardView', () => { expect(mockTrack).toHaveBeenCalledWith( MetaMetricsEvents.PERPS_UI_INTERACTION, expect.objectContaining({ - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.INITIATED, - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.SHARE_PNL_HERO_CARD, + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.INITIATED, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.SHARE_PNL_HERO_CARD, }), ); }); @@ -446,9 +446,9 @@ describe('PerpsHeroCardView', () => { expect(mockTrack).toHaveBeenCalledWith( MetaMetricsEvents.PERPS_UI_INTERACTION, expect.objectContaining({ - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.SUCCESS, - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.SHARE_PNL_HERO_CARD, + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.SUCCESS, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.SHARE_PNL_HERO_CARD, }), ); }); @@ -512,10 +512,10 @@ describe('PerpsHeroCardView', () => { expect(mockTrack).toHaveBeenCalledWith( MetaMetricsEvents.PERPS_UI_INTERACTION, expect.objectContaining({ - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.ERROR_MESSAGE]: 'Share failed', - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.SHARE_PNL_HERO_CARD, + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: 'Share failed', + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.SHARE_PNL_HERO_CARD, }), ); }); diff --git a/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx b/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx index 09c6625ac74..89dd1e1e6ac 100644 --- a/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx +++ b/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx @@ -50,8 +50,8 @@ import Logger from '../../../../../util/Logger'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { buildReferralUrl } from '../../../Rewards/utils'; import { usePerpsToasts } from '../../hooks'; @@ -63,6 +63,7 @@ import { import { useReferralDetails } from '../../../Rewards/hooks/useReferralDetails'; import { useSeasonStatus } from '../../../Rewards/hooks/useSeasonStatus'; import { getPerpsDisplaySymbol } from '../../utils/marketUtils'; +import { ensureError } from '../../../../../util/errorUtils'; // To add a new card, add the image to the array. const CARD_IMAGES: { image: ImageSourcePropType; id: number; name: string }[] = @@ -145,23 +146,23 @@ const PerpsHeroCardView: React.FC = () => { // Track PnL hero card screen viewed // Determine entry point: asset_screen or close_toast const entryPoint = - source === PerpsEventValues.SOURCE.CLOSE_TOAST - ? PerpsEventValues.SOURCE.CLOSE_TOAST - : PerpsEventValues.SOURCE.PERP_ASSET_SCREEN; + source === PERPS_EVENT_VALUE.SOURCE.CLOSE_TOAST + ? PERPS_EVENT_VALUE.SOURCE.CLOSE_TOAST + : PERPS_EVENT_VALUE.SOURCE.PERP_ASSET_SCREEN; usePerpsEventTracking({ eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, properties: { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.PNL_HERO_CARD, - [PerpsEventProperties.ASSET]: position.symbol, - [PerpsEventProperties.DIRECTION]: + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.PNL_HERO_CARD, + [PERPS_EVENT_PROPERTY.ASSET]: position.symbol, + [PERPS_EVENT_PROPERTY.DIRECTION]: data.direction === 'long' - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.SOURCE]: entryPoint, - [PerpsEventProperties.PNL_DOLLAR]: data.pnl, - [PerpsEventProperties.PNL_PERCENT]: data.roe, + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.SOURCE]: entryPoint, + [PERPS_EVENT_PROPERTY.PNL_DOLLAR]: data.pnl, + [PERPS_EVENT_PROPERTY.PNL_PERCENT]: data.roe, }, }); @@ -170,16 +171,16 @@ const PerpsHeroCardView: React.FC = () => { usePerpsEventTracking({ eventName: MetaMetricsEvents.PERPS_UI_INTERACTION, properties: { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.DISPLAY_HERO_CARD, - [PerpsEventProperties.ASSET]: position.symbol, - [PerpsEventProperties.DIRECTION]: + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.DISPLAY_HERO_CARD, + [PERPS_EVENT_PROPERTY.ASSET]: position.symbol, + [PERPS_EVENT_PROPERTY.DIRECTION]: data.direction === 'long' - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.SOURCE]: entryPoint, - [PerpsEventProperties.PNL_DOLLAR]: data.pnl, - [PerpsEventProperties.PNL_PERCENT]: data.roe, + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.SOURCE]: entryPoint, + [PERPS_EVENT_PROPERTY.PNL_DOLLAR]: data.pnl, + [PERPS_EVENT_PROPERTY.PNL_PERCENT]: data.roe, }, }); @@ -362,7 +363,7 @@ const PerpsHeroCardView: React.FC = () => { } return null; } catch (error) { - Logger.error(error as Error, { + Logger.error(ensureError(error), { message: 'Error capturing Perps Hero Card', context: 'PerpsHeroCardView.captureCard', }); @@ -375,16 +376,16 @@ const PerpsHeroCardView: React.FC = () => { const imageSelected = CARD_IMAGES[currentTab].name; const sharedEventProperties = { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.SHARE_PNL_HERO_CARD, - [PerpsEventProperties.SCREEN_NAME]: - PerpsEventValues.SCREEN_NAME.PERPS_HERO_CARD, - [PerpsEventProperties.ASSET]: data.asset, - [PerpsEventProperties.DIRECTION]: data.direction, - [PerpsEventProperties.LEVERAGE]: data.leverage, - [PerpsEventProperties.PNL_PERCENT]: pnlDisplay, - [PerpsEventProperties.IMAGE_SELECTED]: imageSelected, - [PerpsEventProperties.TAB_NUMBER]: currentTab, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.SHARE_PNL_HERO_CARD, + [PERPS_EVENT_PROPERTY.SCREEN_NAME]: + PERPS_EVENT_VALUE.SCREEN_NAME.PERPS_HERO_CARD, + [PERPS_EVENT_PROPERTY.ASSET]: data.asset, + [PERPS_EVENT_PROPERTY.DIRECTION]: data.direction, + [PERPS_EVENT_PROPERTY.LEVERAGE]: data.leverage, + [PERPS_EVENT_PROPERTY.PNL_PERCENT]: pnlDisplay, + [PERPS_EVENT_PROPERTY.IMAGE_SELECTED]: imageSelected, + [PERPS_EVENT_PROPERTY.TAB_NUMBER]: currentTab, }; let result: ShareOpenResult | null = null; @@ -394,7 +395,7 @@ const PerpsHeroCardView: React.FC = () => { if (imageUri) { track(MetaMetricsEvents.PERPS_UI_INTERACTION, { ...sharedEventProperties, - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.INITIATED, + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.INITIATED, }); const message = effectiveReferralCode @@ -418,7 +419,7 @@ const PerpsHeroCardView: React.FC = () => { if (result?.success) { track(MetaMetricsEvents.PERPS_UI_INTERACTION, { ...sharedEventProperties, - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.SUCCESS, + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.SUCCESS, }); showToast(PerpsToastOptions.contentSharing.pnlHeroCard.shareSuccess); } @@ -429,8 +430,8 @@ const PerpsHeroCardView: React.FC = () => { track(MetaMetricsEvents.PERPS_UI_INTERACTION, { ...sharedEventProperties, - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.ERROR_MESSAGE]: errorMessage, + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: errorMessage, }); // Don't show error toast if user dismissed the share dialog @@ -438,7 +439,7 @@ const PerpsHeroCardView: React.FC = () => { showToast(PerpsToastOptions.contentSharing.pnlHeroCard.shareFailed); } - Logger.error(error as Error, { + Logger.error(ensureError(error), { message: 'Error sharing Perps Hero Card', context: 'PerpsHeroCardView.handleShare', }); diff --git a/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.test.tsx b/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.test.tsx index 00d38f01617..9c88e38644d 100644 --- a/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react-native'; import PerpsHomeView from './PerpsHomeView'; -import { PerpsEventValues } from '../../constants/eventNames'; +import { PERPS_EVENT_VALUE } from '../../constants/eventNames'; import { selectPerpsFeedbackEnabledFlag } from '../../selectors/featureFlags'; // Mock navigation @@ -17,7 +17,7 @@ jest.mock('@react-navigation/native', () => ({ }), useRoute: () => ({ params: { - source: 'main_action_button', // PerpsEventValues.SOURCE.MAIN_ACTION_BUTTON + source: 'main_action_button', // PERPS_EVENT_VALUE.SOURCE.MAIN_ACTION_BUTTON }, }), useFocusEffect: (callback: () => void) => { @@ -221,7 +221,7 @@ jest.mock('../../../../../util/trace', () => ({ })); jest.mock('../../constants/eventNames', () => ({ - PerpsEventProperties: { + PERPS_EVENT_PROPERTY: { SCREEN_TYPE: 'screen_type', SOURCE: 'source', BUTTON_CLICKED: 'button_clicked', @@ -229,7 +229,7 @@ jest.mock('../../constants/eventNames', () => ({ INTERACTION_TYPE: 'interaction_type', LOCATION: 'location', }, - PerpsEventValues: { + PERPS_EVENT_VALUE: { SCREEN_TYPE: { MARKETS: 'markets', HOMESCREEN: 'homescreen', @@ -558,7 +558,7 @@ describe('PerpsHomeView', () => { expect(mockNavigateToMarketList).toHaveBeenCalledWith({ defaultSearchVisible: true, defaultMarketTypeFilter: 'all', - source: PerpsEventValues.SOURCE.HOMESCREEN_TAB, + source: PERPS_EVENT_VALUE.SOURCE.HOMESCREEN_TAB, fromHome: true, button_clicked: 'magnifying_glass', button_location: 'perps_home', diff --git a/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx b/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx index 5fdb0ae8eec..235caac57d8 100644 --- a/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx +++ b/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx @@ -59,8 +59,8 @@ import { useMetrics, MetaMetricsEvents } from '../../../../hooks/useMetrics'; import styleSheet from './PerpsHomeView.styles'; import { TraceName } from '../../../../../util/trace'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; import { PerpsHomeViewSelectorsIDs } from '../../Perps.testIds'; @@ -100,7 +100,7 @@ const PerpsHomeView = () => { isEligibilityModalVisible, closeEligibilityModal, } = usePerpsHomeActions({ - buttonLocation: PerpsEventValues.BUTTON_LOCATION.PERPS_HOME, + buttonLocation: PERPS_EVENT_VALUE.BUTTON_LOCATION.PERPS_HOME, }); // Separate geo-block modal state for close all / cancel all actions @@ -188,7 +188,7 @@ const PerpsHomeView = () => { // Track home screen viewed event const source = - route.params?.source || PerpsEventValues.SOURCE.MAIN_ACTION_BUTTON; + route.params?.source || PERPS_EVENT_VALUE.SOURCE.MAIN_ACTION_BUTTON; // Get perp balance status for tracking const livePositions = usePerpsLivePositions({ throttleMs: 5000 }); @@ -204,15 +204,15 @@ const PerpsHomeView = () => { eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, conditions: [!isAnyLoading], properties: { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.PERPS_HOME, - [PerpsEventProperties.SOURCE]: source, - [PerpsEventProperties.HAS_PERP_BALANCE]: hasPerpBalance, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.PERPS_HOME, + [PERPS_EVENT_PROPERTY.SOURCE]: source, + [PERPS_EVENT_PROPERTY.HAS_PERP_BALANCE]: hasPerpBalance, ...(buttonClicked && { - [PerpsEventProperties.BUTTON_CLICKED]: buttonClicked, + [PERPS_EVENT_PROPERTY.BUTTON_CLICKED]: buttonClicked, }), ...(buttonLocation && { - [PerpsEventProperties.BUTTON_LOCATION]: buttonLocation, + [PERPS_EVENT_PROPERTY.BUTTON_LOCATION]: buttonLocation, }), }, }); @@ -222,12 +222,12 @@ const PerpsHomeView = () => { trackEvent( createEventBuilder(MetaMetricsEvents.PERPS_UI_INTERACTION) .addProperties({ - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.BUTTON_CLICKED, - [PerpsEventProperties.BUTTON_CLICKED]: - PerpsEventValues.BUTTON_CLICKED.MAGNIFYING_GLASS, - [PerpsEventProperties.BUTTON_LOCATION]: - PerpsEventValues.BUTTON_LOCATION.PERPS_HOME, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.BUTTON_CLICKED, + [PERPS_EVENT_PROPERTY.BUTTON_CLICKED]: + PERPS_EVENT_VALUE.BUTTON_CLICKED.MAGNIFYING_GLASS, + [PERPS_EVENT_PROPERTY.BUTTON_LOCATION]: + PERPS_EVENT_VALUE.BUTTON_LOCATION.PERPS_HOME, }) .build(), ); @@ -236,10 +236,10 @@ const PerpsHomeView = () => { perpsNavigation.navigateToMarketList({ defaultSearchVisible: true, defaultMarketTypeFilter: 'all', - source: PerpsEventValues.SOURCE.HOMESCREEN_TAB, + source: PERPS_EVENT_VALUE.SOURCE.HOMESCREEN_TAB, fromHome: true, - button_clicked: PerpsEventValues.BUTTON_CLICKED.MAGNIFYING_GLASS, - button_location: PerpsEventValues.BUTTON_LOCATION.PERPS_HOME, + button_clicked: PERPS_EVENT_VALUE.BUTTON_CLICKED.MAGNIFYING_GLASS, + button_location: PERPS_EVENT_VALUE.BUTTON_LOCATION.PERPS_HOME, }); }, [perpsNavigation, trackEvent, createEventBuilder]); @@ -248,17 +248,17 @@ const PerpsHomeView = () => { trackEvent( createEventBuilder(MetaMetricsEvents.PERPS_UI_INTERACTION) .addProperties({ - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.BUTTON_CLICKED, - [PerpsEventProperties.BUTTON_CLICKED]: - PerpsEventValues.BUTTON_CLICKED.TUTORIAL, - [PerpsEventProperties.BUTTON_LOCATION]: - PerpsEventValues.BUTTON_LOCATION.PERPS_HOME, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.BUTTON_CLICKED, + [PERPS_EVENT_PROPERTY.BUTTON_CLICKED]: + PERPS_EVENT_VALUE.BUTTON_CLICKED.TUTORIAL, + [PERPS_EVENT_PROPERTY.BUTTON_LOCATION]: + PERPS_EVENT_VALUE.BUTTON_LOCATION.PERPS_HOME, }) .build(), ); navigation.navigate(Routes.PERPS.TUTORIAL, { - source: PerpsEventValues.SOURCE.HOMESCREEN_TAB, + source: PERPS_EVENT_VALUE.SOURCE.HOMESCREEN_TAB, }); }, [navigation, trackEvent, createEventBuilder]); @@ -274,10 +274,10 @@ const PerpsHomeView = () => { trackEvent( createEventBuilder(MetaMetricsEvents.PERPS_UI_INTERACTION) .addProperties({ - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.CONTACT_SUPPORT, - [PerpsEventProperties.LOCATION]: - PerpsEventValues.BUTTON_LOCATION.PERPS_HOME, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.CONTACT_SUPPORT, + [PERPS_EVENT_PROPERTY.LOCATION]: + PERPS_EVENT_VALUE.BUTTON_LOCATION.PERPS_HOME, }) .build(), ); @@ -292,12 +292,12 @@ const PerpsHomeView = () => { trackEvent( createEventBuilder(MetaMetricsEvents.PERPS_UI_INTERACTION) .addProperties({ - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.BUTTON_CLICKED, - [PerpsEventProperties.BUTTON_CLICKED]: - PerpsEventValues.BUTTON_CLICKED.GIVE_FEEDBACK, - [PerpsEventProperties.BUTTON_LOCATION]: - PerpsEventValues.BUTTON_LOCATION.PERPS_HOME, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.BUTTON_CLICKED, + [PERPS_EVENT_PROPERTY.BUTTON_CLICKED]: + PERPS_EVENT_VALUE.BUTTON_CLICKED.GIVE_FEEDBACK, + [PERPS_EVENT_PROPERTY.BUTTON_LOCATION]: + PERPS_EVENT_VALUE.BUTTON_LOCATION.PERPS_HOME, }) .build(), ); @@ -352,10 +352,10 @@ const PerpsHomeView = () => { // Geo-restriction check for close all positions if (!isEligible) { track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.CLOSE_ALL_POSITIONS_BUTTON, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.CLOSE_ALL_POSITIONS_BUTTON, }); setIsCloseAllGeoBlockVisible(true); return; @@ -458,7 +458,7 @@ const PerpsHomeView = () => { ))} @@ -478,7 +478,7 @@ const PerpsHomeView = () => { ))} diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx index 7bf6427d299..7afa430cc67 100644 --- a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx +++ b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx @@ -81,8 +81,8 @@ import { TimeDuration, } from '../../constants/chartConfig'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { PERPS_CONSTANTS } from '../../constants/perpsConfig'; import { @@ -498,15 +498,15 @@ const PerpsMarketDetailsView: React.FC = () => { !isLoadingPosition, ], properties: { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.ASSET_DETAILS, - [PerpsEventProperties.ASSET]: market?.symbol || '', - [PerpsEventProperties.SOURCE]: - source || PerpsEventValues.SOURCE.PERP_MARKETS, - [PerpsEventProperties.OPEN_POSITION]: existingPosition ? 1 : 0, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.ASSET_DETAILS, + [PERPS_EVENT_PROPERTY.ASSET]: market?.symbol || '', + [PERPS_EVENT_PROPERTY.SOURCE]: + source || PERPS_EVENT_VALUE.SOURCE.PERP_MARKETS, + [PERPS_EVENT_PROPERTY.OPEN_POSITION]: existingPosition ? 1 : 0, // A/B Test context (TAT-1937) - for baseline exposure tracking ...(isButtonColorTestEnabled && { - [PerpsEventProperties.AB_TEST_BUTTON_COLOR]: buttonColorVariant, + [PERPS_EVENT_PROPERTY.AB_TEST_BUTTON_COLOR]: buttonColorVariant, }), }, }); @@ -518,10 +518,10 @@ const PerpsMarketDetailsView: React.FC = () => { // Track chart interaction track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.ASSET]: market?.symbol || '', - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.CANDLE_PERIOD_CHANGED, - [PerpsEventProperties.CANDLE_PERIOD]: newPeriod, + [PERPS_EVENT_PROPERTY.ASSET]: market?.symbol || '', + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.CANDLE_PERIOD_CHANGED, + [PERPS_EVENT_PROPERTY.CANDLE_PERIOD]: newPeriod, }); // Note: Chart will auto-zoom to latest candle when new data arrives (see useEffect below) @@ -586,14 +586,14 @@ const PerpsMarketDetailsView: React.FC = () => { const watchlistCount = controller.getWatchlistMarkets().length; track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.FAVORITE_TOGGLED, - [PerpsEventProperties.ACTION_TYPE]: newWatchlistState - ? PerpsEventValues.ACTION_TYPE.FAVORITE_MARKET - : PerpsEventValues.ACTION_TYPE.UNFAVORITE_MARKET, - [PerpsEventProperties.ASSET]: market.symbol, - [PerpsEventProperties.SOURCE]: PerpsEventValues.SOURCE.PERP_ASSET_SCREEN, - [PerpsEventProperties.FAVORITES_COUNT]: watchlistCount, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.FAVORITE_TOGGLED, + [PERPS_EVENT_PROPERTY.ACTION_TYPE]: newWatchlistState + ? PERPS_EVENT_VALUE.ACTION_TYPE.FAVORITE_MARKET + : PERPS_EVENT_VALUE.ACTION_TYPE.UNFAVORITE_MARKET, + [PERPS_EVENT_PROPERTY.ASSET]: market.symbol, + [PERPS_EVENT_PROPERTY.SOURCE]: PERPS_EVENT_VALUE.SOURCE.PERP_ASSET_SCREEN, + [PERPS_EVENT_PROPERTY.FAVORITES_COUNT]: watchlistCount, }); }, [market, isWatchlist, track]); @@ -602,9 +602,9 @@ const PerpsMarketDetailsView: React.FC = () => { if (!isEligible) { // Track geo-block screen viewed track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: PerpsEventValues.SOURCE.TRADE_ACTION, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: PERPS_EVENT_VALUE.SOURCE.TRADE_ACTION, }); setIsEligibilityModalVisible(true); return; @@ -617,14 +617,14 @@ const PerpsMarketDetailsView: React.FC = () => { }); track(MetaMetricsEvents.PERPS_ERROR, { - [PerpsEventProperties.ERROR_TYPE]: - PerpsEventValues.ERROR_TYPE.VALIDATION, - [PerpsEventProperties.ERROR_MESSAGE]: + [PERPS_EVENT_PROPERTY.ERROR_TYPE]: + PERPS_EVENT_VALUE.ERROR_TYPE.VALIDATION, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: 'Cross margin position detected', - [PerpsEventProperties.SCREEN_NAME]: - PerpsEventValues.SCREEN_NAME.PERPS_MARKET_DETAILS, - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.ASSET_DETAILS, + [PERPS_EVENT_PROPERTY.SCREEN_NAME]: + PERPS_EVENT_VALUE.SCREEN_NAME.PERPS_MARKET_DETAILS, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.ASSET_DETAILS, }); return; @@ -633,14 +633,14 @@ const PerpsMarketDetailsView: React.FC = () => { // Track AB test on button press (TAT-1937) if (isButtonColorTestEnabled) { track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TAP, - [PerpsEventProperties.ASSET]: market.symbol, - [PerpsEventProperties.DIRECTION]: + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TAP, + [PERPS_EVENT_PROPERTY.ASSET]: market.symbol, + [PERPS_EVENT_PROPERTY.DIRECTION]: direction === 'long' - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.AB_TEST_BUTTON_COLOR]: buttonColorVariant, + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.AB_TEST_BUTTON_COLOR]: buttonColorVariant, }); } @@ -674,22 +674,22 @@ const PerpsMarketDetailsView: React.FC = () => { const handleAddFundsPress = async () => { // Track deposit button click from asset screen track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.BUTTON_CLICKED, - [PerpsEventProperties.BUTTON_CLICKED]: - PerpsEventValues.BUTTON_CLICKED.DEPOSIT, - [PerpsEventProperties.BUTTON_LOCATION]: - PerpsEventValues.BUTTON_LOCATION.PERPS_ASSET_SCREEN, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.BUTTON_CLICKED, + [PERPS_EVENT_PROPERTY.BUTTON_CLICKED]: + PERPS_EVENT_VALUE.BUTTON_CLICKED.DEPOSIT, + [PERPS_EVENT_PROPERTY.BUTTON_LOCATION]: + PERPS_EVENT_VALUE.BUTTON_LOCATION.PERPS_ASSET_SCREEN, }); try { if (!isEligible) { // Track geo-block screen viewed track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.ADD_FUNDS_ACTION, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.ADD_FUNDS_ACTION, }); setIsEligibilityModalVisible(true); return; @@ -736,10 +736,10 @@ const PerpsMarketDetailsView: React.FC = () => { // Geo-restriction check for auto-close (TP/SL) action if (!isEligible) { track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.AUTO_CLOSE_ACTION, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.AUTO_CLOSE_ACTION, }); setIsEligibilityModalVisible(true); return; @@ -787,10 +787,10 @@ const PerpsMarketDetailsView: React.FC = () => { // Geo-restriction check for add/remove margin action if (!isEligible) { track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.ADJUST_MARGIN_ACTION, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.ADJUST_MARGIN_ACTION, }); setIsEligibilityModalVisible(true); return; @@ -825,9 +825,9 @@ const PerpsMarketDetailsView: React.FC = () => { if (!market?.symbol) return; track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TAP, - [PerpsEventProperties.ASSET]: market.symbol, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TAP, + [PERPS_EVENT_PROPERTY.ASSET]: market.symbol, }); navigation.navigate(Routes.PERPS.ORDER_BOOK, { @@ -843,10 +843,10 @@ const PerpsMarketDetailsView: React.FC = () => { // Geo-restriction check for close position action if (!isEligible) { track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.CLOSE_POSITION_ACTION, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.CLOSE_POSITION_ACTION, }); setIsEligibilityModalVisible(true); return; @@ -862,10 +862,10 @@ const PerpsMarketDetailsView: React.FC = () => { // Geo-restriction check for modify position action if (!isEligible) { track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.MODIFY_POSITION_ACTION, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.MODIFY_POSITION_ACTION, }); setIsEligibilityModalVisible(true); return; @@ -881,10 +881,10 @@ const PerpsMarketDetailsView: React.FC = () => { // Geo-restriction check for add margin from banner if (!isEligible) { track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.STOP_LOSS_PROMPT_ADD_MARGIN, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.STOP_LOSS_PROMPT_ADD_MARGIN, }); setIsEligibilityModalVisible(true); return; @@ -898,11 +898,11 @@ const PerpsMarketDetailsView: React.FC = () => { // Track the interaction - use ADD_MARGIN interaction type for banner clicks track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.ADD_MARGIN, - [PerpsEventProperties.ASSET]: existingPosition.symbol, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.STOP_LOSS_PROMPT_BANNER, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.ADD_MARGIN, + [PERPS_EVENT_PROPERTY.ASSET]: existingPosition.symbol, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.STOP_LOSS_PROMPT_BANNER, }); }, [existingPosition, navigation, track, isEligible]); @@ -913,10 +913,10 @@ const PerpsMarketDetailsView: React.FC = () => { // Geo-restriction check for set stop loss from banner if (!isEligible) { track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.STOP_LOSS_PROMPT_SET_SL, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.STOP_LOSS_PROMPT_SET_SL, }); setIsEligibilityModalVisible(true); return; @@ -931,7 +931,8 @@ const PerpsMarketDetailsView: React.FC = () => { // Build tracking data const trackingData: TPSLTrackingData = { direction: parseFloat(existingPosition.size) >= 0 ? 'long' : 'short', - source: PerpsEventValues.RISK_MANAGEMENT_SOURCE.STOP_LOSS_PROMPT_BANNER, + source: + PERPS_EVENT_VALUE.RISK_MANAGEMENT_SOURCE.STOP_LOSS_PROMPT_BANNER, positionSize: Math.abs(parseFloat(existingPosition.size)), }; @@ -960,12 +961,12 @@ const PerpsMarketDetailsView: React.FC = () => { // Track the interaction - use STOP_LOSS_ONE_CLICK_PROMPT for one-click stop loss from banner track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.STOP_LOSS_ONE_CLICK_PROMPT, - [PerpsEventProperties.ASSET]: existingPosition.symbol, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.STOP_LOSS_PROMPT_BANNER, - [PerpsEventProperties.STOP_LOSS_PRICE]: suggestedStopLossPrice, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.STOP_LOSS_ONE_CLICK_PROMPT, + [PERPS_EVENT_PROPERTY.ASSET]: existingPosition.symbol, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.STOP_LOSS_PROMPT_BANNER, + [PERPS_EVENT_PROPERTY.STOP_LOSS_PRICE]: suggestedStopLossPrice, }); } catch (error) { Logger.error(ensureError(error), { @@ -1006,9 +1007,9 @@ const PerpsMarketDetailsView: React.FC = () => { // Track full screen chart interaction track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.FULL_SCREEN_CHART, - [PerpsEventProperties.ASSET]: market?.symbol || '', + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.FULL_SCREEN_CHART, + [PERPS_EVENT_PROPERTY.ASSET]: market?.symbol || '', }); }, [market?.symbol, track]); diff --git a/app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx b/app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx index 8c1207d5258..9195b03f74c 100644 --- a/app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx +++ b/app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx @@ -40,8 +40,8 @@ import { useRoute, RouteProp } from '@react-navigation/native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { TraceName } from '../../../../../util/trace'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; @@ -148,22 +148,22 @@ const PerpsMarketListView = ({ (category: MarketTypeFilter) => { // Track analytics for category changes const categoryMap: Record = { - crypto: PerpsEventValues.BUTTON_CLICKED.CRYPTO, - stocks: PerpsEventValues.BUTTON_CLICKED.STOCKS, - commodities: PerpsEventValues.BUTTON_CLICKED.COMMODITIES, - forex: PerpsEventValues.BUTTON_CLICKED.FOREX, - new: PerpsEventValues.BUTTON_CLICKED.NEW, + crypto: PERPS_EVENT_VALUE.BUTTON_CLICKED.CRYPTO, + stocks: PERPS_EVENT_VALUE.BUTTON_CLICKED.STOCKS, + commodities: PERPS_EVENT_VALUE.BUTTON_CLICKED.COMMODITIES, + forex: PERPS_EVENT_VALUE.BUTTON_CLICKED.FOREX, + new: PERPS_EVENT_VALUE.BUTTON_CLICKED.NEW, all: null, }; const targetCategory = categoryMap[category]; if (targetCategory) { track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.BUTTON_CLICKED, - [PerpsEventProperties.BUTTON_CLICKED]: targetCategory, - [PerpsEventProperties.BUTTON_LOCATION]: - PerpsEventValues.BUTTON_LOCATION.MARKET_LIST, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.BUTTON_CLICKED, + [PERPS_EVENT_PROPERTY.BUTTON_CLICKED]: targetCategory, + [PERPS_EVENT_PROPERTY.BUTTON_LOCATION]: + PERPS_EVENT_VALUE.BUTTON_LOCATION.MARKET_LIST, }); } setMarketTypeFilter(category); @@ -197,8 +197,8 @@ const PerpsMarketListView = ({ preSearchFilterRef.current = marketTypeFilter; // Track the event track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.SEARCH_CLICKED, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.SEARCH_CLICKED, }); } }, [ @@ -218,7 +218,7 @@ const PerpsMarketListView = ({ // Track markets screen viewed event const source = - route.params?.source || PerpsEventValues.SOURCE.MAIN_ACTION_BUTTON; + route.params?.source || PERPS_EVENT_VALUE.SOURCE.MAIN_ACTION_BUTTON; // Get perp balance status and provider info for tracking const { account: perpsAccount } = usePerpsLiveAccount({ throttleMs: 5000 }); @@ -235,15 +235,15 @@ const PerpsMarketListView = ({ eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, conditions: [filteredMarkets.length > 0], properties: { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.MARKET_LIST, - [PerpsEventProperties.SOURCE]: source, - [PerpsEventProperties.HAS_PERP_BALANCE]: hasPerpBalance, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.MARKET_LIST, + [PERPS_EVENT_PROPERTY.SOURCE]: source, + [PERPS_EVENT_PROPERTY.HAS_PERP_BALANCE]: hasPerpBalance, ...(buttonClicked && { - [PerpsEventProperties.BUTTON_CLICKED]: buttonClicked, + [PERPS_EVENT_PROPERTY.BUTTON_CLICKED]: buttonClicked, }), ...(buttonLocation && { - [PerpsEventProperties.BUTTON_LOCATION]: buttonLocation, + [PERPS_EVENT_PROPERTY.BUTTON_LOCATION]: buttonLocation, }), }, }); diff --git a/app/components/UI/Perps/Views/PerpsOrderBookView/PerpsOrderBookView.tsx b/app/components/UI/Perps/Views/PerpsOrderBookView/PerpsOrderBookView.tsx index a170292f743..90e685f007f 100644 --- a/app/components/UI/Perps/Views/PerpsOrderBookView/PerpsOrderBookView.tsx +++ b/app/components/UI/Perps/Views/PerpsOrderBookView/PerpsOrderBookView.tsx @@ -56,8 +56,8 @@ import PerpsOrderBookTable, { type UnitDisplay, } from '../../components/PerpsOrderBookTable'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { usePerpsMarkets, @@ -294,9 +294,9 @@ const PerpsOrderBookView: React.FC = ({ eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, conditions: [!!symbol, !!orderBook], properties: { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.ORDER_BOOK, - [PerpsEventProperties.ASSET]: symbol || '', + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.ORDER_BOOK, + [PERPS_EVENT_PROPERTY.ASSET]: symbol || '', }, }); @@ -330,9 +330,9 @@ const PerpsOrderBookView: React.FC = ({ setIsDepthBandSheetVisible(false); track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TAP, - [PerpsEventProperties.ASSET]: symbol || '', + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TAP, + [PERPS_EVENT_PROPERTY.ASSET]: symbol || '', }); }, [symbol, track, saveGrouping], @@ -362,9 +362,9 @@ const PerpsOrderBookView: React.FC = ({ setUnitDisplay(unit); track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TAP, - [PerpsEventProperties.ASSET]: symbol || '', + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TAP, + [PERPS_EVENT_PROPERTY.ASSET]: symbol || '', }); }, [symbol, track], @@ -375,23 +375,23 @@ const PerpsOrderBookView: React.FC = ({ // Geo-restriction check if (!isEligible) { track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.ORDER_BOOK_LONG_BUTTON, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.ORDER_BOOK_LONG_BUTTON, }); setIsEligibilityModalVisible(true); return; } track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TAP, - [PerpsEventProperties.ASSET]: symbol || '', - [PerpsEventProperties.DIRECTION]: PerpsEventValues.DIRECTION.LONG, - [PerpsEventProperties.SOURCE]: PerpsEventValues.SOURCE.PERP_ASSET_SCREEN, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TAP, + [PERPS_EVENT_PROPERTY.ASSET]: symbol || '', + [PERPS_EVENT_PROPERTY.DIRECTION]: PERPS_EVENT_VALUE.DIRECTION.LONG, + [PERPS_EVENT_PROPERTY.SOURCE]: PERPS_EVENT_VALUE.SOURCE.PERP_ASSET_SCREEN, ...(isButtonColorTestEnabled && { - [PerpsEventProperties.AB_TEST_BUTTON_COLOR]: buttonColorVariant, + [PERPS_EVENT_PROPERTY.AB_TEST_BUTTON_COLOR]: buttonColorVariant, }), }); @@ -413,23 +413,23 @@ const PerpsOrderBookView: React.FC = ({ // Geo-restriction check if (!isEligible) { track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.ORDER_BOOK_SHORT_BUTTON, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.ORDER_BOOK_SHORT_BUTTON, }); setIsEligibilityModalVisible(true); return; } track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TAP, - [PerpsEventProperties.ASSET]: symbol || '', - [PerpsEventProperties.DIRECTION]: PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.SOURCE]: PerpsEventValues.SOURCE.PERP_ASSET_SCREEN, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TAP, + [PERPS_EVENT_PROPERTY.ASSET]: symbol || '', + [PERPS_EVENT_PROPERTY.DIRECTION]: PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.SOURCE]: PERPS_EVENT_VALUE.SOURCE.PERP_ASSET_SCREEN, ...(isButtonColorTestEnabled && { - [PerpsEventProperties.AB_TEST_BUTTON_COLOR]: buttonColorVariant, + [PERPS_EVENT_PROPERTY.AB_TEST_BUTTON_COLOR]: buttonColorVariant, }), }); @@ -453,10 +453,10 @@ const PerpsOrderBookView: React.FC = ({ // Geo-restriction check if (!isEligible) { track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.ORDER_BOOK_CLOSE_BUTTON, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.ORDER_BOOK_CLOSE_BUTTON, }); setIsEligibilityModalVisible(true); return; @@ -472,10 +472,10 @@ const PerpsOrderBookView: React.FC = ({ // Geo-restriction check if (!isEligible) { track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.ORDER_BOOK_MODIFY_BUTTON, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.ORDER_BOOK_MODIFY_BUTTON, }); setIsEligibilityModalVisible(true); return; diff --git a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx index c4e8308dae1..3baf906ac42 100644 --- a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx +++ b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx @@ -75,8 +75,8 @@ import PerpsOrderHeader from '../../components/PerpsOrderHeader'; import PerpsOrderTypeBottomSheet from '../../components/PerpsOrderTypeBottomSheet'; import PerpsSlider from '../../components/PerpsSlider'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { DECIMAL_PRECISION_CONFIG, @@ -377,14 +377,14 @@ const PerpsOrderViewContentBase: React.FC = ({ usePerpsEventTracking({ eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, properties: { - [PerpsEventProperties.SCREEN_TYPE]: PerpsEventValues.SCREEN_TYPE.TRADING, - [PerpsEventProperties.ASSET]: orderForm.asset, - [PerpsEventProperties.DIRECTION]: + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: PERPS_EVENT_VALUE.SCREEN_TYPE.TRADING, + [PERPS_EVENT_PROPERTY.ASSET]: orderForm.asset, + [PERPS_EVENT_PROPERTY.DIRECTION]: orderForm.direction === 'long' - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, ...(isButtonColorTestEnabled && { - [PerpsEventProperties.AB_TEST_BUTTON_COLOR]: buttonColorVariant, + [PERPS_EVENT_PROPERTY.AB_TEST_BUTTON_COLOR]: buttonColorVariant, }), }, }); @@ -456,18 +456,18 @@ const PerpsOrderViewContentBase: React.FC = ({ eventName: MetaMetricsEvents.PERPS_UI_INTERACTION, conditions: [!!(orderForm.amount && parseFloat(orderForm.amount) > 0)], properties: { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.ORDER_TYPE_VIEWED, - [PerpsEventProperties.ASSET]: orderForm.asset, - [PerpsEventProperties.DIRECTION]: + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.ORDER_TYPE_VIEWED, + [PERPS_EVENT_PROPERTY.ASSET]: orderForm.asset, + [PERPS_EVENT_PROPERTY.DIRECTION]: orderForm.direction === 'long' - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.ORDER_SIZE]: parseFloat(orderForm.amount || '0'), - [PerpsEventProperties.LEVERAGE_USED]: parseFloat( + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.ORDER_SIZE]: parseFloat(orderForm.amount || '0'), + [PERPS_EVENT_PROPERTY.LEVERAGE_USED]: parseFloat( String(orderForm.leverage), ), - [PerpsEventProperties.ORDER_TYPE]: orderForm.type, + [PERPS_EVENT_PROPERTY.ORDER_TYPE]: orderForm.type, }, }); @@ -841,14 +841,14 @@ const PerpsOrderViewContentBase: React.FC = ({ // Track Place Order button press with A/B test context if (isButtonColorTestEnabled) { track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TAP, - [PerpsEventProperties.ASSET]: orderForm.asset, - [PerpsEventProperties.DIRECTION]: + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TAP, + [PERPS_EVENT_PROPERTY.ASSET]: orderForm.asset, + [PERPS_EVENT_PROPERTY.DIRECTION]: orderForm.direction === 'long' - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.AB_TEST_BUTTON_COLOR]: buttonColorVariant, + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.AB_TEST_BUTTON_COLOR]: buttonColorVariant, }); } @@ -864,13 +864,13 @@ const PerpsOrderViewContentBase: React.FC = ({ // Track validation failure as error encountered track(MetaMetricsEvents.PERPS_ERROR, { - [PerpsEventProperties.ERROR_TYPE]: - PerpsEventValues.ERROR_TYPE.VALIDATION, - [PerpsEventProperties.ERROR_MESSAGE]: firstError, - [PerpsEventProperties.SCREEN_NAME]: - PerpsEventValues.SCREEN_NAME.PERPS_ORDER, - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.TRADING, + [PERPS_EVENT_PROPERTY.ERROR_TYPE]: + PERPS_EVENT_VALUE.ERROR_TYPE.VALIDATION, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: firstError, + [PERPS_EVENT_PROPERTY.SCREEN_NAME]: + PERPS_EVENT_VALUE.SCREEN_NAME.PERPS_ORDER, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.TRADING, }); isSubmittingRef.current = false; // Reset flag on early return @@ -884,14 +884,14 @@ const PerpsOrderViewContentBase: React.FC = ({ }); track(MetaMetricsEvents.PERPS_ERROR, { - [PerpsEventProperties.ERROR_TYPE]: - PerpsEventValues.ERROR_TYPE.VALIDATION, - [PerpsEventProperties.ERROR_MESSAGE]: + [PERPS_EVENT_PROPERTY.ERROR_TYPE]: + PERPS_EVENT_VALUE.ERROR_TYPE.VALIDATION, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: 'Cross margin position detected', - [PerpsEventProperties.SCREEN_NAME]: - PerpsEventValues.SCREEN_NAME.PERPS_ORDER, - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.TRADING, + [PERPS_EVENT_PROPERTY.SCREEN_NAME]: + PERPS_EVENT_VALUE.SCREEN_NAME.PERPS_ORDER, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.TRADING, }); isSubmittingRef.current = false; @@ -1607,29 +1607,29 @@ const PerpsOrderViewContentBase: React.FC = ({ // Track leverage change (consolidated here to avoid duplicate tracking) const eventProperties: Record = { - [PerpsEventProperties.ASSET]: orderForm.asset, - [PerpsEventProperties.DIRECTION]: + [PERPS_EVENT_PROPERTY.ASSET]: orderForm.asset, + [PERPS_EVENT_PROPERTY.DIRECTION]: orderForm.direction === 'long' - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.LEVERAGE_USED]: leverage, + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.LEVERAGE_USED]: leverage, previousLeverage: orderForm.leverage, }; // Add input method if provided if (inputMethod) { - eventProperties[PerpsEventProperties.INPUT_METHOD] = + eventProperties[PERPS_EVENT_PROPERTY.INPUT_METHOD] = inputMethod === 'slider' - ? PerpsEventValues.INPUT_METHOD.SLIDER - : PerpsEventValues.INPUT_METHOD.PRESET; + ? PERPS_EVENT_VALUE.INPUT_METHOD.SLIDER + : PERPS_EVENT_VALUE.INPUT_METHOD.PRESET; } track(MetaMetricsEvents.PERPS_UI_INTERACTION, { ...eventProperties, - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.LEVERAGE_CHANGED, - [PerpsEventProperties.SETTING_TYPE]: - PerpsEventValues.SETTING_TYPE.LEVERAGE, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.LEVERAGE_CHANGED, + [PERPS_EVENT_PROPERTY.SETTING_TYPE]: + PERPS_EVENT_VALUE.SETTING_TYPE.LEVERAGE, }); }} leverage={orderForm.leverage} diff --git a/app/components/UI/Perps/Views/PerpsSelectAdjustMarginActionView/PerpsSelectAdjustMarginActionView.tsx b/app/components/UI/Perps/Views/PerpsSelectAdjustMarginActionView/PerpsSelectAdjustMarginActionView.tsx index 1b2ca3e75af..122d43b8aa7 100644 --- a/app/components/UI/Perps/Views/PerpsSelectAdjustMarginActionView/PerpsSelectAdjustMarginActionView.tsx +++ b/app/components/UI/Perps/Views/PerpsSelectAdjustMarginActionView/PerpsSelectAdjustMarginActionView.tsx @@ -14,8 +14,8 @@ import { usePerpsNavigation } from '../../hooks/usePerpsNavigation'; import { BottomSheetRef } from '../../../../../component-library/components/BottomSheets/BottomSheet'; import { useMetrics, MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; interface PerpsSelectAdjustMarginActionViewProps { @@ -50,17 +50,17 @@ const PerpsSelectAdjustMarginActionView: React.FC< // Track UI interaction for add/remove margin selection const interactionType = { - add_margin: PerpsEventValues.INTERACTION_TYPE.ADD_MARGIN, - reduce_margin: PerpsEventValues.INTERACTION_TYPE.REMOVE_MARGIN, + add_margin: PERPS_EVENT_VALUE.INTERACTION_TYPE.ADD_MARGIN, + reduce_margin: PERPS_EVENT_VALUE.INTERACTION_TYPE.REMOVE_MARGIN, }[action]; trackEvent( createEventBuilder(MetaMetricsEvents.PERPS_UI_INTERACTION) .addProperties({ - [PerpsEventProperties.INTERACTION_TYPE]: interactionType, - [PerpsEventProperties.ASSET]: position.symbol, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.POSITION_SCREEN, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: interactionType, + [PERPS_EVENT_PROPERTY.ASSET]: position.symbol, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.POSITION_SCREEN, }) .build(), ); diff --git a/app/components/UI/Perps/Views/PerpsSelectModifyActionView/PerpsSelectModifyActionView.tsx b/app/components/UI/Perps/Views/PerpsSelectModifyActionView/PerpsSelectModifyActionView.tsx index 63c2d5ac6d5..a0b44d69ab8 100644 --- a/app/components/UI/Perps/Views/PerpsSelectModifyActionView/PerpsSelectModifyActionView.tsx +++ b/app/components/UI/Perps/Views/PerpsSelectModifyActionView/PerpsSelectModifyActionView.tsx @@ -14,8 +14,8 @@ import { usePerpsNavigation } from '../../hooks/usePerpsNavigation'; import { BottomSheetRef } from '../../../../../component-library/components/BottomSheets/BottomSheet'; import { useMetrics, MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; interface PerpsSelectModifyActionViewProps { @@ -52,11 +52,11 @@ const PerpsSelectModifyActionView: React.FC< const getInteractionType = () => { switch (action) { case 'add_to_position': - return PerpsEventValues.INTERACTION_TYPE.INCREASE_EXPOSURE; + return PERPS_EVENT_VALUE.INTERACTION_TYPE.INCREASE_EXPOSURE; case 'reduce_position': - return PerpsEventValues.INTERACTION_TYPE.REDUCE_EXPOSURE; + return PERPS_EVENT_VALUE.INTERACTION_TYPE.REDUCE_EXPOSURE; case 'flip_position': - return PerpsEventValues.INTERACTION_TYPE.FLIP_POSITION; + return PERPS_EVENT_VALUE.INTERACTION_TYPE.FLIP_POSITION; default: return null; } @@ -67,14 +67,14 @@ const PerpsSelectModifyActionView: React.FC< trackEvent( createEventBuilder(MetaMetricsEvents.PERPS_UI_INTERACTION) .addProperties({ - [PerpsEventProperties.INTERACTION_TYPE]: interactionType, - [PerpsEventProperties.ASSET]: position.symbol, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.POSITION_SCREEN, - [PerpsEventProperties.DIRECTION]: + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: interactionType, + [PERPS_EVENT_PROPERTY.ASSET]: position.symbol, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.POSITION_SCREEN, + [PERPS_EVENT_PROPERTY.DIRECTION]: parseFloat(position.size) > 0 - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, }) .build(), ); diff --git a/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx index 898a15ad429..426b64ceb6d 100644 --- a/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render, screen, fireEvent, act } from '@testing-library/react-native'; import PerpsTPSLView from './PerpsTPSLView'; import type { Position } from '../../controllers/types'; -import { PerpsEventValues } from '../../constants/eventNames'; +import { PERPS_EVENT_VALUE } from '../../constants/eventNames'; // Mock dependencies jest.mock('react-native-reanimated', () => @@ -499,7 +499,7 @@ describe('PerpsTPSLView', () => { expect(mockOnConfirm).toHaveBeenCalledWith('3150.00', '2850.00', { direction: 'long', - source: PerpsEventValues.RISK_MANAGEMENT_SOURCE.TRADE_SCREEN, + source: PERPS_EVENT_VALUE.RISK_MANAGEMENT_SOURCE.TRADE_SCREEN, positionSize: 0, takeProfitPercentage: undefined, stopLossPercentage: undefined, @@ -520,7 +520,7 @@ describe('PerpsTPSLView', () => { expect(mockOnConfirm).toHaveBeenCalledWith(undefined, undefined, { direction: 'long', - source: PerpsEventValues.RISK_MANAGEMENT_SOURCE.TRADE_SCREEN, + source: PERPS_EVENT_VALUE.RISK_MANAGEMENT_SOURCE.TRADE_SCREEN, positionSize: 0, takeProfitPercentage: undefined, stopLossPercentage: undefined, diff --git a/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx index 3c524ceabcc..a75fee97f22 100644 --- a/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx +++ b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx @@ -30,8 +30,8 @@ import { useTheme } from '../../../../../util/theme'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { usePerpsLivePrices } from '../../hooks/stream'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; @@ -212,21 +212,21 @@ const PerpsTPSLView: React.FC = () => { // Determine if this is create (new order) or edit (existing position) TP/SL const isEditingExistingPosition = !!position; const tpslScreenType = isEditingExistingPosition - ? PerpsEventValues.SCREEN_TYPE.EDIT_TPSL - : PerpsEventValues.SCREEN_TYPE.CREATE_TPSL; + ? PERPS_EVENT_VALUE.SCREEN_TYPE.EDIT_TPSL + : PERPS_EVENT_VALUE.SCREEN_TYPE.CREATE_TPSL; usePerpsEventTracking({ eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, properties: { - [PerpsEventProperties.SCREEN_TYPE]: tpslScreenType, - [PerpsEventProperties.ASSET]: asset, - [PerpsEventProperties.DIRECTION]: + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: tpslScreenType, + [PERPS_EVENT_PROPERTY.ASSET]: asset, + [PERPS_EVENT_PROPERTY.DIRECTION]: actualDirection === 'long' - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, // Add initial TP/SL state to understand what user already has set - [PerpsEventProperties.HAS_TAKE_PROFIT]: !!initialTakeProfitPrice, - [PerpsEventProperties.HAS_STOP_LOSS]: !!initialStopLossPrice, + [PERPS_EVENT_PROPERTY.HAS_TAKE_PROFIT]: !!initialTakeProfitPrice, + [PERPS_EVENT_PROPERTY.HAS_STOP_LOSS]: !!initialStopLossPrice, }, }); @@ -370,8 +370,8 @@ const PerpsTPSLView: React.FC = () => { const trackingData = { direction: actualDirection, source: isEditingExistingPosition - ? PerpsEventValues.RISK_MANAGEMENT_SOURCE.POSITION_SCREEN - : PerpsEventValues.RISK_MANAGEMENT_SOURCE.TRADE_SCREEN, + ? PERPS_EVENT_VALUE.RISK_MANAGEMENT_SOURCE.POSITION_SCREEN + : PERPS_EVENT_VALUE.RISK_MANAGEMENT_SOURCE.TRADE_SCREEN, positionSize: position?.size ? Math.abs(parseFloat(position.size)) : 0, takeProfitPercentage: formattedTakeProfitPercentage ? parseFloat(formattedTakeProfitPercentage.replace('%', '')) diff --git a/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.test.tsx b/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.test.tsx index 06b0b167844..ffe7ce9835a 100644 --- a/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.test.tsx @@ -11,7 +11,7 @@ import Routes from '../../../../../constants/navigation/Routes'; import { strings } from '../../../../../../locales/i18n'; import type { Position, PerpsMarketData } from '../../controllers/types'; import PerpsTabView from './PerpsTabView'; -import { PerpsEventValues } from '../../constants/eventNames'; +import { PERPS_EVENT_VALUE } from '../../constants/eventNames'; jest.mock('@react-navigation/native', () => ({ useNavigation: jest.fn(), @@ -485,7 +485,7 @@ describe('PerpsTabView', () => { expect(mockNavigation.navigate).toHaveBeenCalledWith(Routes.PERPS.ROOT, { screen: Routes.PERPS.PERPS_HOME, - params: { source: PerpsEventValues.SOURCE.POSITION_TAB }, + params: { source: PERPS_EVENT_VALUE.SOURCE.POSITION_TAB }, }); }); @@ -633,7 +633,7 @@ describe('PerpsTabView', () => { expect(mockNavigation.navigate).toHaveBeenCalledWith(Routes.PERPS.ROOT, { screen: Routes.PERPS.PERPS_HOME, - params: { source: PerpsEventValues.SOURCE.HOMESCREEN_TAB }, + params: { source: PERPS_EVENT_VALUE.SOURCE.HOMESCREEN_TAB }, }); }); }); diff --git a/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.tsx b/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.tsx index 3464a395910..b07e8c788f0 100644 --- a/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.tsx +++ b/app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.tsx @@ -28,8 +28,8 @@ import { useSelector } from 'react-redux'; import { selectHomepageRedesignV1Enabled } from '../../../../../selectors/featureFlagController/homepage'; import { selectPerpsEligibility } from '../../selectors/perpsController'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import type { PerpsNavigationParamList, @@ -125,16 +125,16 @@ const PerpsTabView = () => { account?.totalBalance !== undefined, ], properties: { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.WALLET_HOME_PERPS_TAB, - [PerpsEventProperties.OPEN_POSITION]: positions?.length || 0, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.WALLET_HOME_PERPS_TAB, + [PERPS_EVENT_PROPERTY.OPEN_POSITION]: positions?.length || 0, }, }); const handleManageBalancePress = useCallback(() => { navigation.navigate(Routes.PERPS.ROOT, { screen: Routes.PERPS.PERPS_HOME, - params: { source: PerpsEventValues.SOURCE.HOMESCREEN_TAB }, + params: { source: PERPS_EVENT_VALUE.SOURCE.HOMESCREEN_TAB }, }); }, [navigation]); @@ -146,7 +146,7 @@ const PerpsTabView = () => { // Navigate to trading view for returning users navigation.navigate(Routes.PERPS.ROOT, { screen: Routes.PERPS.PERPS_HOME, - params: { source: PerpsEventValues.SOURCE.POSITION_TAB }, + params: { source: PERPS_EVENT_VALUE.SOURCE.POSITION_TAB }, }); } }, [navigation, isFirstTimeUser]); @@ -156,10 +156,10 @@ const PerpsTabView = () => { // Geo-restriction check for close all positions if (!isEligible) { track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.CLOSE_ALL_POSITIONS_BUTTON, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.CLOSE_ALL_POSITIONS_BUTTON, }); setIsEligibilityModalVisible(true); return; @@ -219,7 +219,7 @@ const PerpsTabView = () => { ))} {(!positions || positions.length === 0) && renderStartTradeCTA()} @@ -267,7 +267,7 @@ const PerpsTabView = () => { ); @@ -292,7 +292,7 @@ const PerpsTabView = () => { const handleSeeAllPerps = useCallback(() => { navigation.navigate(Routes.PERPS.ROOT, { screen: Routes.PERPS.PERPS_HOME, - params: { source: PerpsEventValues.SOURCE.HOMESCREEN_TAB }, + params: { source: PERPS_EVENT_VALUE.SOURCE.HOMESCREEN_TAB }, }); }, [navigation]); diff --git a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.test.tsx b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.test.tsx index 7261cb500a9..2a8867f08a2 100644 --- a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.test.tsx @@ -11,7 +11,7 @@ import { backgroundState } from '../../../../../util/test/initial-root-state'; import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../../../util/test/accountsControllerTestUtils'; import { RootState } from '../../../../../reducers'; import Routes from '../../../../../constants/navigation/Routes'; -import { PerpsEventValues } from '../../constants/eventNames'; +import { PERPS_EVENT_VALUE } from '../../constants/eventNames'; const mockNavigate = jest.fn(); const mockSetOptions = jest.fn(); @@ -642,7 +642,7 @@ describe('PerpsPositionTransactionView', () => { screen: Routes.PERPS.MARKET_DETAILS, params: { market: { symbol: 'ETH', name: 'ETH' }, - source: PerpsEventValues.SOURCE.TRADE_DETAILS, + source: PERPS_EVENT_VALUE.SOURCE.TRADE_DETAILS, }, }); }); diff --git a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.tsx b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.tsx index 6c99d0b72af..7ce3f8e64b2 100644 --- a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.tsx +++ b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.tsx @@ -38,7 +38,7 @@ import { PRICE_RANGES_UNIVERSAL, } from '../../utils/formatUtils'; import { styleSheet } from './PerpsPositionTransactionView.styles'; -import { PerpsEventValues } from '../../constants/eventNames'; +import { PERPS_EVENT_VALUE } from '../../constants/eventNames'; const PerpsPositionTransactionView: React.FC = () => { const { styles } = useStyles(styleSheet, {}); @@ -104,7 +104,7 @@ const PerpsPositionTransactionView: React.FC = () => { screen: Routes.PERPS.MARKET_DETAILS, params: { market, - source: PerpsEventValues.SOURCE.TRADE_DETAILS, + source: PERPS_EVENT_VALUE.SOURCE.TRADE_DETAILS, }, }); }; diff --git a/app/components/UI/Perps/Views/PerpsWithdrawView/PerpsWithdrawView.tsx b/app/components/UI/Perps/Views/PerpsWithdrawView/PerpsWithdrawView.tsx index 0acfd845605..35b1df41d3c 100644 --- a/app/components/UI/Perps/Views/PerpsWithdrawView/PerpsWithdrawView.tsx +++ b/app/components/UI/Perps/Views/PerpsWithdrawView/PerpsWithdrawView.tsx @@ -41,8 +41,8 @@ import { USDC_TOKEN_ICON_URL, } from '../../constants/hyperLiquidConfig'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { usePerpsMeasurement, @@ -162,8 +162,8 @@ const PerpsWithdrawView: React.FC = () => { usePerpsEventTracking({ eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, properties: { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.WITHDRAWAL, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.WITHDRAWAL, }, }); diff --git a/app/components/UI/Perps/__mocks__/serviceMocks.ts b/app/components/UI/Perps/__mocks__/serviceMocks.ts index 6a24144a317..c79b082a4d5 100644 --- a/app/components/UI/Perps/__mocks__/serviceMocks.ts +++ b/app/components/UI/Perps/__mocks__/serviceMocks.ts @@ -7,6 +7,7 @@ import type { ServiceContext } from '../controllers/services/ServiceContext'; import type { PerpsControllerState, InitializationState, + PerpsControllerMessenger, } from '../controllers/PerpsController'; import type { PerpsPlatformDependencies } from '../controllers/types'; @@ -49,44 +50,12 @@ export const createMockInfrastructure = streamManager: { pauseChannel: jest.fn(), resumeChannel: jest.fn(), + clearAllChannels: jest.fn(), }, - // === Controller Access (ALL controllers consolidated) === - controllers: { - // Account operations (wraps AccountsController) - accounts: { - getSelectedEvmAccount: jest.fn(() => ({ - address: '0x1234567890abcdef1234567890abcdef12345678', - })), - formatAccountToCaipId: jest.fn( - (address: string, chainId: string) => - `eip155:${chainId}:${address}`, - ), - }, - // Keyring operations (wraps KeyringController) - keyring: { - signTypedMessage: jest.fn().mockResolvedValue('0xSignatureResult'), - }, - // Network operations (wraps NetworkController) - network: { - getChainIdForNetwork: jest.fn().mockReturnValue('0x1'), - findNetworkClientIdForChain: jest.fn().mockReturnValue('mainnet'), - }, - // Transaction operations (wraps TransactionController) - transaction: { - submit: jest.fn().mockResolvedValue({ - result: Promise.resolve('0xTransactionHash'), - transactionMeta: { id: 'tx-id-123', hash: '0xTransactionHash' }, - }), - }, - // Rewards operations (wraps RewardsController, optional) - rewards: { - getFeeDiscount: jest.fn().mockResolvedValue(0), - }, - // Authentication operations (wraps AuthenticationController) - authentication: { - getBearerToken: jest.fn().mockResolvedValue('mock-bearer-token'), - }, + // === Rewards (no standard messenger action in core) === + rewards: { + getFeeDiscount: jest.fn().mockResolvedValue(0), }, }) as unknown as jest.Mocked; @@ -181,3 +150,59 @@ export const createMockEvmAccount = () => ({ keyring: { type: 'HD Key Tree' }, }, }); + +/** + * Create a mock PerpsControllerMessenger for testing inter-controller communication. + * The messenger.call() method should be configured in each test to return appropriate values. + * + * Common messenger actions used: + * - 'AccountTreeController:getAccountsFromSelectedAccountGroup' - returns array of accounts + * - 'KeyringController:signTypedMessage' - returns signature string + * - 'NetworkController:getState' - returns { selectedNetworkClientId: string } + * - 'NetworkController:getNetworkClientById' - returns { configuration: { chainId: string } } + * - 'AuthenticationController:getBearerToken' - returns bearer token string + * + * @param overrides - Optional partial messenger to override default behavior + */ +export const createMockMessenger = ( + overrides?: Partial, +): jest.Mocked => { + const mockEvmAccount = createMockEvmAccount(); + const base = { + call: jest.fn().mockImplementation((action: string) => { + // Default implementations for common actions + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [mockEvmAccount]; + } + if (action === 'KeyringController:signTypedMessage') { + return Promise.resolve('0xSignatureResult'); + } + if (action === 'NetworkController:getState') { + return { selectedNetworkClientId: 'mainnet' }; + } + if (action === 'NetworkController:getNetworkClientById') { + return { configuration: { chainId: '0x1' } }; + } + if (action === 'AuthenticationController:getBearerToken') { + return Promise.resolve('mock-bearer-token'); + } + return undefined; + }), + publish: jest.fn(), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + registerActionHandler: jest.fn(), + unregisterActionHandler: jest.fn(), + // Additional methods used by PerpsController + registerEventHandler: jest.fn(), + registerInitialEventPayload: jest.fn(), + unregisterEventHandler: jest.fn(), + clearEventSubscriptions: jest.fn(), + }; + return { + ...base, + ...overrides, + } as unknown as jest.Mocked; +}; diff --git a/app/components/UI/Perps/adapters/mobileInfrastructure.ts b/app/components/UI/Perps/adapters/mobileInfrastructure.ts index 45f501afe8c..862cd46dd6a 100644 --- a/app/components/UI/Perps/adapters/mobileInfrastructure.ts +++ b/app/components/UI/Perps/adapters/mobileInfrastructure.ts @@ -1,13 +1,8 @@ /** * Mobile Infrastructure Adapter * - * Provides platform-specific implementations of PerpsPlatformDependencies interfaces - * for the mobile app. This adapter wraps existing mobile utilities to allow - * PerpsController and its services to remain platform-agnostic. - * - * When migrating to core monorepo: - * - This file stays in the mobile app - * - Core will have its own adapter or mock implementations + * Provides mobile-specific implementations of PerpsPlatformDependencies. + * Controller access uses messenger pattern (messenger.call()). */ import Logger from '../../../../util/Logger'; @@ -19,18 +14,10 @@ import { trace, endTrace, TraceName } from '../../../../util/trace'; import { setMeasurement } from '@sentry/react-native'; import performance from 'react-native-performance'; import { getStreamManagerInstance } from '../providers/PerpsStreamManager'; -import { findEvmAccount } from '../utils/accountUtils'; -import { formatAccountToCaipAccountId } from '../utils/rewardsUtils'; import Engine from '../../../../core/Engine'; -import { - SignTypedDataVersion, - type TypedMessageParams, -} from '@metamask/keyring-controller'; -import { TransactionType } from '@metamask/transaction-controller'; import type { PerpsPlatformDependencies, PerpsMetrics, - PerpsControllerAccess, PerpsTraceName, PerpsTraceValue, PerpsAnalyticsEvent, @@ -142,12 +129,8 @@ function createStreamManagerAdapter() { } /** - * Creates mobile-specific platform dependencies for PerpsController - * - * This function wraps all mobile-specific dependencies into a single - * dependencies object that can be injected into PerpsController. - * - * @returns PerpsPlatformDependencies - All platform dependencies bundled + * Creates mobile-specific platform dependencies for PerpsController. + * Controller access uses messenger pattern (messenger.call()). */ export function createMobileInfrastructure(): PerpsPlatformDependencies { return { @@ -211,106 +194,12 @@ export function createMobileInfrastructure(): PerpsPlatformDependencies { // === Platform Services === streamManager: createStreamManagerAdapter(), - // === Controller Access (ALL controllers consolidated) === - controllers: createControllerAccessAdapter(), - }; -} - -/** - * Creates a consolidated controller access adapter that wraps Engine.context controllers. - * This unified adapter provides ALL controller dependencies in one place. - * - * Architecture: - * - accounts: AccountsController access (selected account, CAIP formatting) - * - keyring: KeyringController (signing operations) - * - network: NetworkController (chain ID lookups) - * - transaction: TransactionController (TX submission) - * - rewards: RewardsController (fee discounts, optional) - * - * Benefits: - * 1. Clear separation - all controller access via deps.controllers.* - * 2. Consistent pattern - utilities flat, controllers grouped - * 3. Mockable - test can mock entire controllers object - * 4. Future-proof - add new controller access without bloating top-level - */ -function createControllerAccessAdapter(): PerpsControllerAccess { - return { - // === Account Operations (wraps AccountsController) === - accounts: { - getSelectedEvmAccount: () => { - // Inline implementation (was getEvmAccountFromSelectedAccountGroup) - // Uses pure findEvmAccount from accountUtils + Engine access - const { AccountTreeController } = Engine.context; - const accounts = - AccountTreeController.getAccountsFromSelectedAccountGroup(); - return findEvmAccount(accounts) ?? undefined; - }, - formatAccountToCaipId: (address, chainId) => - formatAccountToCaipAccountId(address, chainId), - }, - - // === Keyring Operations (wraps KeyringController) === - keyring: { - signTypedMessage: async (msgParams, version) => { - // Map version string to SignTypedDataVersion enum - let versionEnum: SignTypedDataVersion; - switch (version) { - case 'V4': - versionEnum = SignTypedDataVersion.V4; - break; - case 'V3': - versionEnum = SignTypedDataVersion.V3; - break; - default: - versionEnum = SignTypedDataVersion.V1; - } - - return Engine.context.KeyringController.signTypedMessage( - msgParams as TypedMessageParams, - versionEnum, - ); - }, - }, - - // === Network Operations (wraps NetworkController) === - network: { - getChainIdForNetwork: (networkClientId) => { - const client = - Engine.context.NetworkController.getNetworkClientById( - networkClientId, - ); - return client.configuration.chainId; - }, - findNetworkClientIdForChain: (chainId) => - Engine.context.NetworkController.findNetworkClientIdByChainId(chainId), - getSelectedNetworkClientId: () => - Engine.context.NetworkController.state.selectedNetworkClientId, - }, - - // === Transaction Operations (wraps TransactionController) === - transaction: { - submit: (txParams, options) => - Engine.context.TransactionController.addTransaction(txParams, { - ...options, - // Bridge string type to TransactionType enum for actual controller - type: options.type as TransactionType | undefined, - }), - }, - - // === Rewards Operations (wraps RewardsController) === - // Provides fee discount capabilities for MetaMask rewards program + // === Rewards === rewards: { getFeeDiscount: (caipAccountId: `${string}:${string}:${string}`) => Engine.context.RewardsController.getPerpsDiscountForAccount( caipAccountId, ), }, - - // === Authentication Operations (wraps AuthenticationController) === - // Provides bearer token access for authenticated API calls (e.g., Data Lake) - authentication: { - getBearerToken: () => - Engine.context.AuthenticationController.getBearerToken(), - }, }; } diff --git a/app/components/UI/Perps/components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.tsx b/app/components/UI/Perps/components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.tsx index 77d2c11e716..d3726c56f37 100644 --- a/app/components/UI/Perps/components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.tsx +++ b/app/components/UI/Perps/components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.tsx @@ -23,8 +23,8 @@ import createStyles from './PerpsBottomSheetTooltip.styles'; import { tooltipContentRegistry } from './content/contentRegistry'; import { PerpsBottomSheetTooltipSelectorsIDs } from '../../Perps.testIds'; import { - PerpsEventValues, - PerpsEventProperties, + PERPS_EVENT_VALUE, + PERPS_EVENT_PROPERTY, } from '../../constants/eventNames'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; import { MetaMetricsEvents } from '../../../../../core/Analytics/MetaMetrics.events'; @@ -98,12 +98,12 @@ const PerpsBottomSheetTooltip = React.memo( const handleGotItPress = useCallback(() => { // Track tooltip button click track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.BUTTON_CLICKED, - [PerpsEventProperties.BUTTON_CLICKED]: - PerpsEventValues.BUTTON_CLICKED.TOOLTIP, - [PerpsEventProperties.BUTTON_LOCATION]: - PerpsEventValues.BUTTON_LOCATION.TOOLTIP, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.BUTTON_CLICKED, + [PERPS_EVENT_PROPERTY.BUTTON_CLICKED]: + PERPS_EVENT_VALUE.BUTTON_CLICKED.TOOLTIP, + [PERPS_EVENT_PROPERTY.BUTTON_LOCATION]: + PERPS_EVENT_VALUE.BUTTON_LOCATION.TOOLTIP, }); handleClose(); }, [track, handleClose]); diff --git a/app/components/UI/Perps/components/PerpsCandlePeriodBottomSheet/PerpsCandlePeriodBottomSheet.tsx b/app/components/UI/Perps/components/PerpsCandlePeriodBottomSheet/PerpsCandlePeriodBottomSheet.tsx index cc9092d9258..6cd6e1cf157 100644 --- a/app/components/UI/Perps/components/PerpsCandlePeriodBottomSheet/PerpsCandlePeriodBottomSheet.tsx +++ b/app/components/UI/Perps/components/PerpsCandlePeriodBottomSheet/PerpsCandlePeriodBottomSheet.tsx @@ -22,8 +22,8 @@ import styleSheet from './PerpsCandlePeriodBottomSheet.styles'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; interface PerpsCandlePeriodBottomSheetProps { @@ -58,12 +58,12 @@ const PerpsCandlePeriodBottomSheet: React.FC< if (isVisible) { // Track candle periods bottom sheet viewed when it becomes visible track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.CANDLE_PERIOD_VIEWED, - [PerpsEventProperties.ASSET]: asset || '', - [PerpsEventProperties.CANDLE_PERIOD]: selectedPeriod, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.PERP_ASSET_SCREEN, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.CANDLE_PERIOD_VIEWED, + [PERPS_EVENT_PROPERTY.ASSET]: asset || '', + [PERPS_EVENT_PROPERTY.CANDLE_PERIOD]: selectedPeriod, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.PERP_ASSET_SCREEN, }); bottomSheetRef.current?.onOpenBottomSheet(); diff --git a/app/components/UI/Perps/components/PerpsCard/PerpsCard.tsx b/app/components/UI/Perps/components/PerpsCard/PerpsCard.tsx index dddd64d1a8a..3fb5898f64d 100644 --- a/app/components/UI/Perps/components/PerpsCard/PerpsCard.tsx +++ b/app/components/UI/Perps/components/PerpsCard/PerpsCard.tsx @@ -22,8 +22,8 @@ import styleSheet from './PerpsCard.styles'; import type { PerpsCardProps } from './PerpsCard.types'; import { HOME_SCREEN_CONFIG } from '../../constants/perpsConfig'; import { - PerpsEventValues, - PerpsEventProperties, + PERPS_EVENT_VALUE, + PERPS_EVENT_PROPERTY, } from '../../constants/eventNames'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; import { MetaMetricsEvents } from '../../../../../core/Analytics/MetaMetrics.events'; @@ -99,16 +99,16 @@ const PerpsCard: React.FC = ({ if (position) { // Map source to button_location const buttonLocation = - source === PerpsEventValues.SOURCE.POSITION_TAB - ? PerpsEventValues.BUTTON_LOCATION.PERPS_TAB - : PerpsEventValues.BUTTON_LOCATION.PERPS_HOME; + source === PERPS_EVENT_VALUE.SOURCE.POSITION_TAB + ? PERPS_EVENT_VALUE.BUTTON_LOCATION.PERPS_TAB + : PERPS_EVENT_VALUE.BUTTON_LOCATION.PERPS_HOME; track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.BUTTON_CLICKED, - [PerpsEventProperties.BUTTON_CLICKED]: - PerpsEventValues.BUTTON_CLICKED.OPEN_POSITION, - [PerpsEventProperties.BUTTON_LOCATION]: buttonLocation, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.BUTTON_CLICKED, + [PERPS_EVENT_PROPERTY.BUTTON_CLICKED]: + PERPS_EVENT_VALUE.BUTTON_CLICKED.OPEN_POSITION, + [PERPS_EVENT_PROPERTY.BUTTON_LOCATION]: buttonLocation, }); } diff --git a/app/components/UI/Perps/components/PerpsConnectionErrorView/PerpsConnectionErrorView.tsx b/app/components/UI/Perps/components/PerpsConnectionErrorView/PerpsConnectionErrorView.tsx index db78100f87a..82e9844bd40 100644 --- a/app/components/UI/Perps/components/PerpsConnectionErrorView/PerpsConnectionErrorView.tsx +++ b/app/components/UI/Perps/components/PerpsConnectionErrorView/PerpsConnectionErrorView.tsx @@ -21,8 +21,8 @@ import Routes from '../../../../../constants/navigation/Routes'; import { MetaMetricsEvents } from '../../../../../core/Analytics'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import styleSheet from './PerpsConnectionErrorView.styles'; @@ -49,7 +49,7 @@ const PerpsConnectionErrorView: React.FC = ({ usePerpsEventTracking({ eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, properties: { - [PerpsEventProperties.SCREEN_TYPE]: PerpsEventValues.SCREEN_TYPE.ERROR, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: PERPS_EVENT_VALUE.SCREEN_TYPE.ERROR, }, }); @@ -117,11 +117,11 @@ const PerpsConnectionErrorView: React.FC = ({ } onPress={() => { track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TAP, - [PerpsEventProperties.ACTION]: - PerpsEventValues.ACTION.CONNECTION_RETRY, - [PerpsEventProperties.ATTEMPT_NUMBER]: retryAttempts + 1, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TAP, + [PERPS_EVENT_PROPERTY.ACTION]: + PERPS_EVENT_VALUE.ACTION.CONNECTION_RETRY, + [PERPS_EVENT_PROPERTY.ATTEMPT_NUMBER]: retryAttempts + 1, }); onRetry(); }} diff --git a/app/components/UI/Perps/components/PerpsFillTag/PerpsFillTag.tsx b/app/components/UI/Perps/components/PerpsFillTag/PerpsFillTag.tsx index ff7d144a56d..1bc795c7993 100644 --- a/app/components/UI/Perps/components/PerpsFillTag/PerpsFillTag.tsx +++ b/app/components/UI/Perps/components/PerpsFillTag/PerpsFillTag.tsx @@ -16,8 +16,8 @@ import { PERPS_SUPPORT_ARTICLES_URLS } from '../../constants/perpsConfig'; import { usePerpsEventTracking } from '../../hooks'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { FillType, PerpsTransaction } from '../../types/transactionHistory'; @@ -36,7 +36,7 @@ interface PerpsFillTagProps { */ const PerpsFillTag: React.FC = ({ transaction, - screenName = PerpsEventValues.SCREEN_NAME.PERPS_ACTIVITY_HISTORY, + screenName = PERPS_EVENT_VALUE.SCREEN_NAME.PERPS_ACTIVITY_HISTORY, }) => { const selectedAccount = useSelector(selectSelectedInternalAccountByScope)( EVM_SCOPE, @@ -115,15 +115,15 @@ const PerpsFillTag: React.FC = ({ console.error('Error opening ADL support article:', error); }); track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TAP, - [PerpsEventProperties.SCREEN_NAME]: screenName, - [PerpsEventProperties.TAB_NAME]: - PerpsEventValues.PERPS_HISTORY_TABS.TRADES, - [PerpsEventProperties.ACTION_TYPE]: - PerpsEventValues.ACTION_TYPE.ADL_LEARN_MORE, - [PerpsEventProperties.ASSET]: transaction.asset, - [PerpsEventProperties.ORDER_TIMESTAMP]: transaction.timestamp, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TAP, + [PERPS_EVENT_PROPERTY.SCREEN_NAME]: screenName, + [PERPS_EVENT_PROPERTY.TAB_NAME]: + PERPS_EVENT_VALUE.PERPS_HISTORY_TABS.TRADES, + [PERPS_EVENT_PROPERTY.ACTION_TYPE]: + PERPS_EVENT_VALUE.ACTION_TYPE.ADL_LEARN_MORE, + [PERPS_EVENT_PROPERTY.ASSET]: transaction.asset, + [PERPS_EVENT_PROPERTY.ORDER_TIMESTAMP]: transaction.timestamp, }); }; diff --git a/app/components/UI/Perps/components/PerpsLeverageBottomSheet/PerpsLeverageBottomSheet.tsx b/app/components/UI/Perps/components/PerpsLeverageBottomSheet/PerpsLeverageBottomSheet.tsx index 2175d23b228..83c41b967be 100644 --- a/app/components/UI/Perps/components/PerpsLeverageBottomSheet/PerpsLeverageBottomSheet.tsx +++ b/app/components/UI/Perps/components/PerpsLeverageBottomSheet/PerpsLeverageBottomSheet.tsx @@ -45,8 +45,8 @@ import { useTheme } from '../../../../../util/theme'; import { Theme } from '../../../../../util/theme/models'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { getLeverageRiskLevel, @@ -401,12 +401,13 @@ const PerpsLeverageBottomSheet: React.FC = ({ conditions: [isVisible], resetConditions: [!isVisible], // Auto-reset when modal closes properties: { - [PerpsEventProperties.SCREEN_TYPE]: PerpsEventValues.SCREEN_TYPE.LEVERAGE, - [PerpsEventProperties.ASSET]: asset, - [PerpsEventProperties.DIRECTION]: + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.LEVERAGE, + [PERPS_EVENT_PROPERTY.ASSET]: asset, + [PERPS_EVENT_PROPERTY.DIRECTION]: direction === 'long' - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, }, }); diff --git a/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.test.tsx b/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.test.tsx index 8ab4571fee0..a4dbf66e83a 100644 --- a/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.test.tsx +++ b/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.test.tsx @@ -127,14 +127,14 @@ jest.mock('../../hooks/usePerpsEventTracking', () => ({ // Mock eventNames constants jest.mock('../../constants/eventNames', () => ({ - PerpsEventProperties: { + PERPS_EVENT_PROPERTY: { INTERACTION_TYPE: 'interaction_type', SETTING_TYPE: 'setting_type', INPUT_METHOD: 'input_method', ASSET: 'asset', DIRECTION: 'direction', }, - PerpsEventValues: { + PERPS_EVENT_VALUE: { INTERACTION_TYPE: { SETTING_CHANGED: 'setting_changed' }, INPUT_METHOD: { PRESET: 'preset', diff --git a/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.tsx b/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.tsx index 4c122707d6a..cbde91fcb3f 100644 --- a/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.tsx +++ b/app/components/UI/Perps/components/PerpsLimitPriceBottomSheet/PerpsLimitPriceBottomSheet.tsx @@ -32,8 +32,8 @@ import { BigNumber } from 'bignumber.js'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; interface PerpsLimitPriceBottomSheetProps { @@ -137,12 +137,12 @@ const PerpsLimitPriceBottomSheet: React.FC = ({ // Track limit price input method if (inputMethod) { track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.SETTING_CHANGED, - [PerpsEventProperties.SETTING_TYPE]: 'limit_price', - [PerpsEventProperties.INPUT_METHOD]: inputMethod, - [PerpsEventProperties.ASSET]: asset, - [PerpsEventProperties.DIRECTION]: direction, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.SETTING_CHANGED, + [PERPS_EVENT_PROPERTY.SETTING_TYPE]: 'limit_price', + [PERPS_EVENT_PROPERTY.INPUT_METHOD]: inputMethod, + [PERPS_EVENT_PROPERTY.ASSET]: asset, + [PERPS_EVENT_PROPERTY.DIRECTION]: direction, }); } @@ -159,7 +159,7 @@ const PerpsLimitPriceBottomSheet: React.FC = ({ return; // Ignore input that would exceed 9 digits } setLimitPrice(value || ''); - setInputMethod(PerpsEventValues.INPUT_METHOD.KEYBOARD); + setInputMethod(PERPS_EVENT_VALUE.INPUT_METHOD.KEYBOARD); }, [], ); @@ -377,7 +377,7 @@ const PerpsLimitPriceBottomSheet: React.FC = ({ setLimitPrice( formatWithSignificantDigits(currentPrice, 4).value.toString(), ); - setInputMethod(PerpsEventValues.INPUT_METHOD.PRESET); + setInputMethod(PERPS_EVENT_VALUE.INPUT_METHOD.PRESET); } }} > @@ -401,7 +401,7 @@ const PerpsLimitPriceBottomSheet: React.FC = ({ 4, ).value.toString(), ); - setInputMethod(PerpsEventValues.INPUT_METHOD.PRESET); + setInputMethod(PERPS_EVENT_VALUE.INPUT_METHOD.PRESET); } }} > @@ -426,7 +426,7 @@ const PerpsLimitPriceBottomSheet: React.FC = ({ ).value.toString(), ); setInputMethod( - PerpsEventValues.INPUT_METHOD.PERCENTAGE_BUTTON, + PERPS_EVENT_VALUE.INPUT_METHOD.PERCENTAGE_BUTTON, ); } }} @@ -453,7 +453,7 @@ const PerpsLimitPriceBottomSheet: React.FC = ({ 4, ).value.toString(), ); - setInputMethod(PerpsEventValues.INPUT_METHOD.PRESET); + setInputMethod(PERPS_EVENT_VALUE.INPUT_METHOD.PRESET); } }} > @@ -478,7 +478,7 @@ const PerpsLimitPriceBottomSheet: React.FC = ({ ).value.toString(), ); setInputMethod( - PerpsEventValues.INPUT_METHOD.PERCENTAGE_BUTTON, + PERPS_EVENT_VALUE.INPUT_METHOD.PERCENTAGE_BUTTON, ); } }} diff --git a/app/components/UI/Perps/components/PerpsMarketBalanceActions/PerpsMarketBalanceActions.tsx b/app/components/UI/Perps/components/PerpsMarketBalanceActions/PerpsMarketBalanceActions.tsx index f5d491e0909..de7693fac65 100644 --- a/app/components/UI/Perps/components/PerpsMarketBalanceActions/PerpsMarketBalanceActions.tsx +++ b/app/components/UI/Perps/components/PerpsMarketBalanceActions/PerpsMarketBalanceActions.tsx @@ -33,7 +33,7 @@ import PerpsEmptyBalance from '../PerpsEmptyBalance'; import DevLogger from '../../../../../core/SDKConnect/utils/DevLogger'; import { PerpsProgressBar } from '../PerpsProgressBar'; import { selectWithdrawalRequestsBySelectedAccount } from '../../../../../selectors/perps'; -import { PerpsEventValues } from '../../constants/eventNames'; +import { PERPS_EVENT_VALUE } from '../../constants/eventNames'; interface PerpsMarketBalanceActionsProps { showActionButtons?: boolean; } @@ -85,8 +85,8 @@ const PerpsMarketBalanceActions: React.FC = ({ // Use hook for eligibility checks and action handlers // Determine button location based on whether balance is empty (empty state) or not (home) const buttonLocation = isBalanceEmpty - ? PerpsEventValues.BUTTON_LOCATION.PERPS_HOME_EMPTY_STATE - : PerpsEventValues.BUTTON_LOCATION.PERPS_HOME; + ? PERPS_EVENT_VALUE.BUTTON_LOCATION.PERPS_HOME_EMPTY_STATE + : PERPS_EVENT_VALUE.BUTTON_LOCATION.PERPS_HOME; const { handleAddFunds, diff --git a/app/components/UI/Perps/components/PerpsMarketTradesList/PerpsMarketTradesList.tsx b/app/components/UI/Perps/components/PerpsMarketTradesList/PerpsMarketTradesList.tsx index c5fedf0930a..eb53b47199e 100644 --- a/app/components/UI/Perps/components/PerpsMarketTradesList/PerpsMarketTradesList.tsx +++ b/app/components/UI/Perps/components/PerpsMarketTradesList/PerpsMarketTradesList.tsx @@ -18,7 +18,7 @@ import { getPerpsDisplaySymbol } from '../../utils/marketUtils'; import { usePerpsMarketFills } from '../../hooks/usePerpsMarketFills'; import { transformFillsToTransactions } from '../../utils/transactionTransforms'; import { PERPS_CONSTANTS } from '../../constants/perpsConfig'; -import { PerpsEventValues } from '../../constants/eventNames'; +import { PERPS_EVENT_VALUE } from '../../constants/eventNames'; interface PerpsMarketTradesListProps { symbol: string; // Market symbol to filter trades @@ -114,7 +114,9 @@ const PerpsMarketTradesList: React.FC = ({ {!!item.subtitle && ( diff --git a/app/components/UI/Perps/components/PerpsOpenOrderCard/PerpsOpenOrderCard.tsx b/app/components/UI/Perps/components/PerpsOpenOrderCard/PerpsOpenOrderCard.tsx index 2809beab66f..7ccee526fb5 100644 --- a/app/components/UI/Perps/components/PerpsOpenOrderCard/PerpsOpenOrderCard.tsx +++ b/app/components/UI/Perps/components/PerpsOpenOrderCard/PerpsOpenOrderCard.tsx @@ -38,8 +38,8 @@ import { selectPerpsEligibility } from '../../selectors/perpsController'; import { getPerpsDisplaySymbol } from '../../utils/marketUtils'; import { useMetrics, MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; /** @@ -146,9 +146,10 @@ const PerpsOpenOrderCard: React.FC = ({ trackEvent( createEventBuilder(MetaMetricsEvents.PERPS_SCREEN_VIEWED) .addProperties({ - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: PerpsEventValues.SOURCE.CANCEL_ORDER, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.CANCEL_ORDER, }) .build(), ); diff --git a/app/components/UI/Perps/components/PerpsOrderTypeBottomSheet/PerpsOrderTypeBottomSheet.tsx b/app/components/UI/Perps/components/PerpsOrderTypeBottomSheet/PerpsOrderTypeBottomSheet.tsx index e1afe76b2ee..94302f8f099 100644 --- a/app/components/UI/Perps/components/PerpsOrderTypeBottomSheet/PerpsOrderTypeBottomSheet.tsx +++ b/app/components/UI/Perps/components/PerpsOrderTypeBottomSheet/PerpsOrderTypeBottomSheet.tsx @@ -13,8 +13,8 @@ import { createStyles } from './PerpsOrderTypeBottomSheet.styles'; import { strings } from '../../../../../../locales/i18n'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; import type { OrderType } from '../../controllers/types'; @@ -67,17 +67,17 @@ const PerpsOrderTypeBottomSheet: React.FC = ({ // Track order type selected only if it's different from current if (type !== currentOrderType) { track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.ORDER_TYPE_SELECTED, - [PerpsEventProperties.ASSET]: asset, - [PerpsEventProperties.DIRECTION]: + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.ORDER_TYPE_SELECTED, + [PERPS_EVENT_PROPERTY.ASSET]: asset, + [PERPS_EVENT_PROPERTY.DIRECTION]: direction === 'long' - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.ORDER_TYPE]: + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.ORDER_TYPE]: type === 'market' - ? PerpsEventValues.ORDER_TYPE.MARKET - : PerpsEventValues.ORDER_TYPE.LIMIT, + ? PERPS_EVENT_VALUE.ORDER_TYPE.MARKET + : PERPS_EVENT_VALUE.ORDER_TYPE.LIMIT, }); } diff --git a/app/components/UI/Perps/components/PerpsRecentActivityList/PerpsRecentActivityList.tsx b/app/components/UI/Perps/components/PerpsRecentActivityList/PerpsRecentActivityList.tsx index 1a1084fea97..20cb328e21c 100644 --- a/app/components/UI/Perps/components/PerpsRecentActivityList/PerpsRecentActivityList.tsx +++ b/app/components/UI/Perps/components/PerpsRecentActivityList/PerpsRecentActivityList.tsx @@ -21,7 +21,7 @@ import styleSheet from './PerpsRecentActivityList.styles'; import { HOME_SCREEN_CONFIG } from '../../constants/perpsConfig'; import PerpsRowSkeleton from '../PerpsRowSkeleton'; import { getPerpsDisplaySymbol } from '../../utils/marketUtils'; -import { PerpsEventValues } from '../../constants/eventNames'; +import { PERPS_EVENT_VALUE } from '../../constants/eventNames'; interface PerpsRecentActivityListProps { transactions: PerpsTransaction[]; @@ -105,7 +105,7 @@ const PerpsRecentActivityList: React.FC = ({ {!!item.subtitle && ( diff --git a/app/components/UI/Perps/components/PerpsTransactionItem/PerpsTransactionItem.test.tsx b/app/components/UI/Perps/components/PerpsTransactionItem/PerpsTransactionItem.test.tsx index ddea7b15f8f..389ca36e741 100644 --- a/app/components/UI/Perps/components/PerpsTransactionItem/PerpsTransactionItem.test.tsx +++ b/app/components/UI/Perps/components/PerpsTransactionItem/PerpsTransactionItem.test.tsx @@ -9,8 +9,8 @@ import { } from '../../types/transactionHistory'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { PERPS_SUPPORT_ARTICLES_URLS } from '../../constants/perpsConfig'; @@ -542,16 +542,16 @@ describe('PerpsTransactionItem', () => { expect(mockTrack).toHaveBeenCalledWith( MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TAP, - [PerpsEventProperties.SCREEN_NAME]: - PerpsEventValues.SCREEN_NAME.PERPS_ACTIVITY_HISTORY, - [PerpsEventProperties.TAB_NAME]: - PerpsEventValues.PERPS_HISTORY_TABS.TRADES, - [PerpsEventProperties.ACTION_TYPE]: - PerpsEventValues.ACTION_TYPE.ADL_LEARN_MORE, - [PerpsEventProperties.ASSET]: 'BTC', - [PerpsEventProperties.ORDER_TIMESTAMP]: 1234567890000, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TAP, + [PERPS_EVENT_PROPERTY.SCREEN_NAME]: + PERPS_EVENT_VALUE.SCREEN_NAME.PERPS_ACTIVITY_HISTORY, + [PERPS_EVENT_PROPERTY.TAB_NAME]: + PERPS_EVENT_VALUE.PERPS_HISTORY_TABS.TRADES, + [PERPS_EVENT_PROPERTY.ACTION_TYPE]: + PERPS_EVENT_VALUE.ACTION_TYPE.ADL_LEARN_MORE, + [PERPS_EVENT_PROPERTY.ASSET]: 'BTC', + [PERPS_EVENT_PROPERTY.ORDER_TIMESTAMP]: 1234567890000, }, ); }); diff --git a/app/components/UI/Perps/components/PerpsTutorialCarousel/PerpsTutorialCarousel.tsx b/app/components/UI/Perps/components/PerpsTutorialCarousel/PerpsTutorialCarousel.tsx index 9a1bb2b9b9a..6b0c6e37176 100644 --- a/app/components/UI/Perps/components/PerpsTutorialCarousel/PerpsTutorialCarousel.tsx +++ b/app/components/UI/Perps/components/PerpsTutorialCarousel/PerpsTutorialCarousel.tsx @@ -29,8 +29,8 @@ import NavigationService from '../../../../../core/NavigationService'; import { EXTERNAL_LINK_TYPE } from '../../../../../constants/browser'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { usePerpsFirstTimeUser } from '../../hooks'; @@ -175,10 +175,10 @@ const PerpsTutorialCarousel: React.FC = () => { useEffect(() => { if (!hasTrackedViewed.current) { track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.TUTORIAL, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.MAIN_ACTION_BUTTON, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.TUTORIAL, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.MAIN_ACTION_BUTTON, }); hasTrackedViewed.current = true; } @@ -222,16 +222,16 @@ const PerpsTutorialCarousel: React.FC = () => { // Only track if tab actually changed (user swipe) if (newTab !== previousTab) { track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TUTORIAL_NAVIGATION, - [PerpsEventProperties.PREVIOUS_SCREEN]: + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TUTORIAL_NAVIGATION, + [PERPS_EVENT_PROPERTY.PREVIOUS_SCREEN]: tutorialScreens[previousTab]?.id || 'unknown', - [PerpsEventProperties.CURRENT_SCREEN]: + [PERPS_EVENT_PROPERTY.CURRENT_SCREEN]: tutorialScreens[newTab]?.id || 'unknown', - [PerpsEventProperties.SCREEN_POSITION]: newTab + 1, - [PerpsEventProperties.TOTAL_SCREENS]: tutorialScreens.length, - [PerpsEventProperties.NAVIGATION_METHOD]: - PerpsEventValues.NAVIGATION_METHOD.SWIPE, + [PERPS_EVENT_PROPERTY.SCREEN_POSITION]: newTab + 1, + [PERPS_EVENT_PROPERTY.TOTAL_SCREENS]: tutorialScreens.length, + [PERPS_EVENT_PROPERTY.NAVIGATION_METHOD]: + PERPS_EVENT_VALUE.NAVIGATION_METHOD.SWIPE, }); previousTabRef.current = newTab; @@ -242,10 +242,10 @@ const PerpsTutorialCarousel: React.FC = () => { // Track tutorial started when user moves to second screen if (newTab === 1 && !hasTrackedStarted.current) { track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TUTORIAL_STARTED, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.MAIN_ACTION_BUTTON, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TUTORIAL_STARTED, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.MAIN_ACTION_BUTTON, }); hasTrackedStarted.current = true; } @@ -274,13 +274,13 @@ const PerpsTutorialCarousel: React.FC = () => { // Track tutorial completed const completionDuration = Date.now() - tutorialStartTime.current; track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TUTORIAL_COMPLETED, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.MAIN_ACTION_BUTTON, - [PerpsEventProperties.COMPLETION_DURATION_TUTORIAL]: completionDuration, - [PerpsEventProperties.STEPS_VIEWED]: currentTab + 1, - [PerpsEventProperties.VIEW_OCCURRENCES]: 1, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TUTORIAL_COMPLETED, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.MAIN_ACTION_BUTTON, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION_TUTORIAL]: completionDuration, + [PERPS_EVENT_PROPERTY.STEPS_VIEWED]: currentTab + 1, + [PERPS_EVENT_PROPERTY.VIEW_OCCURRENCES]: 1, }); // Mark tutorial as completed @@ -294,16 +294,16 @@ const PerpsTutorialCarousel: React.FC = () => { // Track carousel navigation via continue button (immediate, no debounce needed for button clicks) if (nextTab !== currentTab) { track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TUTORIAL_NAVIGATION, - [PerpsEventProperties.PREVIOUS_SCREEN]: + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TUTORIAL_NAVIGATION, + [PERPS_EVENT_PROPERTY.PREVIOUS_SCREEN]: tutorialScreens[currentTab]?.id || 'unknown', - [PerpsEventProperties.CURRENT_SCREEN]: + [PERPS_EVENT_PROPERTY.CURRENT_SCREEN]: tutorialScreens[nextTab]?.id || 'unknown', - [PerpsEventProperties.SCREEN_POSITION]: nextTab + 1, - [PerpsEventProperties.TOTAL_SCREENS]: tutorialScreens.length, - [PerpsEventProperties.NAVIGATION_METHOD]: - PerpsEventValues.NAVIGATION_METHOD.CONTINUE_BUTTON, + [PERPS_EVENT_PROPERTY.SCREEN_POSITION]: nextTab + 1, + [PERPS_EVENT_PROPERTY.TOTAL_SCREENS]: tutorialScreens.length, + [PERPS_EVENT_PROPERTY.NAVIGATION_METHOD]: + PERPS_EVENT_VALUE.NAVIGATION_METHOD.CONTINUE_BUTTON, }); } @@ -314,10 +314,10 @@ const PerpsTutorialCarousel: React.FC = () => { // Track tutorial started on first continue if (currentTab === 0 && !hasTrackedStarted.current) { track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TUTORIAL_STARTED, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.MAIN_ACTION_BUTTON, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TUTORIAL_STARTED, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.MAIN_ACTION_BUTTON, }); hasTrackedStarted.current = true; } @@ -336,13 +336,13 @@ const PerpsTutorialCarousel: React.FC = () => { // Track tutorial completed when skipping from last screen const completionDuration = Date.now() - tutorialStartTime.current; track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.TUTORIAL_COMPLETED, - [PerpsEventProperties.SOURCE]: - PerpsEventValues.SOURCE.MAIN_ACTION_BUTTON, - [PerpsEventProperties.COMPLETION_DURATION_TUTORIAL]: completionDuration, - [PerpsEventProperties.STEPS_VIEWED]: currentTab + 1, - [PerpsEventProperties.VIEW_OCCURRENCES]: 1, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.TUTORIAL_COMPLETED, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.MAIN_ACTION_BUTTON, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION_TUTORIAL]: completionDuration, + [PERPS_EVENT_PROPERTY.STEPS_VIEWED]: currentTab + 1, + [PERPS_EVENT_PROPERTY.VIEW_OCCURRENCES]: 1, }); } diff --git a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.context.test.tsx b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.context.test.tsx index 9dd21da781f..6d83b4f69d9 100644 --- a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.context.test.tsx +++ b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.context.test.tsx @@ -160,6 +160,49 @@ describe('PerpsWebSocketHealthToast.context', () => { expect(result.current.state.reconnectionAttempt).toBe(2); expect(result.current.state.isVisible).toBe(false); }); + + it('when hide({ userDismissed: true }), subsequent Disconnected is suppressed but Connecting and Connected show again', () => { + const { result } = renderHook(() => useWebSocketHealthToastContext(), { + wrapper, + }); + + act(() => { + result.current.show(WebSocketConnectionState.Disconnected, 1); + }); + expect(result.current.state.isVisible).toBe(true); + + act(() => { + result.current.hide({ userDismissed: true }); + }); + expect(result.current.state.isVisible).toBe(false); + + // Showing Disconnected again should not show (user dismissed offline) + act(() => { + result.current.show(WebSocketConnectionState.Disconnected, 2); + }); + expect(result.current.state.isVisible).toBe(false); + + // Showing Connecting should show (user sees reconnection progress) + act(() => { + result.current.show(WebSocketConnectionState.Connecting, 3); + }); + expect(result.current.state.isVisible).toBe(true); + + // Showing Connected shows "online" toast and clears userDismissed + act(() => { + result.current.show(WebSocketConnectionState.Connected, 0); + }); + expect(result.current.state.isVisible).toBe(true); + + act(() => { + result.current.hide(); + }); + // Next Disconnected will show again (userDismissed was cleared) + act(() => { + result.current.show(WebSocketConnectionState.Disconnected, 4); + }); + expect(result.current.state.isVisible).toBe(true); + }); }); describe('setOnRetry()', () => { diff --git a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.context.tsx b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.context.tsx index 6113f520c61..7679194a6e3 100644 --- a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.context.tsx +++ b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.context.tsx @@ -1,4 +1,10 @@ -import React, { createContext, useContext, useState, useCallback } from 'react'; +import React, { + createContext, + useContext, + useState, + useCallback, + useMemo, +} from 'react'; import { WebSocketConnectionState } from '../../controllers/types'; /** No-op function for context defaults */ @@ -13,6 +19,12 @@ export interface WebSocketHealthToastState { reconnectionAttempt: number; } +/** Options for hiding the toast (e.g. user swipe dismiss) */ +export interface WebSocketHealthToastHideOptions { + /** When true, toast will not be shown again until connection is restored (Connected state) */ + userDismissed?: boolean; +} + /** * Context params for controlling the WebSocket health toast. */ @@ -22,7 +34,7 @@ export interface WebSocketHealthToastContextParams { connectionState: WebSocketConnectionState, reconnectionAttempt?: number, ) => void; - hide: () => void; + hide: (options?: WebSocketHealthToastHideOptions) => void; onRetry?: () => void; setOnRetry: (callback: () => void) => void; } @@ -50,22 +62,53 @@ export const WebSocketHealthToastProvider: React.FC<{ children: React.ReactNode; }> = ({ children }) => { const [state, setState] = useState(defaultState); - const [onRetry, setOnRetryCallback] = useState<(() => void) | undefined>( - undefined, - ); + const [userDismissed, setUserDismissed] = useState(false); + const [onRetryCallback, setOnRetryCallback] = useState< + (() => void) | undefined + >(undefined); const show = useCallback( (connectionState: WebSocketConnectionState, reconnectionAttempt = 0) => { + const isConnected = + connectionState === WebSocketConnectionState.Connected; + const isConnecting = + connectionState === WebSocketConnectionState.Connecting; + + // When connection is restored, always show "online" toast and clear dismiss state + // (handled first so we never skip due to stale userDismissed closure) + if (isConnected) { + setUserDismissed(false); + setState({ + isVisible: true, + connectionState, + reconnectionAttempt, + }); + return; + } + + // When reconnecting, clear userDismissed so "connecting" and later "online" toasts can show + if (isConnecting) { + setUserDismissed(false); + } + + // Don't show Disconnected if user previously dismissed (until connection is restoring/restored). + // Connecting is always shown so user sees progress after having dismissed "offline". + if (userDismissed && !isConnecting) { + return; + } setState({ isVisible: true, connectionState, reconnectionAttempt, }); }, - [], + [userDismissed], ); - const hide = useCallback(() => { + const hide = useCallback((options?: WebSocketHealthToastHideOptions) => { + if (options?.userDismissed) { + setUserDismissed(true); + } setState((prev) => ({ ...prev, isVisible: false })); }, []); @@ -73,10 +116,19 @@ export const WebSocketHealthToastProvider: React.FC<{ setOnRetryCallback(() => callback); }, []); + const contextValue = useMemo( + () => ({ + state, + show, + hide, + onRetry: onRetryCallback, + setOnRetry, + }), + [state, show, hide, onRetryCallback, setOnRetry], + ); + return ( - + {children} ); diff --git a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.styles.ts b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.styles.ts index 14448c285da..0090df187eb 100644 --- a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.styles.ts +++ b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.styles.ts @@ -13,15 +13,12 @@ const styleSheet = (params: { theme: Theme }) => { right: 12, zIndex: 9999, }, - // Inner toast content - toast: { - flexDirection: 'row', - alignItems: 'center', - gap: 12, - paddingVertical: 12, - paddingHorizontal: 16, + // Wrapper with default background (close wrap: same edges, radius) + toastWrapper: { borderRadius: 12, backgroundColor: colors.background.default, + padding: 2, + overflow: 'hidden', // Shadow for elevation shadowColor: colors.shadow.default, shadowOffset: { @@ -32,6 +29,16 @@ const styleSheet = (params: { theme: Theme }) => { shadowRadius: 8, elevation: 8, }, + // Inner toast content (muted background) + toast: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + paddingVertical: 12, + paddingHorizontal: 16, + borderRadius: 10, + backgroundColor: colors.background.muted, + }, // Icon container iconContainer: { width: 32, diff --git a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.test.tsx b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.test.tsx index 390690c7e75..28c79e6fa08 100644 --- a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.test.tsx +++ b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.test.tsx @@ -3,7 +3,7 @@ */ import React from 'react'; -import { render, fireEvent, waitFor } from '@testing-library/react-native'; +import { render, fireEvent, waitFor, act } from '@testing-library/react-native'; import PerpsWebSocketHealthToast from './PerpsWebSocketHealthToast'; import { WebSocketConnectionState } from '../../controllers/types'; import { PerpsWebSocketHealthToastSelectorsIDs } from '../../Perps.testIds'; @@ -248,8 +248,10 @@ describe('PerpsWebSocketHealthToast', () => { render(); - // Fast-forward time - jest.advanceTimersByTime(3000); + // Fast-forward time (wrap in act so Animated callbacks flush) + await act(async () => { + jest.advanceTimersByTime(3000); + }); expect(mockHide).toHaveBeenCalled(); }); diff --git a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.tsx b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.tsx index d914830e055..01e53ae1cb8 100644 --- a/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.tsx +++ b/app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.tsx @@ -1,5 +1,12 @@ import React, { memo, useEffect, useRef, useMemo, useState } from 'react'; -import { Animated, TouchableOpacity, View, StyleSheet } from 'react-native'; +import { + Animated, + PanResponder, + TouchableOpacity, + View, + StyleSheet, + Dimensions, +} from 'react-native'; import { IconColor as ReactNativeDsIconColor, IconSize as ReactNativeDsIconSize, @@ -27,6 +34,9 @@ const ANIMATION_DURATION_MS = 300; /** Duration to show the success toast before auto-hiding */ const SUCCESS_TOAST_DURATION_MS = 3000; +/** Minimum horizontal swipe distance (px) to trigger dismiss */ +const SWIPE_DISMISS_THRESHOLD = 80; + /** * PerpsWebSocketHealthToast * @@ -53,10 +63,62 @@ const PerpsWebSocketHealthToast: React.FC = memo(() => { // Animation value for slide-in/out effect (negative = slide from top) const slideAnim = useRef(new Animated.Value(-100)).current; const opacityAnim = useRef(new Animated.Value(0)).current; + // Horizontal swipe for dismiss (left or right) + const swipeAnim = useRef(new Animated.Value(0)).current; // Track if we should auto-hide for success state const hideTimeoutRef = useRef | null>(null); + // Ref to read latest connection state in swipe completion (avoids race: connection + // can restore during 300ms exit animation; we only apply userDismissed if still offline/connecting) + const connectionStateRef = useRef(connectionState); + connectionStateRef.current = connectionState; + + const screenWidth = Dimensions.get('window').width; + + // PanResponder for horizontal swipe-to-dismiss (left or right) + const panResponder = useMemo( + () => + PanResponder.create({ + onStartShouldSetPanResponder: () => false, + onMoveShouldSetPanResponder: (_, gestureState) => + Math.abs(gestureState.dx) > 10, + onPanResponderMove: (_, gestureState) => { + swipeAnim.setValue(gestureState.dx); + }, + onPanResponderRelease: (_, gestureState) => { + const dx = gestureState.dx; + if (Math.abs(dx) >= SWIPE_DISMISS_THRESHOLD) { + const exitDirection = dx > 0 ? 1 : -1; + Animated.timing(swipeAnim, { + toValue: exitDirection * screenWidth, + duration: ANIMATION_DURATION_MS, + useNativeDriver: true, + }).start(({ finished }) => { + if (finished) { + const stateWhenDone = connectionStateRef.current; + const stillOffline = + stateWhenDone === WebSocketConnectionState.Disconnected || + stateWhenDone === WebSocketConnectionState.Connecting; + if (stillOffline) { + hide({ userDismissed: true }); + } + // If connection restored during animation, do nothing: leave Connected toast visible + } + }); + } else { + Animated.spring(swipeAnim, { + toValue: 0, + useNativeDriver: true, + tension: 65, + friction: 11, + }).start(); + } + }, + }), + [hide, screenWidth, swipeAnim], + ); + // Get toast configuration based on connection state const toastConfig = useMemo(() => { switch (connectionState) { @@ -99,6 +161,8 @@ const PerpsWebSocketHealthToast: React.FC = memo(() => { // Handle visibility animation useEffect(() => { if (isVisible) { + // Reset swipe position when showing + swipeAnim.setValue(0); // Show the component immediately, then animate in setShouldRender(true); Animated.parallel([ @@ -136,7 +200,7 @@ const PerpsWebSocketHealthToast: React.FC = memo(() => { // Note: shouldRender is intentionally excluded from deps to prevent animation restart. // We only want to react to isVisible changes - shouldRender is internal lifecycle state. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isVisible, slideAnim, opacityAnim]); + }, [isVisible, slideAnim, opacityAnim, swipeAnim]); // Auto-hide for success state useEffect(() => { @@ -172,53 +236,56 @@ const PerpsWebSocketHealthToast: React.FC = memo(() => { style={[ styles.container, { - transform: [{ translateY: slideAnim }], + transform: [{ translateY: slideAnim }, { translateX: swipeAnim }], opacity: opacityAnim, }, ]} testID={PerpsWebSocketHealthToastSelectorsIDs.TOAST} pointerEvents="box-none" + {...panResponder.panHandlers} > - - {/* Icon or Spinner */} - - {toastConfig.showSpinner ? ( - - ) : ( - - )} - + + + {/* Icon or Spinner */} + + {toastConfig.showSpinner ? ( + + ) : ( + + )} + - {/* Text Content */} - - - {toastConfig.title} - - - {toastConfig.description} - - + {/* Text Content */} + + + {toastConfig.title} + + + {toastConfig.description} + + - {/* Retry Button - only shown when disconnected */} - {connectionState === WebSocketConnectionState.Disconnected && - onRetry && ( - - - {strings('perps.connection.websocket_retry')} - - - )} + {/* Retry Button - only shown when disconnected */} + {connectionState === WebSocketConnectionState.Disconnected && + onRetry && ( + + + {strings('perps.connection.websocket_retry')} + + + )} + diff --git a/app/components/UI/Perps/constants/eventNames.ts b/app/components/UI/Perps/constants/eventNames.ts index 62a7cf3ad6d..f715f21b1b0 100644 --- a/app/components/UI/Perps/constants/eventNames.ts +++ b/app/components/UI/Perps/constants/eventNames.ts @@ -6,7 +6,7 @@ /** * Event property keys - ensures consistent property naming */ -export const PerpsEventProperties = { +export const PERPS_EVENT_PROPERTY = { // Common properties TIMESTAMP: 'timestamp', ASSET: 'asset', @@ -139,7 +139,7 @@ export const PerpsEventProperties = { /** * Property value constants */ -export const PerpsEventValues = { +export const PERPS_EVENT_VALUE = { DIRECTION: { LONG: 'long', SHORT: 'short', diff --git a/app/components/UI/Perps/controllers/PerpsController.test.ts b/app/components/UI/Perps/controllers/PerpsController.test.ts index 5a30c55f807..c94e9919e13 100644 --- a/app/components/UI/Perps/controllers/PerpsController.test.ts +++ b/app/components/UI/Perps/controllers/PerpsController.test.ts @@ -10,7 +10,6 @@ import { getDefaultPerpsControllerState, InitializationState, type PerpsControllerState, - type PerpsControllerMessenger, } from './PerpsController'; import { PERPS_ERROR_CODES } from './perpsErrorCodes'; import { @@ -24,7 +23,10 @@ import type { } from './types'; import { HyperLiquidProvider } from './providers/HyperLiquidProvider'; import { createMockHyperLiquidProvider } from '../__mocks__/providerMocks'; -import { createMockInfrastructure } from '../__mocks__/serviceMocks'; +import { + createMockInfrastructure, + createMockMessenger, +} from '../__mocks__/serviceMocks'; import Engine from '../../../../core/Engine'; jest.mock('./providers/HyperLiquidProvider'); @@ -368,28 +370,6 @@ class TestablePerpsController extends PerpsController { } } -/** - * Factory function to create a properly typed mock messenger - * Encapsulates the type assertion in one place - * Note: Uses 'as unknown as' because PerpsControllerMessenger has private properties - */ -function createMockMessenger( - overrides?: Partial, -): PerpsControllerMessenger { - const base = { - call: jest.fn(), - publish: jest.fn(), - subscribe: jest.fn(), - registerActionHandler: jest.fn(), - registerEventHandler: jest.fn(), - registerInitialEventPayload: jest.fn(), - unregisterActionHandler: jest.fn(), - unregisterEventHandler: jest.fn(), - clearEventSubscriptions: jest.fn(), - }; - return { ...base, ...overrides } as unknown as PerpsControllerMessenger; -} - describe('PerpsController', () => { let controller: TestablePerpsController; let mockProvider: jest.Mocked; @@ -2301,6 +2281,10 @@ describe('PerpsController', () => { const mockTransactionMeta = { id: 'tx-meta-123' }; const mockTxHash = '0xhash123'; + // Local messenger mock for depositWithConfirmation tests + let depositMessengerMock: jest.Mock; + let depositController: TestablePerpsController; + beforeEach(() => { // Mock DepositService jest @@ -2311,16 +2295,38 @@ describe('PerpsController', () => { currentDepositId: mockDepositId, }); - // Mock controllers.network via infrastructure (consolidated pattern for core migration) - ( - mockInfrastructure.controllers.network - .findNetworkClientIdForChain as jest.Mock - ).mockReturnValue(mockNetworkClientId); - - // Also mock on Engine.context for backwards compatibility with tests that check the mock calls - Engine.context.NetworkController.findNetworkClientIdByChainId = jest - .fn() - .mockReturnValue(mockNetworkClientId); + // Create a messenger mock that handles network and transaction actions + depositMessengerMock = jest.fn().mockImplementation((action: string) => { + if (action === 'RemoteFeatureFlagController:getState') { + return { + remoteFeatureFlags: { + perpsPerpTradingGeoBlockedCountriesV2: { + blockedRegions: [], + }, + }, + }; + } + if (action === 'NetworkController:findNetworkClientIdByChainId') { + return mockNetworkClientId; + } + if (action === 'TransactionController:addTransaction') { + return Promise.resolve({ + result: Promise.resolve(mockTxHash), + transactionMeta: mockTransactionMeta, + }); + } + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [ + { + address: mockTransaction.from, + type: 'eip155:eoa', + }, + ]; + } + return undefined; + }); Engine.context.TransactionController.estimateGasFee = jest .fn() @@ -2350,38 +2356,27 @@ describe('PerpsController', () => { }, }; - // Mock controllers.transaction.submit via infrastructure (consolidated pattern for core migration) - ( - mockInfrastructure.controllers.transaction.submit as jest.Mock - ).mockResolvedValue({ - result: Promise.resolve(mockTxHash), - transactionMeta: mockTransactionMeta, + // Create a controller with the custom messenger for this test suite + depositController = new TestablePerpsController({ + messenger: createMockMessenger({ call: depositMessengerMock }), + state: getDefaultPerpsControllerState(), + infrastructure: createMockInfrastructure(), }); - - // Also mock on Engine.context for backwards compatibility with tests that check the mock calls - Engine.context.TransactionController.addTransaction = jest - .fn() - .mockResolvedValue({ - result: Promise.resolve(mockTxHash), - transactionMeta: mockTransactionMeta, - }); }); afterEach(() => { - // Clean up mock properties added in beforeEach to prevent test pollution - delete (Engine.context.NetworkController as any) - .findNetworkClientIdByChainId; - delete (Engine.context.TransactionController as any).addTransaction; delete (Engine.context.TransactionController as any).estimateGasFee; jest.clearAllMocks(); mockAddTransaction.mockClear(); }); it('returns promise result', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); - const result = await controller.depositWithConfirmation({ + const result = await depositController.depositWithConfirmation({ amount: '100', }); @@ -2391,10 +2386,12 @@ describe('PerpsController', () => { }); it('delegates to DepositService.prepareTransaction', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); - await controller.depositWithConfirmation({ amount: '100' }); + await depositController.depositWithConfirmation({ amount: '100' }); expect( mockDepositServiceInstance.prepareTransaction, @@ -2403,133 +2400,192 @@ describe('PerpsController', () => { }); }); - it('calls controllers.network.findNetworkClientIdForChain with correct chainId', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + it('calls NetworkController:findNetworkClientIdByChainId with correct chainId', async () => { + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); - await controller.depositWithConfirmation({ amount: '100' }); + await depositController.depositWithConfirmation({ amount: '100' }); - expect( - mockInfrastructure.controllers.network.findNetworkClientIdForChain, - ).toHaveBeenCalledWith(mockAssetChainId); + expect(depositMessengerMock).toHaveBeenCalledWith( + 'NetworkController:findNetworkClientIdByChainId', + mockAssetChainId, + ); }); - it('calls controllers.transaction.submit with prepared transaction', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + it('calls TransactionController:addTransaction with prepared transaction', async () => { + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); - await controller.depositWithConfirmation({ amount: '100' }); + await depositController.depositWithConfirmation({ amount: '100' }); - expect( - mockInfrastructure.controllers.transaction.submit, - ).toHaveBeenCalledWith(mockTransaction, { - networkClientId: mockNetworkClientId, - origin: 'metamask', - type: 'perpsDeposit', - skipInitialGasEstimate: true, - }); + expect(depositMessengerMock).toHaveBeenCalledWith( + 'TransactionController:addTransaction', + mockTransaction, + { + networkClientId: mockNetworkClientId, + origin: 'metamask', + type: 'perpsDeposit', + skipInitialGasEstimate: true, + }, + ); }); it('throws error when controller not initialized', async () => { - controller.testSetInitialized(false); + depositController.testSetInitialized(false); await expect( - controller.depositWithConfirmation({ amount: '100' }), + depositController.depositWithConfirmation({ amount: '100' }), ).rejects.toThrow('CLIENT_NOT_INITIALIZED'); }); it('throws error when no active provider', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map()); + depositController.testMarkInitialized(); + depositController.testSetProviders(new Map()); await expect( - controller.depositWithConfirmation({ amount: '100' }), + depositController.depositWithConfirmation({ amount: '100' }), ).rejects.toThrow(); }); it('propagates DepositService errors', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); const mockError = new Error('Deposit service failed'); jest .spyOn(mockDepositServiceInstance, 'prepareTransaction') .mockRejectedValue(mockError); await expect( - controller.depositWithConfirmation({ amount: '100' }), + depositController.depositWithConfirmation({ amount: '100' }), ).rejects.toThrow('Deposit service failed'); }); - it('propagates controllers.network.findNetworkClientIdForChain errors', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + it('propagates NetworkController:findNetworkClientIdByChainId errors', async () => { + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); const mockError = new Error('Network client not found'); - ( - mockInfrastructure.controllers.network - .findNetworkClientIdForChain as jest.Mock - ).mockImplementation(() => { - throw mockError; + depositMessengerMock.mockImplementation((action: string) => { + if (action === 'NetworkController:findNetworkClientIdByChainId') { + throw mockError; + } + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [{ address: mockTransaction.from, type: 'eip155:eoa' }]; + } + return undefined; }); await expect( - controller.depositWithConfirmation({ amount: '100' }), + depositController.depositWithConfirmation({ amount: '100' }), ).rejects.toThrow('Network client not found'); }); - it('propagates controllers.transaction.submit errors', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + it('propagates TransactionController:addTransaction errors', async () => { + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); const mockError = new Error('Transaction failed'); - ( - mockInfrastructure.controllers.transaction.submit as jest.Mock - ).mockRejectedValue(mockError); + depositMessengerMock.mockImplementation((action: string) => { + if (action === 'NetworkController:findNetworkClientIdByChainId') { + return mockNetworkClientId; + } + if (action === 'TransactionController:addTransaction') { + return Promise.reject(mockError); + } + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [{ address: mockTransaction.from, type: 'eip155:eoa' }]; + } + return undefined; + }); await expect( - controller.depositWithConfirmation({ amount: '100' }), + depositController.depositWithConfirmation({ amount: '100' }), ).rejects.toThrow('Transaction failed'); }); it('clears transaction ID when error occurs and not user cancellation', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); - controller.testUpdate((state) => { + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); + depositController.testUpdate((state) => { state.lastDepositTransactionId = 'old-tx-id'; }); const mockError = new Error('Network error'); - ( - mockInfrastructure.controllers.transaction.submit as jest.Mock - ).mockRejectedValue(mockError); + depositMessengerMock.mockImplementation((action: string) => { + if (action === 'NetworkController:findNetworkClientIdByChainId') { + return mockNetworkClientId; + } + if (action === 'TransactionController:addTransaction') { + return Promise.reject(mockError); + } + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [{ address: mockTransaction.from, type: 'eip155:eoa' }]; + } + return undefined; + }); await expect( - controller.depositWithConfirmation({ amount: '100' }), + depositController.depositWithConfirmation({ amount: '100' }), ).rejects.toThrow('Network error'); - expect(controller.state.lastDepositTransactionId).toBeNull(); + expect(depositController.state.lastDepositTransactionId).toBeNull(); }); it('preserves state when user cancels transaction', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); - controller.testUpdate((state) => { + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); + depositController.testUpdate((state) => { state.lastDepositTransactionId = 'old-tx-id'; }); const mockError = new Error('User denied transaction signature'); - ( - mockInfrastructure.controllers.transaction.submit as jest.Mock - ).mockRejectedValue(mockError); + depositMessengerMock.mockImplementation((action: string) => { + if (action === 'NetworkController:findNetworkClientIdByChainId') { + return mockNetworkClientId; + } + if (action === 'TransactionController:addTransaction') { + return Promise.reject(mockError); + } + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [{ address: mockTransaction.from, type: 'eip155:eoa' }]; + } + return undefined; + }); await expect( - controller.depositWithConfirmation({ amount: '100' }), + depositController.depositWithConfirmation({ amount: '100' }), ).rejects.toThrow('User denied'); // When user cancels, transaction ID is not cleared - expect(controller.state.lastDepositTransactionId).toBe('old-tx-id'); + expect(depositController.state.lastDepositTransactionId).toBe( + 'old-tx-id', + ); }); it('clears stale deposit results before transaction', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); - controller.testUpdate((state) => { + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); + depositController.testUpdate((state) => { state.lastDepositResult = { success: true, txHash: '0xold', @@ -2540,40 +2596,48 @@ describe('PerpsController', () => { }; }); - const { result } = await controller.depositWithConfirmation({ + const { result } = await depositController.depositWithConfirmation({ amount: '100', }); await result; // After promise resolves, lastDepositResult is set with new result - expect(controller.state.lastDepositResult).toBeTruthy(); - expect(controller.state.lastDepositResult?.success).toBe(true); + expect(depositController.state.lastDepositResult).toBeTruthy(); + expect(depositController.state.lastDepositResult?.success).toBe(true); }); it('updates state with transaction details', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); - await controller.depositWithConfirmation({ amount: '100' }); + await depositController.depositWithConfirmation({ amount: '100' }); - expect(controller.state.lastDepositTransactionId).toBe('tx-meta-123'); + expect(depositController.state.lastDepositTransactionId).toBe( + 'tx-meta-123', + ); }); it('stores depositId from service immediately', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); - await controller.depositWithConfirmation({ amount: '100' }); + await depositController.depositWithConfirmation({ amount: '100' }); - expect(controller.state.depositRequests[0].id).toBe(mockDepositId); + expect(depositController.state.depositRequests[0].id).toBe(mockDepositId); }); it('delegates to DepositService with provider', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); - await controller.depositWithConfirmation({ amount: '100' }); + await depositController.depositWithConfirmation({ amount: '100' }); expect( mockDepositServiceInstance.prepareTransaction, @@ -2583,65 +2647,85 @@ describe('PerpsController', () => { }); it('adds deposit request to tracking initially as pending', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); - await controller.depositWithConfirmation({ amount: '100' }); + await depositController.depositWithConfirmation({ amount: '100' }); - expect(controller.state.depositRequests).toHaveLength(1); - expect(controller.state.depositRequests[0].id).toBe(mockDepositId); - expect(controller.state.depositRequests[0].amount).toBe('100'); - expect(controller.state.depositRequests[0].asset).toBe('USDC'); + expect(depositController.state.depositRequests).toHaveLength(1); + expect(depositController.state.depositRequests[0].id).toBe(mockDepositId); + expect(depositController.state.depositRequests[0].amount).toBe('100'); + expect(depositController.state.depositRequests[0].asset).toBe('USDC'); }); it('uses default amount when not provided', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); - await controller.depositWithConfirmation(); + await depositController.depositWithConfirmation(); - expect(controller.state.depositRequests[0].amount).toBe('0'); + expect(depositController.state.depositRequests[0].amount).toBe('0'); }); it('updates deposit request to completed when transaction succeeds', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); - const { result } = await controller.depositWithConfirmation({ + const { result } = await depositController.depositWithConfirmation({ amount: '100', }); await result; // After promise resolves, deposit request is marked as completed - expect(controller.state.depositRequests[0].status).toBe('completed'); - expect(controller.state.depositRequests[0].success).toBe(true); - expect(controller.state.depositRequests[0].txHash).toBe(mockTxHash); + expect(depositController.state.depositRequests[0].status).toBe( + 'completed', + ); + expect(depositController.state.depositRequests[0].success).toBe(true); + expect(depositController.state.depositRequests[0].txHash).toBe( + mockTxHash, + ); }); it('handles concurrent deposit operations without data corruption', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); - const deposit1 = controller.depositWithConfirmation({ amount: '100' }); - const deposit2 = controller.depositWithConfirmation({ amount: '200' }); + const deposit1 = depositController.depositWithConfirmation({ + amount: '100', + }); + const deposit2 = depositController.depositWithConfirmation({ + amount: '200', + }); await Promise.all([deposit1, deposit2]); - expect(controller.state.depositRequests).toHaveLength(2); - const amounts = controller.state.depositRequests.map((req) => req.amount); + expect(depositController.state.depositRequests).toHaveLength(2); + const amounts = depositController.state.depositRequests.map( + (req) => req.amount, + ); expect(amounts).toContain('100'); expect(amounts).toContain('200'); }); it('uses addTransaction when placeOrder is true', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); mockAddTransaction.mockResolvedValue({ transactionMeta: mockTransactionMeta, }); - await controller.depositWithConfirmation({ + await depositController.depositWithConfirmation({ amount: '100', placeOrder: true, }); @@ -2652,18 +2736,25 @@ describe('PerpsController', () => { type: 'perpsDepositAndOrder', skipInitialGasEstimate: true, }); - expect( - mockInfrastructure.controllers.transaction.submit, - ).not.toHaveBeenCalled(); - expect(controller.state.lastDepositTransactionId).toBe('tx-meta-123'); + // TransactionController:addTransaction should not be called for placeOrder (uses addTransaction helper instead) + expect(depositMessengerMock).not.toHaveBeenCalledWith( + 'TransactionController:addTransaction', + expect.anything(), + expect.objectContaining({ type: 'perpsDeposit' }), + ); + expect(depositController.state.lastDepositTransactionId).toBe( + 'tx-meta-123', + ); }); it('clears depositInProgress after successful transaction', async () => { jest.useFakeTimers(); - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); - const { result } = await controller.depositWithConfirmation({ + const { result } = await depositController.depositWithConfirmation({ amount: '100', }); @@ -2671,33 +2762,46 @@ describe('PerpsController', () => { await result; // Initially depositInProgress should be true - expect(controller.state.depositInProgress).toBe(true); + expect(depositController.state.depositInProgress).toBe(true); // Fast-forward the setTimeout jest.advanceTimersByTime(100); // After timeout, depositInProgress should be cleared - expect(controller.state.depositInProgress).toBe(false); - expect(controller.state.lastDepositTransactionId).toBeNull(); + expect(depositController.state.depositInProgress).toBe(false); + expect(depositController.state.lastDepositTransactionId).toBeNull(); jest.useRealTimers(); }); it('handles non-user-cancelled transaction errors after confirmation', async () => { jest.useFakeTimers(); - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); - // Mock submit to succeed initially, but result promise rejects + // Mock messenger to succeed initially, but result promise rejects const mockError = new Error('Network error occurred'); - ( - mockInfrastructure.controllers.transaction.submit as jest.Mock - ).mockResolvedValue({ - result: Promise.reject(mockError), - transactionMeta: mockTransactionMeta, + depositMessengerMock.mockImplementation((action: string) => { + if (action === 'NetworkController:findNetworkClientIdByChainId') { + return mockNetworkClientId; + } + if (action === 'TransactionController:addTransaction') { + return Promise.resolve({ + result: Promise.reject(mockError), + transactionMeta: mockTransactionMeta, + }); + } + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [{ address: mockTransaction.from, type: 'eip155:eoa' }]; + } + return undefined; }); - const { result } = await controller.depositWithConfirmation({ + const { result } = await depositController.depositWithConfirmation({ amount: '100', }); @@ -2705,9 +2809,9 @@ describe('PerpsController', () => { await expect(result).rejects.toThrow('Network error occurred'); // Should set error state - expect(controller.state.depositInProgress).toBe(false); - expect(controller.state.lastDepositTransactionId).toBeNull(); - expect(controller.state.lastDepositResult).toEqual({ + expect(depositController.state.depositInProgress).toBe(false); + expect(depositController.state.lastDepositTransactionId).toBeNull(); + expect(depositController.state.lastDepositResult).toEqual({ success: false, error: 'Network error occurred', amount: '100', @@ -2717,15 +2821,17 @@ describe('PerpsController', () => { }); // Should update deposit request status - expect(controller.state.depositRequests[0].status).toBe('failed'); - expect(controller.state.depositRequests[0].success).toBe(false); + expect(depositController.state.depositRequests[0].status).toBe('failed'); + expect(depositController.state.depositRequests[0].success).toBe(false); jest.useRealTimers(); }); it('handles user cancelled transaction with different error messages', async () => { - markControllerAsInitialized(); - controller.testSetProviders(new Map([['hyperliquid', mockProvider]])); + depositController.testMarkInitialized(); + depositController.testSetProviders( + new Map([['hyperliquid', mockProvider]]), + ); const cancellationMessages = [ 'User rejected transaction signature', @@ -2734,26 +2840,44 @@ describe('PerpsController', () => { ]; for (const message of cancellationMessages) { + // Reset deposit controller state for each iteration + depositController.testUpdate((state) => { + state.depositRequests = []; + state.lastDepositResult = null; + state.depositInProgress = false; + }); jest.clearAllMocks(); const mockError = new Error(message); - // Mock submit to succeed initially, but result promise rejects with user cancellation - ( - mockInfrastructure.controllers.transaction.submit as jest.Mock - ).mockResolvedValue({ - result: Promise.reject(mockError), - transactionMeta: mockTransactionMeta, + // Mock messenger to succeed initially, but result promise rejects with user cancellation + depositMessengerMock.mockImplementation((action: string) => { + if (action === 'NetworkController:findNetworkClientIdByChainId') { + return mockNetworkClientId; + } + if (action === 'TransactionController:addTransaction') { + return Promise.resolve({ + result: Promise.reject(mockError), + transactionMeta: mockTransactionMeta, + }); + } + if ( + action === + 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [{ address: mockTransaction.from, type: 'eip155:eoa' }]; + } + return undefined; }); - const { result } = await controller.depositWithConfirmation({ + const { result } = await depositController.depositWithConfirmation({ amount: '100', }); await expect(result).rejects.toThrow(message); // Should clear state but not set error result - expect(controller.state.depositInProgress).toBe(false); - expect(controller.state.lastDepositTransactionId).toBeNull(); - expect(controller.state.lastDepositResult).toBeNull(); + expect(depositController.state.depositInProgress).toBe(false); + expect(depositController.state.lastDepositTransactionId).toBeNull(); + expect(depositController.state.lastDepositResult).toBeNull(); } }); }); diff --git a/app/components/UI/Perps/controllers/PerpsController.ts b/app/components/UI/Perps/controllers/PerpsController.ts index 4eab31239bd..495ac18d2d8 100644 --- a/app/components/UI/Perps/controllers/PerpsController.ts +++ b/app/components/UI/Perps/controllers/PerpsController.ts @@ -5,9 +5,16 @@ import { StateMetadata, } from '@metamask/base-controller'; import type { Messenger } from '@metamask/messenger'; -import type { NetworkControllerGetStateAction } from '@metamask/network-controller'; +import type { + NetworkControllerGetStateAction, + NetworkControllerGetNetworkClientByIdAction, + NetworkControllerFindNetworkClientIdByChainIdAction, +} from '@metamask/network-controller'; +import type { AccountTreeControllerGetAccountsFromSelectedAccountGroupAction } from '@metamask/account-tree-controller'; +import type { KeyringControllerSignTypedMessageAction } from '@metamask/keyring-controller'; import type { AuthenticationController } from '@metamask/profile-sync-controller'; import { + TransactionControllerAddTransactionAction, TransactionControllerTransactionConfirmedEvent, TransactionControllerTransactionFailedEvent, TransactionControllerTransactionSubmittedEvent, @@ -19,8 +26,8 @@ import { TransactionStatus, } from '../types/transactionTypes'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../constants/eventNames'; import { ensureError } from '../../../../util/errorUtils'; import type { CandleData } from '../types/perps-types'; @@ -114,6 +121,7 @@ import type { RemoteFeatureFlagControllerGetStateAction, } from '@metamask/remote-feature-flag-controller'; import { wait } from '../utils/wait'; +import { getSelectedEvmAccount } from '../utils/accountUtils'; import { ORIGIN_METAMASK } from '@metamask/controller-utils'; // Re-export error codes from separate file to avoid circular dependencies @@ -627,12 +635,17 @@ export type PerpsControllerActions = }; /** - * External actions the PerpsController can call + * External actions the PerpsController can call via messenger */ export type AllowedActions = | NetworkControllerGetStateAction | AuthenticationController.AuthenticationControllerGetBearerToken - | RemoteFeatureFlagControllerGetStateAction; + | RemoteFeatureFlagControllerGetStateAction + | AccountTreeControllerGetAccountsFromSelectedAccountGroupAction + | KeyringControllerSignTypedMessageAction + | NetworkControllerGetNetworkClientByIdAction + | NetworkControllerFindNetworkClientIdByChainIdAction + | TransactionControllerAddTransactionAction; /** * External events the PerpsController can subscribe to @@ -750,18 +763,20 @@ export class PerpsController extends BaseController< infrastructure, }; - // Instantiate services with platform dependencies + // Instantiate services with platform dependencies and messenger + // Services that need inter-controller communication receive the messenger this.tradingService = new TradingService(infrastructure); this.marketDataService = new MarketDataService(infrastructure); - this.accountService = new AccountService(infrastructure); + this.accountService = new AccountService(infrastructure, messenger); this.eligibilityService = new EligibilityService(infrastructure); - this.dataLakeService = new DataLakeService(infrastructure); - this.depositService = new DepositService(infrastructure); + this.dataLakeService = new DataLakeService(infrastructure, messenger); + this.depositService = new DepositService(infrastructure, messenger); this.featureFlagConfigurationService = new FeatureFlagConfigurationService( infrastructure, ); this.rewardsIntegrationService = new RewardsIntegrationService( infrastructure, + messenger, ); // Set HIP-3 fallback configuration from client (will be updated if remote flags available) @@ -844,6 +859,49 @@ export class PerpsController extends BaseController< return this.options.infrastructure.metrics; } + // ============================================================================ + // Messenger-based Controller Access + // These methods use the messenger pattern for inter-controller communication + // ============================================================================ + + /** + * Find network client ID for a given chain via messenger + */ + private findNetworkClientIdForChain(chainId: string): string | undefined { + return this.messenger.call( + 'NetworkController:findNetworkClientIdByChainId', + chainId as `0x${string}`, + ); + } + + /** + * Submit a transaction via messenger (shows confirmation screen) + */ + private async submitTransaction( + txParams: { + from: string; + to?: string; + value?: string; + data?: string; + gas?: string; + }, + options: { + networkClientId: string; + origin?: string; + type?: TransactionType; + skipInitialGasEstimate?: boolean; + }, + ): Promise<{ + result: Promise; + transactionMeta: { id: string; hash?: string }; + }> { + return this.messenger.call( + 'TransactionController:addTransaction', + txParams, + options, + ); + } + /** * Clean up old withdrawal/deposit requests that don't have accountAddress * These are from before the accountAddress field was added and can't be displayed @@ -1093,6 +1151,7 @@ export class PerpsController extends BaseController< allowlistMarkets: this.hip3AllowlistMarkets, blocklistMarkets: this.hip3BlocklistMarkets, platformDependencies: this.options.infrastructure, + messenger: this.messenger, }); this.providers.set('hyperliquid', hyperLiquidProvider); @@ -1251,13 +1310,10 @@ export class PerpsController extends BaseController< /** * Ensure TradingService has controller dependencies set. - * Uses injectable dependencies from infrastructure for core migration compatibility. + * RewardsIntegrationService uses messenger internally for controller access. */ private ensureTradingServiceDeps(): void { - const { controllers } = this.options.infrastructure; this.tradingService.setControllerDependencies({ - controllers, - messenger: this.messenger, rewardsIntegrationService: this.rewardsIntegrationService, }); } @@ -1491,7 +1547,6 @@ export class PerpsController extends BaseController< */ async depositWithConfirmation(params: DepositWithConfirmationParams = {}) { const { amount, placeOrder } = params; - const { controllers } = this.options.infrastructure; try { // Clear any stale results when starting a new deposit flow @@ -1502,14 +1557,13 @@ export class PerpsController extends BaseController< const { transaction, assetChainId, currentDepositId } = await this.depositService.prepareTransaction({ provider }); + // Get current account address via messenger (outside of update() for proper typing) + const evmAccount = getSelectedEvmAccount(this.messenger); + const accountAddress = evmAccount?.address || 'unknown'; + this.update((state) => { state.lastDepositResult = null; - // Get current account address via infrastructure - const evmAccount = - this.options.infrastructure.controllers.accounts.getSelectedEvmAccount(); - const accountAddress = evmAccount?.address || 'unknown'; - // Add deposit request to tracking const depositRequest = { id: currentDepositId, @@ -1527,8 +1581,7 @@ export class PerpsController extends BaseController< state.depositRequests.unshift(depositRequest); // Add to beginning of array }); - const networkClientId = - controllers.network.findNetworkClientIdForChain(assetChainId); + const networkClientId = this.findNetworkClientIdForChain(assetChainId); if (!networkClientId) { throw new Error( @@ -1563,7 +1616,7 @@ export class PerpsController extends BaseController< } else { // submit shows the confirmation screen and returns a promise // The promise will resolve when transaction completes or reject if cancelled/failed - const submitResult = await controllers.transaction.submit(transaction, { + const submitResult = await this.submitTransaction(transaction, { ...defaultTransactionOptions, type: TransactionType.perpsDeposit, }); @@ -1762,11 +1815,11 @@ export class PerpsController extends BaseController< this.getMetrics().trackPerpsEvent( PerpsAnalyticsEvent.WithdrawalTransaction, { - [PerpsEventProperties.STATUS]: + [PERPS_EVENT_PROPERTY.STATUS]: status === 'completed' - ? PerpsEventValues.STATUS.COMPLETED - : PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.WITHDRAWAL_AMOUNT]: + ? PERPS_EVENT_VALUE.STATUS.COMPLETED + : PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.WITHDRAWAL_AMOUNT]: Number.parseFloat(withdrawalAmount), }, ); @@ -1953,6 +2006,7 @@ export class PerpsController extends BaseController< allowlistMarkets: this.hip3AllowlistMarkets, blocklistMarkets: this.hip3BlocklistMarkets, platformDependencies: this.options.infrastructure, + messenger: this.messenger, }); return provider.getMarkets(params); } diff --git a/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.test.ts b/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.test.ts index a20b41654eb..43258e3a944 100644 --- a/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.test.ts +++ b/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.test.ts @@ -1,6 +1,9 @@ import type { CaipAssetId, Hex } from '@metamask/utils'; import { HyperLiquidClientService } from '../../services/HyperLiquidClientService'; -import { createMockInfrastructure } from '../../__mocks__/serviceMocks'; +import { + createMockInfrastructure, + createMockMessenger, +} from '../../__mocks__/serviceMocks'; import { HyperLiquidSubscriptionService } from '../../services/HyperLiquidSubscriptionService'; import { HyperLiquidWalletService } from '../../services/HyperLiquidWalletService'; import { REFERRAL_CONFIG } from '../../constants/hyperLiquidConfig'; @@ -286,6 +289,7 @@ const createMockExchangeClient = (overrides: Record = {}) => ({ // Create shared mock platform dependencies for provider tests const mockPlatformDependencies: PerpsPlatformDependencies = createMockInfrastructure(); +const mockMessenger = createMockMessenger(); /** * Helper to create HyperLiquidProvider with mock platform dependencies @@ -302,6 +306,7 @@ const createTestProvider = ( new HyperLiquidProvider({ ...options, platformDependencies: mockPlatformDependencies, + messenger: mockMessenger, }); describe('HyperLiquidProvider', () => { diff --git a/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.ts b/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.ts index 5102cf7fbc4..455952196c7 100644 --- a/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.ts +++ b/app/components/UI/Perps/controllers/providers/HyperLiquidProvider.ts @@ -37,6 +37,7 @@ import { TradingReadinessCache, PerpsSigningCache, } from '../../services/TradingReadinessCache'; +import type { PerpsControllerMessenger } from '../PerpsController'; import { adaptAccountStateFromSDK, adaptHyperLiquidLedgerUpdateToUserHistoryItem, @@ -324,6 +325,7 @@ export class HyperLiquidProvider implements PerpsProvider { blocklistMarkets?: string[]; useDexAbstraction?: boolean; platformDependencies: PerpsPlatformDependencies; + messenger: PerpsControllerMessenger; }) { this.deps = options.platformDependencies; const isTestnet = options.isTestnet ?? false; @@ -336,9 +338,13 @@ export class HyperLiquidProvider implements PerpsProvider { // Attempt native balance abstraction, fallback to programmatic transfer if unsupported this.useDexAbstraction = options.useDexAbstraction ?? true; - // Initialize services with injected platform dependencies + // Initialize services with injected platform dependencies and messenger this.clientService = new HyperLiquidClientService(this.deps, { isTestnet }); - this.walletService = new HyperLiquidWalletService(this.deps, { isTestnet }); + this.walletService = new HyperLiquidWalletService( + this.deps, + options.messenger, + { isTestnet }, + ); this.subscriptionService = new HyperLiquidSubscriptionService( this.clientService, this.walletService, diff --git a/app/components/UI/Perps/controllers/services/AccountService.test.ts b/app/components/UI/Perps/controllers/services/AccountService.test.ts index 749c4bb3071..c92a0aead69 100644 --- a/app/components/UI/Perps/controllers/services/AccountService.test.ts +++ b/app/components/UI/Perps/controllers/services/AccountService.test.ts @@ -2,6 +2,7 @@ import { AccountService } from './AccountService'; import { createMockServiceContext, createMockInfrastructure, + createMockMessenger, } from '../../__mocks__/serviceMocks'; import { createMockHyperLiquidProvider } from '../../__mocks__/providerMocks'; import type { ServiceContext } from './ServiceContext'; @@ -12,17 +13,20 @@ import { type WithdrawResult, type PerpsPlatformDependencies, } from '../types'; -import type { PerpsControllerState } from '../PerpsController'; +import type { + PerpsControllerState, + PerpsControllerMessenger, +} from '../PerpsController'; jest.mock('uuid', () => ({ v4: () => 'mock-withdrawal-trace-id' })); jest.mock('../../constants/eventNames', () => ({ - PerpsEventProperties: { + PERPS_EVENT_PROPERTY: { STATUS: 'status', WITHDRAWAL_AMOUNT: 'withdrawal_amount', COMPLETION_DURATION: 'completion_duration', ERROR_MESSAGE: 'error_message', }, - PerpsEventValues: { + PERPS_EVENT_VALUE: { STATUS: { EXECUTED: 'executed', FAILED: 'failed', @@ -45,6 +49,7 @@ describe('AccountService', () => { let mockContext: ServiceContext; let mockRefreshAccountState: jest.Mock; let mockDeps: PerpsPlatformDependencies; + let mockMessenger: jest.Mocked; let accountService: AccountService; const mockWithdrawParams: WithdrawParams = { @@ -63,7 +68,8 @@ describe('AccountService', () => { // Create mock dependencies and service instance mockDeps = createMockInfrastructure(); - accountService = new AccountService(mockDeps); + mockMessenger = createMockMessenger(); + accountService = new AccountService(mockDeps, mockMessenger); jest.clearAllMocks(); diff --git a/app/components/UI/Perps/controllers/services/AccountService.ts b/app/components/UI/Perps/controllers/services/AccountService.ts index 8d8da2b0508..d9696211b61 100644 --- a/app/components/UI/Perps/controllers/services/AccountService.ts +++ b/app/components/UI/Perps/controllers/services/AccountService.ts @@ -9,11 +9,13 @@ import { type WithdrawResult, type PerpsPlatformDependencies, } from '../types'; +import type { PerpsControllerMessenger } from '../PerpsController'; import type { TransactionStatus } from '../../types/transactionTypes'; +import { getSelectedEvmAccount } from '../../utils/accountUtils'; import { v4 as uuidv4 } from 'uuid'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import { USDC_SYMBOL } from '../../constants/hyperLiquidConfig'; import { PERPS_ERROR_CODES } from '../perpsErrorCodes'; @@ -25,17 +27,24 @@ import { PERPS_ERROR_CODES } from '../perpsErrorCodes'; * Stateless service that delegates to provider. * Controller handles state updates and analytics. * - * Instance-based service with constructor injection of platform dependencies. + * Instance-based service with constructor injection of platform dependencies + * and messenger for inter-controller communication. */ export class AccountService { private readonly deps: PerpsPlatformDependencies; + private readonly messenger: PerpsControllerMessenger; /** * Create a new AccountService instance * @param deps - Platform dependencies for logging, metrics, etc. + * @param messenger - Messenger for inter-controller communication */ - constructor(deps: PerpsPlatformDependencies) { + constructor( + deps: PerpsPlatformDependencies, + messenger: PerpsControllerMessenger, + ) { this.deps = deps; + this.messenger = messenger; } /** @@ -98,9 +107,8 @@ export class AccountService { const feeAmount = 1.0; // HyperLiquid withdrawal fee is $1 USDC const netAmount = Math.max(0, grossAmount - feeAmount); - // Get current account address via controllers.accounts - const evmAccount = - this.deps.controllers.accounts.getSelectedEvmAccount(); + // Get current account address via messenger + const evmAccount = getSelectedEvmAccount(this.messenger); const accountAddress = evmAccount?.address || 'unknown'; this.deps.debugLogger.log( @@ -196,9 +204,9 @@ export class AccountService { this.deps.metrics.trackPerpsEvent( PerpsAnalyticsEvent.WithdrawalTransaction, { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.EXECUTED, - [PerpsEventProperties.WITHDRAWAL_AMOUNT]: parseFloat(params.amount), - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.EXECUTED, + [PERPS_EVENT_PROPERTY.WITHDRAWAL_AMOUNT]: parseFloat(params.amount), + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, }, ); @@ -259,10 +267,10 @@ export class AccountService { this.deps.metrics.trackPerpsEvent( PerpsAnalyticsEvent.WithdrawalTransaction, { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.WITHDRAWAL_AMOUNT]: parseFloat(params.amount), - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, - [PerpsEventProperties.ERROR_MESSAGE]: result.error || 'Unknown error', + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.WITHDRAWAL_AMOUNT]: parseFloat(params.amount), + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: result.error || 'Unknown error', }, ); @@ -317,10 +325,10 @@ export class AccountService { this.deps.metrics.trackPerpsEvent( PerpsAnalyticsEvent.WithdrawalTransaction, { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.WITHDRAWAL_AMOUNT]: params.amount, - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, - [PerpsEventProperties.ERROR_MESSAGE]: errorMessage, + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.WITHDRAWAL_AMOUNT]: params.amount, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: errorMessage, }, ); diff --git a/app/components/UI/Perps/controllers/services/DataLakeService.test.ts b/app/components/UI/Perps/controllers/services/DataLakeService.test.ts index 8cca720127b..c554f8849b6 100644 --- a/app/components/UI/Perps/controllers/services/DataLakeService.test.ts +++ b/app/components/UI/Perps/controllers/services/DataLakeService.test.ts @@ -3,9 +3,11 @@ import { createMockServiceContext, createMockEvmAccount, createMockInfrastructure, + createMockMessenger, } from '../../__mocks__/serviceMocks'; import type { ServiceContext } from './ServiceContext'; import type { PerpsPlatformDependencies } from '../types'; +import type { PerpsControllerMessenger } from '../PerpsController'; jest.mock('uuid', () => ({ v4: () => 'mock-trace-id' })); @@ -18,13 +20,15 @@ global.setTimeout = jest.fn((fn: () => void) => { describe('DataLakeService', () => { let mockContext: ServiceContext; let mockDeps: jest.Mocked; + let mockMessenger: jest.Mocked; let dataLakeService: DataLakeService; const mockEvmAccount = createMockEvmAccount(); const mockToken = 'mock-bearer-token'; beforeEach(() => { mockDeps = createMockInfrastructure(); - dataLakeService = new DataLakeService(mockDeps); + mockMessenger = createMockMessenger(); + dataLakeService = new DataLakeService(mockDeps, mockMessenger); mockContext = createMockServiceContext({ errorContext: { controller: 'DataLakeService', method: 'test' }, @@ -34,12 +38,18 @@ describe('DataLakeService', () => { }, }); - ( - mockDeps.controllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockReturnValue(mockEvmAccount); - ( - mockDeps.controllers.authentication.getBearerToken as jest.Mock - ).mockResolvedValue(mockToken); + // Configure messenger to return expected values + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [mockEvmAccount]; + } + if (action === 'AuthenticationController:getBearerToken') { + return Promise.resolve(mockToken); + } + return undefined; + }); jest.clearAllMocks(); }); @@ -81,9 +91,9 @@ describe('DataLakeService', () => { }); expect(result).toEqual({ success: true }); - expect( - mockDeps.controllers.authentication.getBearerToken, - ).toHaveBeenCalled(); + expect(mockMessenger.call).toHaveBeenCalledWith( + 'AuthenticationController:getBearerToken', + ); expect(fetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ @@ -135,9 +145,17 @@ describe('DataLakeService', () => { }); it('returns error when account is missing', async () => { - ( - mockDeps.controllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockReturnValue(null); + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return []; + } + if (action === 'AuthenticationController:getBearerToken') { + return Promise.resolve(mockToken); + } + return undefined; + }); const result = await dataLakeService.reportOrder({ action: 'open', @@ -158,9 +176,17 @@ describe('DataLakeService', () => { }); it('returns error when token is missing', async () => { - ( - mockDeps.controllers.authentication.getBearerToken as jest.Mock - ).mockResolvedValue(null); + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [mockEvmAccount]; + } + if (action === 'AuthenticationController:getBearerToken') { + return Promise.resolve(null); + } + return undefined; + }); const result = await dataLakeService.reportOrder({ action: 'open', diff --git a/app/components/UI/Perps/controllers/services/DataLakeService.ts b/app/components/UI/Perps/controllers/services/DataLakeService.ts index 436d041c8a2..65fb462825e 100644 --- a/app/components/UI/Perps/controllers/services/DataLakeService.ts +++ b/app/components/UI/Perps/controllers/services/DataLakeService.ts @@ -8,6 +8,8 @@ import { PerpsTraceOperations, type PerpsPlatformDependencies, } from '../types'; +import type { PerpsControllerMessenger } from '../PerpsController'; +import { getSelectedEvmAccount } from '../../utils/accountUtils'; /** * DataLakeService @@ -16,31 +18,31 @@ import { * Implements exponential backoff retry logic and performance tracing. * Stateless service that operates purely on external API calls. * - * Instance-based service with constructor injection of platform dependencies. + * Instance-based service with constructor injection of platform dependencies + * and messenger for inter-controller communication. */ export class DataLakeService { private readonly deps: PerpsPlatformDependencies; + private readonly messenger: PerpsControllerMessenger; /** * Create a new DataLakeService instance * @param deps - Platform dependencies for logging, metrics, etc. + * @param messenger - Messenger for inter-controller communication */ - constructor(deps: PerpsPlatformDependencies) { + constructor( + deps: PerpsPlatformDependencies, + messenger: PerpsControllerMessenger, + ) { this.deps = deps; + this.messenger = messenger; } /** - * Error context helper for consistent logging + * Get bearer token via messenger */ - private getErrorContext( - method: string, - additionalContext?: Record, - ): Record { - return { - controller: 'DataLakeService', - method, - ...additionalContext, - }; + private async getBearerToken(): Promise { + return this.messenger.call('AuthenticationController:getBearerToken'); } /** @@ -124,8 +126,8 @@ export class DataLakeService { const apiCallStartTime = this.deps.performance.now(); try { - const token = await this.deps.controllers.authentication.getBearerToken(); - const evmAccount = this.deps.controllers.accounts.getSelectedEvmAccount(); + const token = await this.getBearerToken(); + const evmAccount = getSelectedEvmAccount(this.messenger); if (!evmAccount || !token) { this.deps.debugLogger.log('DataLake API: Missing requirements', { diff --git a/app/components/UI/Perps/controllers/services/DepositService.test.ts b/app/components/UI/Perps/controllers/services/DepositService.test.ts index 34a6dbad6e7..4bfeca61f7f 100644 --- a/app/components/UI/Perps/controllers/services/DepositService.test.ts +++ b/app/components/UI/Perps/controllers/services/DepositService.test.ts @@ -3,12 +3,14 @@ import { createMockHyperLiquidProvider } from '../../__mocks__/providerMocks'; import { createMockEvmAccount, createMockInfrastructure, + createMockMessenger, } from '../../__mocks__/serviceMocks'; import { generateDepositId } from '../../utils/idUtils'; import { toHex } from '@metamask/controller-utils'; import { parseCaipAssetId } from '@metamask/utils'; import { generateTransferData } from '../../../../../util/transactions'; import type { PerpsProvider, PerpsPlatformDependencies } from '../types'; +import type { PerpsControllerMessenger } from '../PerpsController'; jest.mock('../../utils/idUtils'); jest.mock('@metamask/utils'); @@ -33,6 +35,7 @@ jest.mock('@metamask/controller-utils', () => { describe('DepositService', () => { let mockProvider: jest.Mocked; let mockDeps: jest.Mocked; + let mockMessenger: jest.Mocked; let service: DepositService; const mockEvmAccount = createMockEvmAccount(); const mockDepositId = 'deposit-123'; @@ -45,7 +48,8 @@ describe('DepositService', () => { createMockHyperLiquidProvider() as unknown as jest.Mocked; mockDeps = createMockInfrastructure(); - service = new DepositService(mockDeps); + mockMessenger = createMockMessenger(); + service = new DepositService(mockDeps, mockMessenger); mockProvider.getDepositRoutes.mockReturnValue([ { @@ -55,10 +59,15 @@ describe('DepositService', () => { }, ]); - // Setup mock EVM account via dependency injection - mockDeps.controllers.accounts.getSelectedEvmAccount = jest - .fn() - .mockReturnValue(mockEvmAccount); + // Setup mock EVM account via messenger + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [mockEvmAccount]; + } + return undefined; + }); (generateDepositId as jest.Mock).mockReturnValue(mockDepositId); // Mock generateTransferData to return a valid ERC-20 transfer data (generateTransferData as jest.Mock).mockReturnValue( @@ -153,20 +162,25 @@ describe('DepositService', () => { expect(result.transaction.data).toMatch(/^0xa9059cbb/); }); - it('retrieves EVM account from selected account group via dependency injection', async () => { + it('retrieves EVM account from selected account group via messenger', async () => { await service.prepareTransaction({ provider: mockProvider, }); - expect( - mockDeps.controllers.accounts.getSelectedEvmAccount, - ).toHaveBeenCalledTimes(1); + expect(mockMessenger.call).toHaveBeenCalledWith( + 'AccountTreeController:getAccountsFromSelectedAccountGroup', + ); }); it('throws error when no EVM account is found', async () => { - mockDeps.controllers.accounts.getSelectedEvmAccount = jest - .fn() - .mockReturnValue(null); + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return []; + } + return undefined; + }); await expect( service.prepareTransaction({ @@ -318,7 +332,8 @@ describe('DepositService', () => { describe('instance isolation', () => { it('each instance uses its own deps', async () => { const mockDeps2 = createMockInfrastructure(); - const service2 = new DepositService(mockDeps2); + const mockMessenger2 = createMockMessenger(); + const service2 = new DepositService(mockDeps2, mockMessenger2); await service.prepareTransaction({ provider: mockProvider }); await service2.prepareTransaction({ provider: mockProvider }); diff --git a/app/components/UI/Perps/controllers/services/DepositService.ts b/app/components/UI/Perps/controllers/services/DepositService.ts index a78a4db1c6c..583d4b281d6 100644 --- a/app/components/UI/Perps/controllers/services/DepositService.ts +++ b/app/components/UI/Perps/controllers/services/DepositService.ts @@ -6,6 +6,8 @@ import type { TransactionParams } from '@metamask/transaction-controller'; import { generateTransferData } from '../../../../../util/transactions'; import { generateDepositId } from '../../utils/idUtils'; import type { PerpsProvider, PerpsPlatformDependencies } from '../types'; +import type { PerpsControllerMessenger } from '../PerpsController'; +import { getSelectedEvmAccount } from '../../utils/accountUtils'; // Temporary to avoid estimation failures due to insufficient balance const DEPOSIT_GAS_LIMIT = toHex(100000); @@ -17,17 +19,24 @@ const DEPOSIT_GAS_LIMIT = toHex(100000); * Stateless service that prepares transaction data for TransactionController. * Controller handles TransactionController integration and promise lifecycle. * - * Instance-based service with constructor injection of platform dependencies. + * Instance-based service with constructor injection of platform dependencies + * and messenger for inter-controller communication. */ export class DepositService { private readonly deps: PerpsPlatformDependencies; + private readonly messenger: PerpsControllerMessenger; /** * Create a new DepositService instance * @param deps - Platform dependencies for logging, metrics, etc. + * @param messenger - Messenger for inter-controller communication */ - constructor(deps: PerpsPlatformDependencies) { + constructor( + deps: PerpsPlatformDependencies, + messenger: PerpsControllerMessenger, + ) { this.deps = deps; + this.messenger = messenger; } /** @@ -61,8 +70,8 @@ export class DepositService { amount: '0x0', }); - // Get EVM account from selected account group via dependency injection - const evmAccount = this.deps.controllers.accounts.getSelectedEvmAccount(); + // Get EVM account from selected account group via messenger + const evmAccount = getSelectedEvmAccount(this.messenger); if (!evmAccount) { throw new Error( 'No EVM-compatible account found in selected account group', diff --git a/app/components/UI/Perps/controllers/services/RewardsIntegrationService.test.ts b/app/components/UI/Perps/controllers/services/RewardsIntegrationService.test.ts index bca3a930e4b..3bece730206 100644 --- a/app/components/UI/Perps/controllers/services/RewardsIntegrationService.test.ts +++ b/app/components/UI/Perps/controllers/services/RewardsIntegrationService.test.ts @@ -2,58 +2,21 @@ import { RewardsIntegrationService } from './RewardsIntegrationService'; import { createMockEvmAccount, createMockInfrastructure, + createMockMessenger, } from '../../__mocks__/serviceMocks'; import type { PerpsControllerMessenger } from '../PerpsController'; -import type { - PerpsPlatformDependencies, - PerpsControllerAccess, -} from '../types'; - -// Helper to get rewards mock with type safety -const getRewardsMock = (controllers: jest.Mocked) => { - if (!controllers.rewards) { - throw new Error('rewards mock not set up'); - } - return controllers.rewards; -}; +import type { PerpsPlatformDependencies } from '../types'; describe('RewardsIntegrationService', () => { - let mockControllers: jest.Mocked; let mockMessenger: jest.Mocked; let mockDeps: jest.Mocked; let service: RewardsIntegrationService; const mockEvmAccount = createMockEvmAccount(); beforeEach(() => { - mockControllers = { - accounts: { - getSelectedEvmAccount: jest.fn(), - formatAccountToCaipId: jest.fn(), - }, - keyring: { - signTypedMessage: jest.fn(), - }, - network: { - getChainIdForNetwork: jest.fn(), - findNetworkClientIdForChain: jest.fn(), - }, - transaction: { - submit: jest.fn(), - }, - rewards: { - getFeeDiscount: jest.fn(), - }, - authentication: { - getBearerToken: jest.fn(), - }, - } as unknown as jest.Mocked; - - mockMessenger = { - call: jest.fn(), - } as unknown as jest.Mocked; - + mockMessenger = createMockMessenger(); mockDeps = createMockInfrastructure(); - service = new RewardsIntegrationService(mockDeps); + service = new RewardsIntegrationService(mockDeps, mockMessenger); jest.clearAllMocks(); }); @@ -65,37 +28,32 @@ describe('RewardsIntegrationService', () => { describe('calculateUserFeeDiscount', () => { it('calculates fee discount successfully with valid discount', async () => { const mockDiscountBips = 6500; // 65% - const mockCaipAccountId = - 'eip155:1:0x1234567890abcdef1234567890abcdef12345678'; - - ( - mockControllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockReturnValue(mockEvmAccount); - ( - mockControllers.accounts.formatAccountToCaipId as jest.Mock - ).mockReturnValue(mockCaipAccountId); - (mockMessenger.call as jest.Mock).mockReturnValue({ - selectedNetworkClientId: 'mainnet', - }); - ( - mockControllers.network.getChainIdForNetwork as jest.Mock - ).mockReturnValue('0x1'); - ( - getRewardsMock(mockControllers).getFeeDiscount as jest.Mock - ).mockResolvedValue(mockDiscountBips); - - const result = await service.calculateUserFeeDiscount({ - controllers: mockControllers, - messenger: mockMessenger, + + // Configure messenger to return expected values + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [mockEvmAccount]; + } + if (action === 'NetworkController:getState') { + return { selectedNetworkClientId: 'mainnet' }; + } + if (action === 'NetworkController:getNetworkClientById') { + return { configuration: { chainId: '0x1' } }; + } + return undefined; }); + (mockDeps.rewards.getFeeDiscount as jest.Mock).mockResolvedValue( + mockDiscountBips, + ); + + const result = await service.calculateUserFeeDiscount(); expect(result).toBe(6500); - expect( - getRewardsMock(mockControllers).getFeeDiscount, - ).toHaveBeenCalledWith(mockCaipAccountId); - expect( - mockControllers.accounts.formatAccountToCaipId, - ).toHaveBeenCalledWith(mockEvmAccount.address, '0x1'); + expect(mockDeps.rewards.getFeeDiscount).toHaveBeenCalledWith( + expect.stringMatching(/^eip155:1:0x/), + ); expect(mockDeps.debugLogger.log).toHaveBeenCalledWith( 'RewardsIntegrationService: Fee discount calculated', expect.objectContaining({ @@ -106,146 +64,90 @@ describe('RewardsIntegrationService', () => { }); it('returns undefined when no discount available', async () => { - const mockCaipAccountId = - 'eip155:1:0x1234567890abcdef1234567890abcdef12345678'; - - ( - mockControllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockReturnValue(mockEvmAccount); - (mockMessenger.call as jest.Mock).mockReturnValue({ - selectedNetworkClientId: 'mainnet', - }); - ( - mockControllers.network.getChainIdForNetwork as jest.Mock - ).mockReturnValue('0x1'); - ( - mockControllers.accounts.formatAccountToCaipId as jest.Mock - ).mockReturnValue(mockCaipAccountId); - ( - getRewardsMock(mockControllers).getFeeDiscount as jest.Mock - ).mockResolvedValue(0); - - const result = await service.calculateUserFeeDiscount({ - controllers: mockControllers, - messenger: mockMessenger, + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [mockEvmAccount]; + } + if (action === 'NetworkController:getState') { + return { selectedNetworkClientId: 'mainnet' }; + } + if (action === 'NetworkController:getNetworkClientById') { + return { configuration: { chainId: '0x1' } }; + } + return undefined; }); + (mockDeps.rewards.getFeeDiscount as jest.Mock).mockResolvedValue(0); + + const result = await service.calculateUserFeeDiscount(); expect(result).toBe(0); }); it('returns undefined when no EVM account found', async () => { - ( - mockControllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockReturnValue(null); - - const result = await service.calculateUserFeeDiscount({ - controllers: mockControllers, - messenger: mockMessenger, + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return []; + } + return undefined; }); + const result = await service.calculateUserFeeDiscount(); + expect(result).toBeUndefined(); expect(mockDeps.debugLogger.log).toHaveBeenCalledWith( 'RewardsIntegrationService: No EVM account found for fee discount', ); - expect( - getRewardsMock(mockControllers).getFeeDiscount, - ).not.toHaveBeenCalled(); + expect(mockDeps.rewards.getFeeDiscount).not.toHaveBeenCalled(); }); it('returns undefined when chain ID not found', async () => { - ( - mockControllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockReturnValue(mockEvmAccount); - (mockMessenger.call as jest.Mock).mockReturnValue({ - selectedNetworkClientId: 'mainnet', - }); - ( - mockControllers.network.getChainIdForNetwork as jest.Mock - ).mockImplementation(() => { - throw new Error('Network client not found'); + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [mockEvmAccount]; + } + if (action === 'NetworkController:getState') { + return { selectedNetworkClientId: 'mainnet' }; + } + if (action === 'NetworkController:getNetworkClientById') { + throw new Error('Network client not found'); + } + return undefined; }); - const result = await service.calculateUserFeeDiscount({ - controllers: mockControllers, - messenger: mockMessenger, - }); - - expect(result).toBeUndefined(); - expect(mockDeps.logger.error).toHaveBeenCalledWith( - expect.any(Error), - expect.objectContaining({ - context: expect.objectContaining({ - name: 'RewardsIntegrationService.calculateUserFeeDiscount', - }), - }), - ); - expect( - getRewardsMock(mockControllers).getFeeDiscount, - ).not.toHaveBeenCalled(); - }); - - it('returns undefined when CAIP account ID formatting fails', async () => { - ( - mockControllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockReturnValue(mockEvmAccount); - (mockMessenger.call as jest.Mock).mockReturnValue({ - selectedNetworkClientId: 'mainnet', - }); - ( - mockControllers.network.getChainIdForNetwork as jest.Mock - ).mockReturnValue('0x1'); - ( - mockControllers.accounts.formatAccountToCaipId as jest.Mock - ).mockReturnValue(null); - - const result = await service.calculateUserFeeDiscount({ - controllers: mockControllers, - messenger: mockMessenger, - }); + const result = await service.calculateUserFeeDiscount(); expect(result).toBeUndefined(); - expect(mockDeps.logger.error).toHaveBeenCalledWith( - expect.any(Error), - expect.objectContaining({ - context: expect.objectContaining({ - name: 'RewardsIntegrationService.calculateUserFeeDiscount', - data: expect.objectContaining({ - address: mockEvmAccount.address, - chainId: '0x1', - }), - }), - }), - ); - expect( - getRewardsMock(mockControllers).getFeeDiscount, - ).not.toHaveBeenCalled(); + expect(mockDeps.rewards.getFeeDiscount).not.toHaveBeenCalled(); }); it('returns undefined when getFeeDiscount throws error', async () => { const mockError = new Error('Rewards API error'); - const mockCaipAccountId = - 'eip155:1:0x1234567890abcdef1234567890abcdef12345678'; - - ( - mockControllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockReturnValue(mockEvmAccount); - (mockMessenger.call as jest.Mock).mockReturnValue({ - selectedNetworkClientId: 'mainnet', - }); - ( - mockControllers.network.getChainIdForNetwork as jest.Mock - ).mockReturnValue('0x1'); - ( - mockControllers.accounts.formatAccountToCaipId as jest.Mock - ).mockReturnValue(mockCaipAccountId); - ( - getRewardsMock(mockControllers).getFeeDiscount as jest.Mock - ).mockRejectedValue(mockError); - - const result = await service.calculateUserFeeDiscount({ - controllers: mockControllers, - messenger: mockMessenger, + + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [mockEvmAccount]; + } + if (action === 'NetworkController:getState') { + return { selectedNetworkClientId: 'mainnet' }; + } + if (action === 'NetworkController:getNetworkClientById') { + return { configuration: { chainId: '0x1' } }; + } + return undefined; }); + (mockDeps.rewards.getFeeDiscount as jest.Mock).mockRejectedValue( + mockError, + ); + + const result = await service.calculateUserFeeDiscount(); expect(result).toBeUndefined(); expect(mockDeps.logger.error).toHaveBeenCalledWith( @@ -261,17 +163,19 @@ describe('RewardsIntegrationService', () => { it('returns undefined when NetworkController throws error', async () => { const mockError = new Error('Network error'); - ( - mockControllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockReturnValue(mockEvmAccount); - (mockMessenger.call as jest.Mock).mockImplementation(() => { - throw mockError; + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [mockEvmAccount]; + } + if (action === 'NetworkController:getState') { + throw mockError; + } + return undefined; }); - const result = await service.calculateUserFeeDiscount({ - controllers: mockControllers, - messenger: mockMessenger, - }); + const result = await service.calculateUserFeeDiscount(); expect(result).toBeUndefined(); expect(mockDeps.logger.error).toHaveBeenCalled(); @@ -288,37 +192,31 @@ describe('RewardsIntegrationService', () => { // Reset only specific mocks, keeping mockDeps intact jest.clearAllMocks(); mockDeps = createMockInfrastructure(); - service = new RewardsIntegrationService(mockDeps); - - const mockCaipAccountId = `eip155:${parseInt(chain.chainId, 16)}:${mockEvmAccount.address}`; - - // Mock the passed mockControllers.accounts methods - ( - mockControllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockReturnValue(mockEvmAccount); - ( - mockControllers.accounts.formatAccountToCaipId as jest.Mock - ).mockReturnValue(mockCaipAccountId); - - (mockMessenger.call as jest.Mock).mockReturnValue({ - selectedNetworkClientId: chain.name.toLowerCase(), - }); - ( - mockControllers.network.getChainIdForNetwork as jest.Mock - ).mockReturnValue(chain.chainId as `0x${string}`); - ( - getRewardsMock(mockControllers).getFeeDiscount as jest.Mock - ).mockResolvedValue(5000); - - const result = await service.calculateUserFeeDiscount({ - controllers: mockControllers, - messenger: mockMessenger, - }); + mockMessenger = createMockMessenger(); + service = new RewardsIntegrationService(mockDeps, mockMessenger); + + (mockMessenger.call as jest.Mock).mockImplementation( + (action: string) => { + if ( + action === + 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [mockEvmAccount]; + } + if (action === 'NetworkController:getState') { + return { selectedNetworkClientId: chain.name.toLowerCase() }; + } + if (action === 'NetworkController:getNetworkClientById') { + return { configuration: { chainId: chain.chainId } }; + } + return undefined; + }, + ); + (mockDeps.rewards.getFeeDiscount as jest.Mock).mockResolvedValue(5000); + + const result = await service.calculateUserFeeDiscount(); expect(result).toBe(5000); - expect( - mockControllers.accounts.formatAccountToCaipId, - ).toHaveBeenCalledWith(mockEvmAccount.address, chain.chainId); } }); @@ -334,29 +232,28 @@ describe('RewardsIntegrationService', () => { for (const testCase of testCases) { jest.clearAllMocks(); - const mockCaipAccountId = - 'eip155:1:0x1234567890abcdef1234567890abcdef12345678'; - - ( - mockControllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockReturnValue(mockEvmAccount); - (mockMessenger.call as jest.Mock).mockReturnValue({ - selectedNetworkClientId: 'mainnet', - }); - ( - mockControllers.network.getChainIdForNetwork as jest.Mock - ).mockReturnValue('0x1'); - ( - mockControllers.accounts.formatAccountToCaipId as jest.Mock - ).mockReturnValue(mockCaipAccountId); - ( - getRewardsMock(mockControllers).getFeeDiscount as jest.Mock - ).mockResolvedValue(testCase.bips); - - await service.calculateUserFeeDiscount({ - controllers: mockControllers, - messenger: mockMessenger, - }); + (mockMessenger.call as jest.Mock).mockImplementation( + (action: string) => { + if ( + action === + 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [mockEvmAccount]; + } + if (action === 'NetworkController:getState') { + return { selectedNetworkClientId: 'mainnet' }; + } + if (action === 'NetworkController:getNetworkClientById') { + return { configuration: { chainId: '0x1' } }; + } + return undefined; + }, + ); + (mockDeps.rewards.getFeeDiscount as jest.Mock).mockResolvedValue( + testCase.bips, + ); + + await service.calculateUserFeeDiscount(); expect(mockDeps.debugLogger.log).toHaveBeenCalledWith( 'RewardsIntegrationService: Fee discount calculated', @@ -372,22 +269,33 @@ describe('RewardsIntegrationService', () => { describe('instance isolation', () => { it('each instance uses its own deps', async () => { const mockDeps2 = createMockInfrastructure(); - const service2 = new RewardsIntegrationService(mockDeps2); - - // First service - mock the passed controllers - ( - mockControllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockReturnValue(null); - await service.calculateUserFeeDiscount({ - controllers: mockControllers, - messenger: mockMessenger, - }); - - // Second service - uses same mockControllers but different mockDeps - await service2.calculateUserFeeDiscount({ - controllers: mockControllers, - messenger: mockMessenger, + const mockMessenger2 = createMockMessenger(); + const service2 = new RewardsIntegrationService(mockDeps2, mockMessenger2); + + // First service - mock messenger to return empty array (no EVM account) + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return []; + } + return undefined; }); + await service.calculateUserFeeDiscount(); + + // Second service - uses same mock pattern + (mockMessenger2.call as jest.Mock).mockImplementation( + (action: string) => { + if ( + action === + 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return []; + } + return undefined; + }, + ); + await service2.calculateUserFeeDiscount(); // Each instance should use its own logger expect(mockDeps.debugLogger.log).toHaveBeenCalledTimes(1); diff --git a/app/components/UI/Perps/controllers/services/RewardsIntegrationService.ts b/app/components/UI/Perps/controllers/services/RewardsIntegrationService.ts index 3edb78463f6..293fc62b772 100644 --- a/app/components/UI/Perps/controllers/services/RewardsIntegrationService.ts +++ b/app/components/UI/Perps/controllers/services/RewardsIntegrationService.ts @@ -1,9 +1,8 @@ import { ensureError } from '../../../../../util/errorUtils'; +import { formatAccountToCaipAccountId } from '../../utils/rewardsUtils'; import type { PerpsControllerMessenger } from '../PerpsController'; -import type { - PerpsPlatformDependencies, - PerpsControllerAccess, -} from '../types'; +import type { PerpsPlatformDependencies } from '../types'; +import { getSelectedEvmAccount } from '../../utils/accountUtils'; /** * RewardsIntegrationService @@ -11,34 +10,49 @@ import type { * Handles rewards-related operations and fee discount calculations. * Stateless service that coordinates with RewardsController and NetworkController. * - * Instance-based service with constructor injection of platform dependencies. + * Instance-based service with constructor injection of platform dependencies + * and messenger for inter-controller communication. */ export class RewardsIntegrationService { private readonly deps: PerpsPlatformDependencies; + private readonly messenger: PerpsControllerMessenger; /** * Create a new RewardsIntegrationService instance * @param deps - Platform dependencies for logging, metrics, etc. + * @param messenger - Messenger for inter-controller communication */ - constructor(deps: PerpsPlatformDependencies) { + constructor( + deps: PerpsPlatformDependencies, + messenger: PerpsControllerMessenger, + ) { this.deps = deps; + this.messenger = messenger; + } + + /** + * Get chain ID for a network client via messenger + */ + private getChainIdForNetwork(networkClientId: string): string | undefined { + try { + const networkClient = this.messenger.call( + 'NetworkController:getNetworkClientById', + networkClientId, + ); + return networkClient.configuration.chainId; + } catch { + // Network client may not exist + return undefined; + } } /** * Calculate user fee discount from rewards * Returns discount in basis points (e.g., 6500 = 65% discount) - * - * @param options.controllers - Consolidated controller access interface - * @param options.messenger - Controller messenger for network state access */ - async calculateUserFeeDiscount(options: { - controllers: PerpsControllerAccess; - messenger: PerpsControllerMessenger; - }): Promise { - const { controllers, messenger } = options; - + async calculateUserFeeDiscount(): Promise { try { - const evmAccount = controllers.accounts.getSelectedEvmAccount(); + const evmAccount = getSelectedEvmAccount(this.messenger); if (!evmAccount) { this.deps.debugLogger.log( @@ -47,19 +61,10 @@ export class RewardsIntegrationService { return undefined; } - // Get the chain ID using controllers.network - const networkState = messenger.call('NetworkController:getState'); + // Get the chain ID using messenger + const networkState = this.messenger.call('NetworkController:getState'); const selectedNetworkClientId = networkState.selectedNetworkClientId; - let chainId: string | undefined; - - try { - chainId = controllers.network.getChainIdForNetwork( - selectedNetworkClientId, - ); - } catch { - // Network client may not exist - chainId = undefined; - } + const chainId = this.getChainIdForNetwork(selectedNetworkClientId); if (!chainId) { this.deps.logger.error( @@ -76,7 +81,8 @@ export class RewardsIntegrationService { return undefined; } - const caipAccountId = controllers.accounts.formatAccountToCaipId( + // Use pure utility function for CAIP formatting + const caipAccountId = formatAccountToCaipAccountId( evmAccount.address, chainId, ); @@ -98,7 +104,8 @@ export class RewardsIntegrationService { return undefined; } - const discountBips = await controllers.rewards.getFeeDiscount( + // Use rewards from deps (stays as DI - no messenger action in core) + const discountBips = await this.deps.rewards.getFeeDiscount( caipAccountId as `${string}:${string}:${string}`, ); diff --git a/app/components/UI/Perps/controllers/services/TradingService.test.ts b/app/components/UI/Perps/controllers/services/TradingService.test.ts index 8b60020b080..33e6762c96e 100644 --- a/app/components/UI/Perps/controllers/services/TradingService.test.ts +++ b/app/components/UI/Perps/controllers/services/TradingService.test.ts @@ -53,8 +53,6 @@ describe('TradingService', () => { }; // Set controller dependencies for fee discount calculation tradingService.setControllerDependencies({ - controllers: mockDeps.controllers, - messenger: {} as never, rewardsIntegrationService: mockRewardsIntegrationService as never, }); mockProvider = diff --git a/app/components/UI/Perps/controllers/services/TradingService.ts b/app/components/UI/Perps/controllers/services/TradingService.ts index ffcdd848afa..8fe3a7cd040 100644 --- a/app/components/UI/Perps/controllers/services/TradingService.ts +++ b/app/components/UI/Perps/controllers/services/TradingService.ts @@ -4,17 +4,15 @@ import { v4 as uuidv4 } from 'uuid'; import { PerpsMeasurementName } from '../../constants/performanceMetrics'; import type { RewardsIntegrationService } from './RewardsIntegrationService'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../../constants/eventNames'; import type { ServiceContext } from './ServiceContext'; -import type { PerpsControllerMessenger } from '../PerpsController'; import { PerpsAnalyticsEvent, PerpsTraceNames, PerpsTraceOperations, type PerpsProvider, - type PerpsControllerAccess, type OrderParams, type OrderResult, type EditOrderParams, @@ -36,8 +34,6 @@ import { * These are singletons that don't change per-call, injected once via setControllerDependencies(). */ export interface TradingServiceControllerDeps { - controllers: PerpsControllerAccess; - messenger: PerpsControllerMessenger; rewardsIntegrationService: RewardsIntegrationService; } @@ -111,78 +107,78 @@ export class TradingService { const status = result?.success === true - ? PerpsEventValues.STATUS.EXECUTED - : PerpsEventValues.STATUS.FAILED; + ? PERPS_EVENT_VALUE.STATUS.EXECUTED + : PERPS_EVENT_VALUE.STATUS.FAILED; // Build base properties const properties: PerpsAnalyticsProperties = { - [PerpsEventProperties.STATUS]: status, - [PerpsEventProperties.ASSET]: params.symbol, - [PerpsEventProperties.DIRECTION]: params.isBuy - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.ORDER_TYPE]: params.orderType, - [PerpsEventProperties.LEVERAGE]: parseFloat(String(params.leverage || 1)), - [PerpsEventProperties.ORDER_SIZE]: parseFloat( + [PERPS_EVENT_PROPERTY.STATUS]: status, + [PERPS_EVENT_PROPERTY.ASSET]: params.symbol, + [PERPS_EVENT_PROPERTY.DIRECTION]: params.isBuy + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.ORDER_TYPE]: params.orderType, + [PERPS_EVENT_PROPERTY.LEVERAGE]: parseFloat(String(params.leverage || 1)), + [PERPS_EVENT_PROPERTY.ORDER_SIZE]: parseFloat( result?.filledSize || params.size, ), - [PerpsEventProperties.COMPLETION_DURATION]: duration, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: duration, }; // Add optional properties if (params.trackingData?.marginUsed != null) { - properties[PerpsEventProperties.MARGIN_USED] = + properties[PERPS_EVENT_PROPERTY.MARGIN_USED] = params.trackingData.marginUsed; } if (params.trackingData?.totalFee != null) { - properties[PerpsEventProperties.FEES] = params.trackingData.totalFee; + properties[PERPS_EVENT_PROPERTY.FEES] = params.trackingData.totalFee; } if (result?.averagePrice || params.trackingData?.marketPrice) { - properties[PerpsEventProperties.ASSET_PRICE] = result?.averagePrice + properties[PERPS_EVENT_PROPERTY.ASSET_PRICE] = result?.averagePrice ? parseFloat(result.averagePrice) : params.trackingData?.marketPrice; } if (params.orderType === 'limit' && params.price) { - properties[PerpsEventProperties.LIMIT_PRICE] = parseFloat(params.price); + properties[PERPS_EVENT_PROPERTY.LIMIT_PRICE] = parseFloat(params.price); } if (params.trackingData?.source) { - properties[PerpsEventProperties.SOURCE] = params.trackingData.source; + properties[PERPS_EVENT_PROPERTY.SOURCE] = params.trackingData.source; } if (params.trackingData?.tradeAction) { - properties[PerpsEventProperties.ACTION] = params.trackingData.tradeAction; + properties[PERPS_EVENT_PROPERTY.ACTION] = params.trackingData.tradeAction; } // Add success-specific properties - if (status === PerpsEventValues.STATUS.EXECUTED) { + if (status === PERPS_EVENT_VALUE.STATUS.EXECUTED) { if (params.trackingData?.metamaskFee != null) { - properties[PerpsEventProperties.METAMASK_FEE] = + properties[PERPS_EVENT_PROPERTY.METAMASK_FEE] = params.trackingData.metamaskFee; } if (params.trackingData?.metamaskFeeRate != null) { - properties[PerpsEventProperties.METAMASK_FEE_RATE] = + properties[PERPS_EVENT_PROPERTY.METAMASK_FEE_RATE] = params.trackingData.metamaskFeeRate; } if (params.trackingData?.feeDiscountPercentage != null) { - properties[PerpsEventProperties.DISCOUNT_PERCENTAGE] = + properties[PERPS_EVENT_PROPERTY.DISCOUNT_PERCENTAGE] = params.trackingData.feeDiscountPercentage; } if (params.trackingData?.estimatedPoints != null) { - properties[PerpsEventProperties.ESTIMATED_REWARDS] = + properties[PERPS_EVENT_PROPERTY.ESTIMATED_REWARDS] = params.trackingData.estimatedPoints; } if (params.takeProfitPrice) { - properties[PerpsEventProperties.TAKE_PROFIT_PRICE] = parseFloat( + properties[PERPS_EVENT_PROPERTY.TAKE_PROFIT_PRICE] = parseFloat( params.takeProfitPrice, ); } if (params.stopLossPrice) { - properties[PerpsEventProperties.STOP_LOSS_PRICE] = parseFloat( + properties[PERPS_EVENT_PROPERTY.STOP_LOSS_PRICE] = parseFloat( params.stopLossPrice, ); } } else { // Add failure-specific properties - properties[PerpsEventProperties.ERROR_MESSAGE] = + properties[PERPS_EVENT_PROPERTY.ERROR_MESSAGE] = error?.message || result?.error || 'Unknown error'; } @@ -478,8 +474,8 @@ export class TradingService { } { const direction = parseFloat(position.size) > 0 - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT; + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT; const filledSize = result.filledSize ? parseFloat(result.filledSize) : 0; const requestedSize = params.size @@ -487,14 +483,14 @@ export class TradingService { : Math.abs(parseFloat(position.size)); const isPartiallyFilled = filledSize > 0 && filledSize < requestedSize; - const orderType = params.orderType || PerpsEventValues.ORDER_TYPE.MARKET; + const orderType = params.orderType || PERPS_EVENT_VALUE.ORDER_TYPE.MARKET; const closePercentage = params.size ? (parseFloat(params.size) / Math.abs(parseFloat(position.size))) * 100 : 100; const closeType = closePercentage === 100 - ? PerpsEventValues.CLOSE_TYPE.FULL - : PerpsEventValues.CLOSE_TYPE.PARTIAL; + ? PERPS_EVENT_VALUE.CLOSE_TYPE.FULL + : PERPS_EVENT_VALUE.CLOSE_TYPE.PARTIAL; return { direction, @@ -525,67 +521,67 @@ export class TradingService { error?: string, ): Record { const baseProperties = { - [PerpsEventProperties.STATUS]: status, - [PerpsEventProperties.ASSET]: position.symbol, - [PerpsEventProperties.DIRECTION]: metrics.direction, - [PerpsEventProperties.ORDER_TYPE]: metrics.orderType, - [PerpsEventProperties.ORDER_SIZE]: metrics.requestedSize, - [PerpsEventProperties.OPEN_POSITION_SIZE]: Math.abs( + [PERPS_EVENT_PROPERTY.STATUS]: status, + [PERPS_EVENT_PROPERTY.ASSET]: position.symbol, + [PERPS_EVENT_PROPERTY.DIRECTION]: metrics.direction, + [PERPS_EVENT_PROPERTY.ORDER_TYPE]: metrics.orderType, + [PERPS_EVENT_PROPERTY.ORDER_SIZE]: metrics.requestedSize, + [PERPS_EVENT_PROPERTY.OPEN_POSITION_SIZE]: Math.abs( parseFloat(position.size), ), - [PerpsEventProperties.PERCENTAGE_CLOSED]: metrics.closePercentage, + [PERPS_EVENT_PROPERTY.PERCENTAGE_CLOSED]: metrics.closePercentage, ...(position.unrealizedPnl && { - [PerpsEventProperties.PNL_DOLLAR]: parseFloat(position.unrealizedPnl), + [PERPS_EVENT_PROPERTY.PNL_DOLLAR]: parseFloat(position.unrealizedPnl), }), ...(position.returnOnEquity && { - [PerpsEventProperties.PNL_PERCENT]: + [PERPS_EVENT_PROPERTY.PNL_PERCENT]: parseFloat(position.returnOnEquity) * 100, }), ...(params.trackingData?.totalFee != null && { - [PerpsEventProperties.FEE]: params.trackingData.totalFee, + [PERPS_EVENT_PROPERTY.FEE]: params.trackingData.totalFee, }), ...(params.trackingData?.metamaskFee != null && { - [PerpsEventProperties.METAMASK_FEE]: params.trackingData.metamaskFee, + [PERPS_EVENT_PROPERTY.METAMASK_FEE]: params.trackingData.metamaskFee, }), ...(params.trackingData?.metamaskFeeRate != null && { - [PerpsEventProperties.METAMASK_FEE_RATE]: + [PERPS_EVENT_PROPERTY.METAMASK_FEE_RATE]: params.trackingData.metamaskFeeRate, }), ...(params.trackingData?.feeDiscountPercentage != null && { - [PerpsEventProperties.DISCOUNT_PERCENTAGE]: + [PERPS_EVENT_PROPERTY.DISCOUNT_PERCENTAGE]: params.trackingData.feeDiscountPercentage, }), ...(params.trackingData?.estimatedPoints != null && { - [PerpsEventProperties.ESTIMATED_REWARDS]: + [PERPS_EVENT_PROPERTY.ESTIMATED_REWARDS]: params.trackingData.estimatedPoints, }), ...((params.trackingData?.marketPrice || result?.averagePrice) && { - [PerpsEventProperties.ASSET_PRICE]: result?.averagePrice + [PERPS_EVENT_PROPERTY.ASSET_PRICE]: result?.averagePrice ? parseFloat(result.averagePrice) : params.trackingData?.marketPrice, }), ...(params.orderType === 'limit' && params.price && { - [PerpsEventProperties.LIMIT_PRICE]: parseFloat(params.price), + [PERPS_EVENT_PROPERTY.LIMIT_PRICE]: parseFloat(params.price), }), ...(params.trackingData?.receivedAmount != null && { - [PerpsEventProperties.RECEIVED_AMOUNT]: + [PERPS_EVENT_PROPERTY.RECEIVED_AMOUNT]: params.trackingData.receivedAmount, }), }; // Add success-specific properties - if (status === PerpsEventValues.STATUS.EXECUTED) { + if (status === PERPS_EVENT_VALUE.STATUS.EXECUTED) { return { ...baseProperties, - [PerpsEventProperties.CLOSE_TYPE]: metrics.closeType, + [PERPS_EVENT_PROPERTY.CLOSE_TYPE]: metrics.closeType, }; } // Add error for failures return { ...baseProperties, - ...(error && { [PerpsEventProperties.ERROR_MESSAGE]: error }), + ...(error && { [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: error }), }; } @@ -611,14 +607,14 @@ export class TradingService { : { direction: parseFloat(position.size) > 0 - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, closePercentage: params.size ? (parseFloat(params.size) / Math.abs(parseFloat(position.size))) * 100 : 100, - closeType: PerpsEventValues.CLOSE_TYPE.FULL, - orderType: params.orderType || PerpsEventValues.ORDER_TYPE.MARKET, + closeType: PERPS_EVENT_VALUE.CLOSE_TYPE.FULL, + orderType: params.orderType || PERPS_EVENT_VALUE.ORDER_TYPE.MARKET, requestedSize: params.size ? parseFloat(params.size) : Math.abs(parseFloat(position.size)), @@ -633,17 +629,17 @@ export class TradingService { params, metrics, result, - PerpsEventValues.STATUS.PARTIALLY_FILLED, + PERPS_EVENT_VALUE.STATUS.PARTIALLY_FILLED, ); this.deps.metrics.trackPerpsEvent( PerpsAnalyticsEvent.PositionCloseTransaction, { ...partialProperties, - [PerpsEventProperties.AMOUNT_FILLED]: metrics.filledSize, - [PerpsEventProperties.REMAINING_AMOUNT]: + [PERPS_EVENT_PROPERTY.AMOUNT_FILLED]: metrics.filledSize, + [PERPS_EVENT_PROPERTY.REMAINING_AMOUNT]: metrics.requestedSize - metrics.filledSize, - [PerpsEventProperties.COMPLETION_DURATION]: duration, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: duration, }, ); } @@ -651,8 +647,8 @@ export class TradingService { // Determine status const status = result?.success === true - ? PerpsEventValues.STATUS.EXECUTED - : PerpsEventValues.STATUS.FAILED; + ? PERPS_EVENT_VALUE.STATUS.EXECUTED + : PERPS_EVENT_VALUE.STATUS.FAILED; const errorMessage = error?.message || result?.error; @@ -670,7 +666,7 @@ export class TradingService { PerpsAnalyticsEvent.PositionCloseTransaction, { ...eventProperties, - [PerpsEventProperties.COMPLETION_DURATION]: duration, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: duration, }, ); } @@ -724,17 +720,13 @@ export class TradingService { return undefined; } - const { controllers, messenger, rewardsIntegrationService } = - this.controllerDeps; + const { rewardsIntegrationService } = this.controllerDeps; const orderExecutionFeeDiscountStartTime = this.deps.performance.now(); - // Calculate fee discount using injected controllers + // Calculate fee discount using messenger pattern (service handles controller access internally) const discountBips = - await rewardsIntegrationService.calculateUserFeeDiscount({ - controllers, - messenger, - }); + await rewardsIntegrationService.calculateUserFeeDiscount(); const orderExecutionFeeDiscountDuration = this.deps.performance.now() - orderExecutionFeeDiscountStartTime; @@ -813,18 +805,18 @@ export class TradingService { // Track order edit executed const editExecutedProps: PerpsAnalyticsProperties = { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.EXECUTED, - [PerpsEventProperties.ASSET]: params.newOrder.symbol, - [PerpsEventProperties.DIRECTION]: params.newOrder.isBuy - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.ORDER_TYPE]: params.newOrder.orderType, - [PerpsEventProperties.LEVERAGE]: params.newOrder.leverage || 1, - [PerpsEventProperties.ORDER_SIZE]: params.newOrder.size, - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.EXECUTED, + [PERPS_EVENT_PROPERTY.ASSET]: params.newOrder.symbol, + [PERPS_EVENT_PROPERTY.DIRECTION]: params.newOrder.isBuy + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.ORDER_TYPE]: params.newOrder.orderType, + [PERPS_EVENT_PROPERTY.LEVERAGE]: params.newOrder.leverage || 1, + [PERPS_EVENT_PROPERTY.ORDER_SIZE]: params.newOrder.size, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, }; if (params.newOrder.price) { - editExecutedProps[PerpsEventProperties.LIMIT_PRICE] = parseFloat( + editExecutedProps[PERPS_EVENT_PROPERTY.LIMIT_PRICE] = parseFloat( params.newOrder.price, ); } @@ -839,16 +831,16 @@ export class TradingService { this.deps.metrics.trackPerpsEvent( PerpsAnalyticsEvent.TradeTransaction, { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.ASSET]: params.newOrder.symbol, - [PerpsEventProperties.DIRECTION]: params.newOrder.isBuy - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.ORDER_TYPE]: params.newOrder.orderType, - [PerpsEventProperties.LEVERAGE]: params.newOrder.leverage || 1, - [PerpsEventProperties.ORDER_SIZE]: params.newOrder.size, - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, - [PerpsEventProperties.ERROR_MESSAGE]: + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.ASSET]: params.newOrder.symbol, + [PERPS_EVENT_PROPERTY.DIRECTION]: params.newOrder.isBuy + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.ORDER_TYPE]: params.newOrder.orderType, + [PERPS_EVENT_PROPERTY.LEVERAGE]: params.newOrder.leverage || 1, + [PERPS_EVENT_PROPERTY.ORDER_SIZE]: params.newOrder.size, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: result.error || 'Unknown error', }, ); @@ -862,16 +854,16 @@ export class TradingService { // Track order edit exception this.deps.metrics.trackPerpsEvent(PerpsAnalyticsEvent.TradeTransaction, { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.ASSET]: params.newOrder.symbol, - [PerpsEventProperties.DIRECTION]: params.newOrder.isBuy - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.ORDER_TYPE]: params.newOrder.orderType, - [PerpsEventProperties.LEVERAGE]: params.newOrder.leverage || 1, - [PerpsEventProperties.ORDER_SIZE]: params.newOrder.size, - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, - [PerpsEventProperties.ERROR_MESSAGE]: + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.ASSET]: params.newOrder.symbol, + [PERPS_EVENT_PROPERTY.DIRECTION]: params.newOrder.isBuy + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.ORDER_TYPE]: params.newOrder.orderType, + [PERPS_EVENT_PROPERTY.LEVERAGE]: params.newOrder.leverage || 1, + [PERPS_EVENT_PROPERTY.ORDER_SIZE]: params.newOrder.size, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: error instanceof Error ? error.message : 'Unknown error', }); @@ -952,9 +944,9 @@ export class TradingService { this.deps.metrics.trackPerpsEvent( PerpsAnalyticsEvent.OrderCancelTransaction, { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.EXECUTED, - [PerpsEventProperties.ASSET]: params.symbol, - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.EXECUTED, + [PERPS_EVENT_PROPERTY.ASSET]: params.symbol, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, }, ); @@ -964,10 +956,10 @@ export class TradingService { this.deps.metrics.trackPerpsEvent( PerpsAnalyticsEvent.OrderCancelTransaction, { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.ASSET]: params.symbol, - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, - [PerpsEventProperties.ERROR_MESSAGE]: + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.ASSET]: params.symbol, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: result.error || 'Unknown error', }, ); @@ -983,10 +975,10 @@ export class TradingService { this.deps.metrics.trackPerpsEvent( PerpsAnalyticsEvent.OrderCancelTransaction, { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.ASSET]: params.symbol, - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, - [PerpsEventProperties.ERROR_MESSAGE]: + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.ASSET]: params.symbol, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: error instanceof Error ? error.message : 'Unknown error', }, ); @@ -1151,14 +1143,14 @@ export class TradingService { // Track batch cancel event (success or failure) const batchCancelProps: PerpsAnalyticsProperties = { - [PerpsEventProperties.STATUS]: + [PERPS_EVENT_PROPERTY.STATUS]: operationResult?.success && operationResult.successCount > 0 - ? PerpsEventValues.STATUS.EXECUTED - : PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, + ? PERPS_EVENT_VALUE.STATUS.EXECUTED + : PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, }; if (operationError) { - batchCancelProps[PerpsEventProperties.ERROR_MESSAGE] = + batchCancelProps[PERPS_EVENT_PROPERTY.ERROR_MESSAGE] = operationError.message; } this.deps.metrics.trackPerpsEvent( @@ -1438,14 +1430,14 @@ export class TradingService { // Track batch close event (success or failure) const batchCloseProps: PerpsAnalyticsProperties = { - [PerpsEventProperties.STATUS]: + [PERPS_EVENT_PROPERTY.STATUS]: operationResult?.success && operationResult.successCount > 0 - ? PerpsEventValues.STATUS.EXECUTED - : PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, + ? PERPS_EVENT_VALUE.STATUS.EXECUTED + : PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, }; if (operationError) { - batchCloseProps[PerpsEventProperties.ERROR_MESSAGE] = + batchCloseProps[PERPS_EVENT_PROPERTY.ERROR_MESSAGE] = operationError.message; } this.deps.metrics.trackPerpsEvent( @@ -1480,7 +1472,7 @@ export class TradingService { const direction = params.trackingData?.direction; const positionSize = params.trackingData?.positionSize; const source = - params.trackingData?.source || PerpsEventValues.SOURCE.TP_SL_VIEW; + params.trackingData?.source || PERPS_EVENT_VALUE.SOURCE.TP_SL_VIEW; const takeProfitPercentage = params.trackingData?.takeProfitPercentage; const stopLossPercentage = params.trackingData?.stopLossPercentage; const isEditingExistingPosition = @@ -1535,8 +1527,8 @@ export class TradingService { // Determine screen type based on whether editing existing position const screenType = isEditingExistingPosition - ? PerpsEventValues.SCREEN_TYPE.EDIT_TPSL - : PerpsEventValues.SCREEN_TYPE.CREATE_TPSL; + ? PERPS_EVENT_VALUE.SCREEN_TYPE.EDIT_TPSL + : PERPS_EVENT_VALUE.SCREEN_TYPE.CREATE_TPSL; // Determine if TP/SL are set const hasTakeProfit = !!params.takeProfitPrice; @@ -1544,42 +1536,42 @@ export class TradingService { // Build comprehensive event properties const eventProperties = { - [PerpsEventProperties.STATUS]: result?.success - ? PerpsEventValues.STATUS.EXECUTED - : PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.ASSET]: params.symbol, - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, - [PerpsEventProperties.SOURCE]: source, - [PerpsEventProperties.SCREEN_TYPE]: screenType, - [PerpsEventProperties.HAS_TAKE_PROFIT]: hasTakeProfit, - [PerpsEventProperties.HAS_STOP_LOSS]: hasStopLoss, + [PERPS_EVENT_PROPERTY.STATUS]: result?.success + ? PERPS_EVENT_VALUE.STATUS.EXECUTED + : PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.ASSET]: params.symbol, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, + [PERPS_EVENT_PROPERTY.SOURCE]: source, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: screenType, + [PERPS_EVENT_PROPERTY.HAS_TAKE_PROFIT]: hasTakeProfit, + [PERPS_EVENT_PROPERTY.HAS_STOP_LOSS]: hasStopLoss, ...(direction && { - [PerpsEventProperties.DIRECTION]: + [PERPS_EVENT_PROPERTY.DIRECTION]: direction === 'long' - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, }), ...(positionSize !== undefined && { - [PerpsEventProperties.POSITION_SIZE]: positionSize, + [PERPS_EVENT_PROPERTY.POSITION_SIZE]: positionSize, }), ...(params.takeProfitPrice && { - [PerpsEventProperties.TAKE_PROFIT_PRICE]: parseFloat( + [PERPS_EVENT_PROPERTY.TAKE_PROFIT_PRICE]: parseFloat( params.takeProfitPrice, ), }), ...(params.stopLossPrice && { - [PerpsEventProperties.STOP_LOSS_PRICE]: parseFloat( + [PERPS_EVENT_PROPERTY.STOP_LOSS_PRICE]: parseFloat( params.stopLossPrice, ), }), ...(takeProfitPercentage !== undefined && { - [PerpsEventProperties.TAKE_PROFIT_PERCENTAGE]: takeProfitPercentage, + [PERPS_EVENT_PROPERTY.TAKE_PROFIT_PERCENTAGE]: takeProfitPercentage, }), ...(stopLossPercentage !== undefined && { - [PerpsEventProperties.STOP_LOSS_PERCENTAGE]: stopLossPercentage, + [PERPS_EVENT_PROPERTY.STOP_LOSS_PERCENTAGE]: stopLossPercentage, }), ...(errorMessage && { - [PerpsEventProperties.ERROR_MESSAGE]: errorMessage, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: errorMessage, }), }; @@ -1642,12 +1634,12 @@ export class TradingService { // Track success analytics this.deps.metrics.trackPerpsEvent(PerpsAnalyticsEvent.RiskManagement, { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.EXECUTED, - [PerpsEventProperties.ASSET]: symbol, - [PerpsEventProperties.ACTION]: + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.EXECUTED, + [PERPS_EVENT_PROPERTY.ASSET]: symbol, + [PERPS_EVENT_PROPERTY.ACTION]: parseFloat(amount) > 0 ? 'add_margin' : 'remove_margin', - [PerpsEventProperties.MARGIN_USED]: Math.abs(parseFloat(amount)), - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, + [PERPS_EVENT_PROPERTY.MARGIN_USED]: Math.abs(parseFloat(amount)), + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, }); } @@ -1670,13 +1662,13 @@ export class TradingService { // Track failure analytics this.deps.metrics.trackPerpsEvent(PerpsAnalyticsEvent.RiskManagement, { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.ASSET]: symbol, - [PerpsEventProperties.ACTION]: + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.ASSET]: symbol, + [PERPS_EVENT_PROPERTY.ACTION]: parseFloat(amount) > 0 ? 'add_margin' : 'remove_margin', - [PerpsEventProperties.MARGIN_USED]: Math.abs(parseFloat(amount)), - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, - [PerpsEventProperties.ERROR_MESSAGE]: errorMessage, + [PERPS_EVENT_PROPERTY.MARGIN_USED]: Math.abs(parseFloat(amount)), + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: errorMessage, }); this.deps.tracer.endTrace({ @@ -1767,16 +1759,16 @@ export class TradingService { this.deps.metrics.trackPerpsEvent( PerpsAnalyticsEvent.TradeTransaction, { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.EXECUTED, - [PerpsEventProperties.ASSET]: position.symbol, - [PerpsEventProperties.DIRECTION]: oppositeDirection - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.ORDER_TYPE]: 'market', - [PerpsEventProperties.LEVERAGE]: position.leverage?.value || 1, - [PerpsEventProperties.ORDER_SIZE]: positionSize, - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, - [PerpsEventProperties.ACTION]: 'flip_position', + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.EXECUTED, + [PERPS_EVENT_PROPERTY.ASSET]: position.symbol, + [PERPS_EVENT_PROPERTY.DIRECTION]: oppositeDirection + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.ORDER_TYPE]: 'market', + [PERPS_EVENT_PROPERTY.LEVERAGE]: position.leverage?.value || 1, + [PERPS_EVENT_PROPERTY.ORDER_SIZE]: positionSize, + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, + [PERPS_EVENT_PROPERTY.ACTION]: 'flip_position', }, ); } @@ -1800,11 +1792,11 @@ export class TradingService { // Track failure analytics this.deps.metrics.trackPerpsEvent(PerpsAnalyticsEvent.TradeTransaction, { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.ASSET]: position.symbol, - [PerpsEventProperties.ACTION]: 'flip_position', - [PerpsEventProperties.COMPLETION_DURATION]: completionDuration, - [PerpsEventProperties.ERROR_MESSAGE]: errorMessage, + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.ASSET]: position.symbol, + [PERPS_EVENT_PROPERTY.ACTION]: 'flip_position', + [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: errorMessage, }); this.deps.tracer.endTrace({ diff --git a/app/components/UI/Perps/controllers/types/index.ts b/app/components/UI/Perps/controllers/types/index.ts index 24e1285d299..72b934c6878 100644 --- a/app/components/UI/Perps/controllers/types/index.ts +++ b/app/components/UI/Perps/controllers/types/index.ts @@ -1233,7 +1233,7 @@ export type PerpsTraceValue = string | number | boolean; /** * Properties allowed in analytics events. More constrained than unknown. - * Named PerpsAnalyticsProperties to avoid conflict with PerpsEventProperties + * Named PerpsAnalyticsProperties to avoid conflict with PERPS_EVENT_PROPERTY * constant object from eventNames.ts (which contains property key names). */ export type PerpsAnalyticsProperties = Record< @@ -1339,88 +1339,12 @@ export interface PerpsTracer { setMeasurement(name: string, value: number, unit: string): void; } -/** - * Injectable keyring controller interface for signing operations. - * Allows services to sign typed messages without directly accessing Engine. - */ -export interface PerpsKeyringController { - signTypedMessage( - msgParams: { from: string; data: unknown }, - version: string, - ): Promise; -} - -/** - * Injectable account utilities interface. - * Provides access to selected account without coupling to Engine singleton. - */ -export interface PerpsAccountUtils { - getSelectedEvmAccount(): { address: string } | undefined; - formatAccountToCaipId(address: string, chainId: string): string | null; -} - // ============================================================================ -// Controller Access Interfaces -// These granular interfaces define the specific operations needed from each -// controller, enabling cleaner dependency injection and easier testing. +// Rewards Interface // ============================================================================ /** - * Network controller operations required by Perps. - * Provides chain ID lookups and network client identification. - */ -/** - * Network controller operations required by Perps. - * Provides chain ID lookups and network client identification. - */ -export interface PerpsNetworkOperations { - /** - * Get the chain ID for a given network client. - */ - getChainIdForNetwork(networkClientId: string): Hex; - - /** - * Find the network client ID for a given chain. - */ - findNetworkClientIdForChain(chainId: Hex): string | undefined; - - /** - * Get the currently selected network client ID. - */ - getSelectedNetworkClientId(): string; -} - -/** - * Transaction controller operations required by Perps. - * Provides transaction submission capabilities. - */ -export interface PerpsTransactionOperations { - /** - * Submit a transaction to the blockchain. - * Returns the result promise and transaction metadata. - */ - submit( - txParams: { - from: string; - to?: string; - value?: string; - data?: string; - }, - options: { - networkClientId: string; - origin?: string; - type?: string; // Will be bridged to TransactionType in adapter - skipInitialGasEstimate?: boolean; - gasFeeToken?: Hex; - }, - ): Promise<{ - result: Promise; // Resolves to txHash - transactionMeta: { id: string; hash?: string }; - }>; -} - -/** - * Rewards controller operations required by Perps (optional). + * Rewards controller operations required by Perps. * Provides fee discount capabilities for MetaMask rewards program. */ export interface PerpsRewardsOperations { @@ -1434,52 +1358,14 @@ export interface PerpsRewardsOperations { } /** - * Authentication controller operations required by Perps (optional). - * Provides bearer token access for authenticated API calls. - */ -export interface PerpsAuthenticationOperations { - /** - * Get a bearer token for authenticated API requests. - */ - getBearerToken(): Promise; -} - -/** - * Consolidated controller access interface. - * Groups ALL controller dependencies in one place for clarity. - * - * Benefits: - * 1. Clear separation: observability utilities vs controller access - * 2. Consistent pattern: all controllers accessed via deps.controllers.* - * 3. Mockable: test can mock entire controllers object - * 4. Future-proof: add new controller access without bloating top-level - */ -export interface PerpsControllerAccess { - /** Account utilities - wraps AccountsController access */ - accounts: PerpsAccountUtils; - /** Keyring operations - wraps KeyringController for signing */ - keyring: PerpsKeyringController; - /** Network operations - wraps NetworkController for chain lookups */ - network: PerpsNetworkOperations; - /** Transaction operations - wraps TransactionController for TX submission */ - transaction: PerpsTransactionOperations; - /** Rewards operations - wraps RewardsController for fee discounts */ - rewards: PerpsRewardsOperations; - /** Authentication operations - wraps AuthenticationController for bearer tokens */ - authentication: PerpsAuthenticationOperations; -} - -/** - * Combined platform dependencies for PerpsController and services. - * All platform-specific dependencies are bundled here for easy injection. + * Platform dependencies for PerpsController and services. * * Architecture: - * - Observability: logger, debugLogger, metrics, performance, tracer (stateless utilities) - * - Platform: streamManager (mobile/extension specific capabilities) - * - Controllers: consolidated access to all external controllers + * - Observability: logger, debugLogger, metrics, performance, tracer + * - Platform: streamManager (mobile/extension specific) + * - Rewards: fee discount operations * - * This interface enables dependency injection for platform-specific services, - * allowing PerpsController to be moved to core without mobile-specific imports. + * Controller access uses messenger pattern (messenger.call()). */ export interface PerpsPlatformDependencies { // === Observability (stateless utilities) === @@ -1492,6 +1378,6 @@ export interface PerpsPlatformDependencies { // === Platform Services (mobile/extension specific) === streamManager: PerpsStreamManager; - // === Controller Access (ALL controllers consolidated) === - controllers: PerpsControllerAccess; + // === Rewards (no standard messenger action in core) === + rewards: PerpsRewardsOperations; } diff --git a/app/components/UI/Perps/hooks/usePerpsEventTracking.test.ts b/app/components/UI/Perps/hooks/usePerpsEventTracking.test.ts index 17ab7725e55..0f9a8ca12ff 100644 --- a/app/components/UI/Perps/hooks/usePerpsEventTracking.test.ts +++ b/app/components/UI/Perps/hooks/usePerpsEventTracking.test.ts @@ -1,6 +1,6 @@ import { renderHook, act } from '@testing-library/react-native'; import { usePerpsEventTracking } from './usePerpsEventTracking'; -import { PerpsEventProperties } from '../constants/eventNames'; +import { PERPS_EVENT_PROPERTY } from '../constants/eventNames'; // Mock useMetrics hook const mockTrackEvent = jest.fn(); @@ -45,7 +45,7 @@ describe('usePerpsEventTracking', () => { expect(mockCreateEventBuilder).toHaveBeenCalledWith(mockEvent); const eventBuilder = mockCreateEventBuilder.mock.results[0].value; expect(eventBuilder.addProperties).toHaveBeenCalledWith({ - [PerpsEventProperties.TIMESTAMP]: 1234567890, + [PERPS_EVENT_PROPERTY.TIMESTAMP]: 1234567890, }); expect(eventBuilder.build).toHaveBeenCalled(); expect(mockTrackEvent).toHaveBeenCalledWith({ type: 'mock-event' }); @@ -67,7 +67,7 @@ describe('usePerpsEventTracking', () => { const eventBuilder = mockCreateEventBuilder.mock.results[0].value; expect(eventBuilder.addProperties).toHaveBeenCalledWith({ - [PerpsEventProperties.TIMESTAMP]: 1234567890, + [PERPS_EVENT_PROPERTY.TIMESTAMP]: 1234567890, ...customProps, }); }); diff --git a/app/components/UI/Perps/hooks/usePerpsEventTracking.ts b/app/components/UI/Perps/hooks/usePerpsEventTracking.ts index 6ac95511e88..b8c617b8eef 100644 --- a/app/components/UI/Perps/hooks/usePerpsEventTracking.ts +++ b/app/components/UI/Perps/hooks/usePerpsEventTracking.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useMemo } from 'react'; import { useMetrics, MetaMetricsEvents } from '../../../hooks/useMetrics'; -import { PerpsEventProperties } from '../constants/eventNames'; +import { PERPS_EVENT_PROPERTY } from '../constants/eventNames'; // Static helper function - moved outside component to avoid recreation const allTrue = (conditionArray: boolean[]): boolean => @@ -58,7 +58,7 @@ export const usePerpsEventTracking = (options?: EventTrackingOptions) => { properties: Record = {}, ) => { const props = { - [PerpsEventProperties.TIMESTAMP]: Date.now(), + [PERPS_EVENT_PROPERTY.TIMESTAMP]: Date.now(), ...properties, }; trackEvent(createEventBuilder(eventName).addProperties(props).build()); diff --git a/app/components/UI/Perps/hooks/usePerpsHomeActions.test.ts b/app/components/UI/Perps/hooks/usePerpsHomeActions.test.ts index e1b0b332b7f..99a1c835aa6 100644 --- a/app/components/UI/Perps/hooks/usePerpsHomeActions.test.ts +++ b/app/components/UI/Perps/hooks/usePerpsHomeActions.test.ts @@ -8,8 +8,8 @@ import { useConfirmNavigation } from '../../../Views/confirmations/hooks/useConf import Routes from '../../../../constants/navigation/Routes'; import { MetaMetricsEvents } from '../../../../core/Analytics/MetaMetrics.events'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../constants/eventNames'; // Mock dependencies @@ -210,9 +210,10 @@ describe('usePerpsHomeActions', () => { expect(mockTrack).toHaveBeenCalledWith( MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: PerpsEventValues.SOURCE.DEPOSIT_BUTTON, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.DEPOSIT_BUTTON, }, ); }); @@ -296,8 +297,8 @@ describe('usePerpsHomeActions', () => { expect(mockTrack).not.toHaveBeenCalledWith( MetaMetricsEvents.PERPS_SCREEN_VIEWED, expect.objectContaining({ - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, }), ); }); @@ -315,9 +316,9 @@ describe('usePerpsHomeActions', () => { expect(mockTrack).toHaveBeenCalledWith( MetaMetricsEvents.PERPS_UI_INTERACTION, expect.objectContaining({ - [PerpsEventProperties.BUTTON_CLICKED]: - PerpsEventValues.BUTTON_CLICKED.WITHDRAW, - [PerpsEventProperties.IS_GEO_BLOCKED]: true, + [PERPS_EVENT_PROPERTY.BUTTON_CLICKED]: + PERPS_EVENT_VALUE.BUTTON_CLICKED.WITHDRAW, + [PERPS_EVENT_PROPERTY.IS_GEO_BLOCKED]: true, }), ); }); @@ -337,9 +338,9 @@ describe('usePerpsHomeActions', () => { expect(mockTrack).toHaveBeenCalledWith( MetaMetricsEvents.PERPS_UI_INTERACTION, expect.objectContaining({ - [PerpsEventProperties.BUTTON_CLICKED]: - PerpsEventValues.BUTTON_CLICKED.WITHDRAW, - [PerpsEventProperties.IS_GEO_BLOCKED]: false, + [PERPS_EVENT_PROPERTY.BUTTON_CLICKED]: + PERPS_EVENT_VALUE.BUTTON_CLICKED.WITHDRAW, + [PERPS_EVENT_PROPERTY.IS_GEO_BLOCKED]: false, }), ); }); diff --git a/app/components/UI/Perps/hooks/usePerpsHomeActions.ts b/app/components/UI/Perps/hooks/usePerpsHomeActions.ts index 21e50699ba9..86adb8560a0 100644 --- a/app/components/UI/Perps/hooks/usePerpsHomeActions.ts +++ b/app/components/UI/Perps/hooks/usePerpsHomeActions.ts @@ -12,8 +12,8 @@ import type { PerpsNavigationParamList } from '../controllers/types'; import { ensureError } from '../../../../util/errorUtils'; import { PERPS_CONSTANTS } from '../constants/perpsConfig'; import { - PerpsEventValues, - PerpsEventProperties, + PERPS_EVENT_VALUE, + PERPS_EVENT_PROPERTY, } from '../constants/eventNames'; import { usePerpsEventTracking } from './usePerpsEventTracking'; import { MetaMetricsEvents } from '../../../../core/Analytics/MetaMetrics.events'; @@ -82,21 +82,21 @@ export const usePerpsHomeActions = ( const handleAddFunds = useCallback(async () => { track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.BUTTON_CLICKED, - [PerpsEventProperties.BUTTON_CLICKED]: - PerpsEventValues.BUTTON_CLICKED.DEPOSIT, - [PerpsEventProperties.BUTTON_LOCATION]: - buttonLocation || PerpsEventValues.BUTTON_LOCATION.PERPS_HOME, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.BUTTON_CLICKED, + [PERPS_EVENT_PROPERTY.BUTTON_CLICKED]: + PERPS_EVENT_VALUE.BUTTON_CLICKED.DEPOSIT, + [PERPS_EVENT_PROPERTY.BUTTON_LOCATION]: + buttonLocation || PERPS_EVENT_VALUE.BUTTON_LOCATION.PERPS_HOME, }); if (!isEligible) { DevLogger.log('[usePerpsHomeActions] User not eligible for deposit'); // Track geo-block screen viewed track(MetaMetricsEvents.PERPS_SCREEN_VIEWED, { - [PerpsEventProperties.SCREEN_TYPE]: - PerpsEventValues.SCREEN_TYPE.GEO_BLOCK_NOTIF, - [PerpsEventProperties.SOURCE]: PerpsEventValues.SOURCE.DEPOSIT_BUTTON, + [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: + PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF, + [PERPS_EVENT_PROPERTY.SOURCE]: PERPS_EVENT_VALUE.SOURCE.DEPOSIT_BUTTON, }); setIsEligibilityModalVisible(true); return; @@ -151,13 +151,13 @@ export const usePerpsHomeActions = ( const handleWithdraw = useCallback(async () => { // Track withdrawal button click with geo-block status for monitoring (TAT-2337) track(MetaMetricsEvents.PERPS_UI_INTERACTION, { - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.BUTTON_CLICKED, - [PerpsEventProperties.BUTTON_CLICKED]: - PerpsEventValues.BUTTON_CLICKED.WITHDRAW, - [PerpsEventProperties.BUTTON_LOCATION]: - buttonLocation || PerpsEventValues.BUTTON_LOCATION.PERPS_HOME, - [PerpsEventProperties.IS_GEO_BLOCKED]: !isEligible, + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.BUTTON_CLICKED, + [PERPS_EVENT_PROPERTY.BUTTON_CLICKED]: + PERPS_EVENT_VALUE.BUTTON_CLICKED.WITHDRAW, + [PERPS_EVENT_PROPERTY.BUTTON_LOCATION]: + buttonLocation || PERPS_EVENT_VALUE.BUTTON_LOCATION.PERPS_HOME, + [PERPS_EVENT_PROPERTY.IS_GEO_BLOCKED]: !isEligible, }); // Note: Withdrawals are intentionally NOT geo-blocked (TAT-2337) diff --git a/app/components/UI/Perps/hooks/usePerpsHomeSectionTracking.ts b/app/components/UI/Perps/hooks/usePerpsHomeSectionTracking.ts index e0b31531580..1020375a729 100644 --- a/app/components/UI/Perps/hooks/usePerpsHomeSectionTracking.ts +++ b/app/components/UI/Perps/hooks/usePerpsHomeSectionTracking.ts @@ -2,13 +2,13 @@ import { useRef, useCallback } from 'react'; import { LayoutChangeEvent, NativeScrollEvent } from 'react-native'; import { MetaMetricsEvents, useMetrics } from '../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../constants/eventNames'; /** * Section identifiers for home screen tracking - * Maps to PerpsEventValues.SOURCE values for consistency + * Maps to PERPS_EVENT_VALUE.SOURCE values for consistency */ export type HomeSectionId = 'explore_crypto' | 'explore_stocks' | 'activity'; @@ -54,11 +54,11 @@ export function usePerpsHomeSectionTracking() { const getSectionSource = (sectionId: HomeSectionId): string => { switch (sectionId) { case 'explore_crypto': - return PerpsEventValues.SOURCE.PERPS_HOME_EXPLORE_CRYPTO; + return PERPS_EVENT_VALUE.SOURCE.PERPS_HOME_EXPLORE_CRYPTO; case 'explore_stocks': - return PerpsEventValues.SOURCE.PERPS_HOME_EXPLORE_STOCKS; + return PERPS_EVENT_VALUE.SOURCE.PERPS_HOME_EXPLORE_STOCKS; case 'activity': - return PerpsEventValues.SOURCE.PERPS_HOME_ACTIVITY; + return PERPS_EVENT_VALUE.SOURCE.PERPS_HOME_ACTIVITY; default: return sectionId; } @@ -78,12 +78,12 @@ export function usePerpsHomeSectionTracking() { trackEvent( createEventBuilder(MetaMetricsEvents.PERPS_UI_INTERACTION) .addProperties({ - [PerpsEventProperties.INTERACTION_TYPE]: - PerpsEventValues.INTERACTION_TYPE.SLIDE, - [PerpsEventProperties.SECTION_VIEWED]: + [PERPS_EVENT_PROPERTY.INTERACTION_TYPE]: + PERPS_EVENT_VALUE.INTERACTION_TYPE.SLIDE, + [PERPS_EVENT_PROPERTY.SECTION_VIEWED]: getSectionSource(sectionId), - [PerpsEventProperties.LOCATION]: - PerpsEventValues.BUTTON_LOCATION.PERPS_HOME, + [PERPS_EVENT_PROPERTY.LOCATION]: + PERPS_EVENT_VALUE.BUTTON_LOCATION.PERPS_HOME, }) .build(), ); diff --git a/app/components/UI/Perps/hooks/usePerpsNavigation.ts b/app/components/UI/Perps/hooks/usePerpsNavigation.ts index 22d14c0772f..8ead36677f3 100644 --- a/app/components/UI/Perps/hooks/usePerpsNavigation.ts +++ b/app/components/UI/Perps/hooks/usePerpsNavigation.ts @@ -7,8 +7,8 @@ import { usePerpsTrading } from './usePerpsTrading'; import usePerpsToasts from './usePerpsToasts'; import { usePerpsEventTracking } from './usePerpsEventTracking'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../constants/eventNames'; import { MetaMetricsEvents } from '../../../hooks/useMetrics'; import Logger from '../../../../util/Logger'; @@ -164,10 +164,11 @@ export const usePerpsNavigation = (): PerpsNavigationHandlers => { }); track(MetaMetricsEvents.PERPS_ERROR, { - [PerpsEventProperties.ERROR_TYPE]: - PerpsEventValues.ERROR_TYPE.BACKEND, - [PerpsEventProperties.ERROR_MESSAGE]: err.message, - [PerpsEventProperties.SOURCE]: PerpsEventValues.SOURCE.TRADE_ACTION, + [PERPS_EVENT_PROPERTY.ERROR_TYPE]: + PERPS_EVENT_VALUE.ERROR_TYPE.BACKEND, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: err.message, + [PERPS_EVENT_PROPERTY.SOURCE]: + PERPS_EVENT_VALUE.SOURCE.TRADE_ACTION, }); showToast( diff --git a/app/components/UI/Perps/hooks/usePerpsOrderExecution.ts b/app/components/UI/Perps/hooks/usePerpsOrderExecution.ts index 8067e32d9e0..0d46c63ec9a 100644 --- a/app/components/UI/Perps/hooks/usePerpsOrderExecution.ts +++ b/app/components/UI/Perps/hooks/usePerpsOrderExecution.ts @@ -5,8 +5,8 @@ import DevLogger from '../../../../core/SDKConnect/utils/DevLogger'; import { TraceName, TraceOperation } from '../../../../util/trace'; import { MetaMetricsEvents } from '../../../hooks/useMetrics'; import { - PerpsEventProperties, - PerpsEventValues, + PERPS_EVENT_PROPERTY, + PERPS_EVENT_VALUE, } from '../constants/eventNames'; import type { OrderParams, OrderResult, Position } from '../controllers/types'; import { usePerpsEventTracking } from './usePerpsEventTracking'; @@ -80,17 +80,17 @@ export function usePerpsOrderExecution( if (isPartiallyFilled) { // Track partially filled event track(MetaMetricsEvents.PERPS_TRADE_TRANSACTION, { - [PerpsEventProperties.STATUS]: - PerpsEventValues.STATUS.PARTIALLY_FILLED, - [PerpsEventProperties.ASSET]: orderParams.symbol, - [PerpsEventProperties.DIRECTION]: orderParams.isBuy - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.LEVERAGE]: orderParams.leverage || 1, - [PerpsEventProperties.ORDER_SIZE]: orderSize, - [PerpsEventProperties.ORDER_TYPE]: orderParams.orderType, - [PerpsEventProperties.AMOUNT_FILLED]: filledSize, - [PerpsEventProperties.REMAINING_AMOUNT]: orderSize - filledSize, + [PERPS_EVENT_PROPERTY.STATUS]: + PERPS_EVENT_VALUE.STATUS.PARTIALLY_FILLED, + [PERPS_EVENT_PROPERTY.ASSET]: orderParams.symbol, + [PERPS_EVENT_PROPERTY.DIRECTION]: orderParams.isBuy + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.LEVERAGE]: orderParams.leverage || 1, + [PERPS_EVENT_PROPERTY.ORDER_SIZE]: orderSize, + [PERPS_EVENT_PROPERTY.ORDER_TYPE]: orderParams.orderType, + [PERPS_EVENT_PROPERTY.AMOUNT_FILLED]: filledSize, + [PERPS_EVENT_PROPERTY.REMAINING_AMOUNT]: orderSize - filledSize, }); } @@ -133,14 +133,14 @@ export function usePerpsOrderExecution( // Track order failure with specific event track(MetaMetricsEvents.PERPS_TRADE_TRANSACTION, { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.ASSET]: orderParams.symbol, - [PerpsEventProperties.DIRECTION]: orderParams.isBuy - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.ORDER_TYPE]: orderParams.orderType, - [PerpsEventProperties.ORDER_SIZE]: orderParams.size, - [PerpsEventProperties.ERROR_MESSAGE]: errorMessage, + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.ASSET]: orderParams.symbol, + [PERPS_EVENT_PROPERTY.DIRECTION]: orderParams.isBuy + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.ORDER_TYPE]: orderParams.orderType, + [PERPS_EVENT_PROPERTY.ORDER_SIZE]: orderParams.size, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: errorMessage, }); onError?.(errorMessage); @@ -176,14 +176,14 @@ export function usePerpsOrderExecution( // Track exception with specific event track(MetaMetricsEvents.PERPS_TRADE_TRANSACTION, { - [PerpsEventProperties.STATUS]: PerpsEventValues.STATUS.FAILED, - [PerpsEventProperties.ASSET]: orderParams.symbol, - [PerpsEventProperties.DIRECTION]: orderParams.isBuy - ? PerpsEventValues.DIRECTION.LONG - : PerpsEventValues.DIRECTION.SHORT, - [PerpsEventProperties.ORDER_TYPE]: orderParams.orderType, - [PerpsEventProperties.ORDER_SIZE]: orderParams.size, - [PerpsEventProperties.ERROR_MESSAGE]: errorMessage, + [PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.FAILED, + [PERPS_EVENT_PROPERTY.ASSET]: orderParams.symbol, + [PERPS_EVENT_PROPERTY.DIRECTION]: orderParams.isBuy + ? PERPS_EVENT_VALUE.DIRECTION.LONG + : PERPS_EVENT_VALUE.DIRECTION.SHORT, + [PERPS_EVENT_PROPERTY.ORDER_TYPE]: orderParams.orderType, + [PERPS_EVENT_PROPERTY.ORDER_SIZE]: orderParams.size, + [PERPS_EVENT_PROPERTY.ERROR_MESSAGE]: errorMessage, }); onError?.(errorMessage); diff --git a/app/components/UI/Perps/hooks/usePerpsToasts.tsx b/app/components/UI/Perps/hooks/usePerpsToasts.tsx index 75b32adbdca..3025e73fdb6 100644 --- a/app/components/UI/Perps/hooks/usePerpsToasts.tsx +++ b/app/components/UI/Perps/hooks/usePerpsToasts.tsx @@ -20,7 +20,7 @@ import { import Routes from '../../../../constants/navigation/Routes'; import { capitalize } from '../../../../util/general'; import { useAppThemeFromContext } from '../../../../util/theme'; -import { PerpsEventValues } from '../constants/eventNames'; +import { PERPS_EVENT_VALUE } from '../constants/eventNames'; import { OrderDirection } from '../types/perps-types'; import { formatPerpsFiat } from '../utils/formatUtils'; import { handlePerpsError } from '../utils/translatePerpsError'; @@ -298,7 +298,7 @@ const usePerpsToasts = (): { navigation.navigate(Routes.PERPS.PNL_HERO_CARD, { position, marketPrice, - source: PerpsEventValues.SOURCE.CLOSE_TOAST, + source: PERPS_EVENT_VALUE.SOURCE.CLOSE_TOAST, }); }, }), diff --git a/app/components/UI/Perps/hooks/useWebSocketHealthToast.test.ts b/app/components/UI/Perps/hooks/useWebSocketHealthToast.test.ts index 09b83d33b31..6e48169f7b1 100644 --- a/app/components/UI/Perps/hooks/useWebSocketHealthToast.test.ts +++ b/app/components/UI/Perps/hooks/useWebSocketHealthToast.test.ts @@ -42,6 +42,8 @@ jest.mock('../../../../core/Engine', () => ({ // Auto-retry delay constant (must match the one in the hook) const AUTO_RETRY_DELAY_MS = 10000; +// Offline banner delay (must match the one in the hook) +const OFFLINE_BANNER_DELAY_MS = 1000; describe('useWebSocketHealthToast', () => { let mockUnsubscribe: jest.Mock; @@ -85,7 +87,7 @@ describe('useWebSocketHealthToast', () => { expect(mockShow).not.toHaveBeenCalled(); }); - it('should show toast when initial state is DISCONNECTED', () => { + it('should show toast when initial state is DISCONNECTED (after delay)', () => { renderHook(() => useWebSocketHealthToast()); // Simulate initial callback with DISCONNECTED state @@ -93,13 +95,19 @@ describe('useWebSocketHealthToast', () => { connectionStateCallback(WebSocketConnectionState.Disconnected, 1); }); + expect(mockShow).not.toHaveBeenCalled(); + + act(() => { + jest.advanceTimersByTime(OFFLINE_BANNER_DELAY_MS); + }); + expect(mockShow).toHaveBeenCalledWith( WebSocketConnectionState.Disconnected, 1, ); }); - it('should show toast when initial state is CONNECTING', () => { + it('should show toast when initial state is CONNECTING (after delay)', () => { renderHook(() => useWebSocketHealthToast()); // Simulate initial callback with CONNECTING state @@ -107,6 +115,12 @@ describe('useWebSocketHealthToast', () => { connectionStateCallback(WebSocketConnectionState.Connecting, 2); }); + expect(mockShow).not.toHaveBeenCalled(); + + act(() => { + jest.advanceTimersByTime(OFFLINE_BANNER_DELAY_MS); + }); + expect(mockShow).toHaveBeenCalledWith( WebSocketConnectionState.Connecting, 2, @@ -115,7 +129,7 @@ describe('useWebSocketHealthToast', () => { }); describe('State transitions', () => { - it('should show disconnected toast on CONNECTED → DISCONNECTED transition', () => { + it('should show disconnected toast on CONNECTED → DISCONNECTED transition (after delay)', () => { renderHook(() => useWebSocketHealthToast()); // First callback: CONNECTED (initial state) @@ -124,31 +138,46 @@ describe('useWebSocketHealthToast', () => { }); mockShow.mockClear(); - // Second callback: DISCONNECTED (transition) + // Second callback: DISCONNECTED (transition - schedules show after delay) act(() => { connectionStateCallback(WebSocketConnectionState.Disconnected, 1); }); + expect(mockShow).not.toHaveBeenCalled(); + + act(() => { + jest.advanceTimersByTime(OFFLINE_BANNER_DELAY_MS); + }); + expect(mockShow).toHaveBeenCalledWith( WebSocketConnectionState.Disconnected, 1, ); }); - it('should show connecting toast on DISCONNECTED → CONNECTING transition', () => { + it('should show connecting toast on DISCONNECTED → CONNECTING transition (after delay)', () => { renderHook(() => useWebSocketHealthToast()); // First callback: DISCONNECTED (initial - marks as experienced disconnection) act(() => { connectionStateCallback(WebSocketConnectionState.Disconnected, 1); }); + act(() => { + jest.advanceTimersByTime(OFFLINE_BANNER_DELAY_MS); + }); mockShow.mockClear(); - // Second callback: CONNECTING (transition) + // Second callback: CONNECTING (transition - schedules show after delay) act(() => { connectionStateCallback(WebSocketConnectionState.Connecting, 2); }); + expect(mockShow).not.toHaveBeenCalled(); + + act(() => { + jest.advanceTimersByTime(OFFLINE_BANNER_DELAY_MS); + }); + expect(mockShow).toHaveBeenCalledWith( WebSocketConnectionState.Connecting, 2, @@ -163,19 +192,19 @@ describe('useWebSocketHealthToast', () => { connectionStateCallback(WebSocketConnectionState.Connected, 0); }); - // Disconnected + // Disconnected (schedules show after delay; we reconnect before delay) act(() => { connectionStateCallback(WebSocketConnectionState.Disconnected, 1); }); mockShow.mockClear(); - // Reconnecting + // Reconnecting (schedules show after delay; we reconnect before delay) act(() => { connectionStateCallback(WebSocketConnectionState.Connecting, 2); }); mockShow.mockClear(); - // Reconnected successfully + // Reconnected successfully (clears delay, shows Connected immediately) act(() => { connectionStateCallback(WebSocketConnectionState.Connected, 0); }); @@ -244,13 +273,22 @@ describe('useWebSocketHealthToast', () => { act(() => { connectionStateCallback(WebSocketConnectionState.Disconnected, 1); }); + act(() => { + jest.advanceTimersByTime(OFFLINE_BANNER_DELAY_MS); + }); mockShow.mockClear(); - // Reconnecting with attempt 3 + // Reconnecting with attempt 3 (schedules show after delay) act(() => { connectionStateCallback(WebSocketConnectionState.Connecting, 3); }); + expect(mockShow).not.toHaveBeenCalled(); + + act(() => { + jest.advanceTimersByTime(OFFLINE_BANNER_DELAY_MS); + }); + expect(mockShow).toHaveBeenCalledWith( WebSocketConnectionState.Connecting, 3, @@ -299,6 +337,46 @@ describe('useWebSocketHealthToast', () => { }); }); + describe('Offline banner delay (flicker prevention)', () => { + it('should NOT show offline banner if reconnected within delay', () => { + renderHook(() => useWebSocketHealthToast()); + + // Initial: CONNECTED + act(() => { + connectionStateCallback(WebSocketConnectionState.Connected, 0); + }); + mockShow.mockClear(); + + // Disconnected (schedules show after 1s) + act(() => { + connectionStateCallback(WebSocketConnectionState.Disconnected, 1); + }); + + expect(mockShow).not.toHaveBeenCalled(); + + // Reconnect before delay expires + act(() => { + connectionStateCallback(WebSocketConnectionState.Connecting, 1); + }); + act(() => { + connectionStateCallback(WebSocketConnectionState.Connected, 0); + }); + + // Advance past the banner delay - show was never scheduled for Disconnected/Connecting + // because we cleared the timer when we got Connected + act(() => { + jest.advanceTimersByTime(OFFLINE_BANNER_DELAY_MS); + }); + + // Should only have shown Connected (reconnection success), not Disconnected + expect(mockShow).toHaveBeenCalledTimes(1); + expect(mockShow).toHaveBeenCalledWith( + WebSocketConnectionState.Connected, + 0, + ); + }); + }); + describe('DISCONNECTING state', () => { it('should not show toast for DISCONNECTING state', () => { renderHook(() => useWebSocketHealthToast()); diff --git a/app/components/UI/Perps/hooks/useWebSocketHealthToast.ts b/app/components/UI/Perps/hooks/useWebSocketHealthToast.ts index 0a49986705b..f528a4d6853 100644 --- a/app/components/UI/Perps/hooks/useWebSocketHealthToast.ts +++ b/app/components/UI/Perps/hooks/useWebSocketHealthToast.ts @@ -7,6 +7,9 @@ import { useWebSocketHealthToastContext } from '../components/PerpsWebSocketHeal /** Delay before automatically attempting to reconnect after disconnection */ const AUTO_RETRY_DELAY_MS = 10000; +/** Delay before showing offline/connecting banner to avoid flicker on quick reconnects */ +const OFFLINE_BANNER_DELAY_MS = 1000; + /** * Hook to monitor WebSocket connection health and trigger toast notifications * when the connection is lost or restored. @@ -20,8 +23,9 @@ const AUTO_RETRY_DELAY_MS = 10000; * * Behavior: * - On initial connection (fresh mount with CONNECTED state): No toast shown - * - On mount/remount with DISCONNECTED or CONNECTING state: Toast shown immediately - * - On state transitions after mount: Toast shown for reconnection scenarios + * - On mount/remount with DISCONNECTED or CONNECTING state: Toast shown after 1s delay + * - On state transitions after mount: Offline/connecting toasts shown after 1s delay to avoid flicker on quick reconnects + * - Connected toast is shown immediately when connection is restored * - Auto-retry: After 10 seconds in DISCONNECTED state, automatically attempts reconnection */ export function useWebSocketHealthToast(): void { @@ -37,6 +41,30 @@ export function useWebSocketHealthToast(): void { const autoRetryTimeoutRef = useRef | null>( null, ); + // Timer for delayed offline/connecting banner (avoids flicker on quick reconnects) + const showBannerDelayTimeoutRef = useRef | null>(null); + + // Clear show-banner delay timer (so we don't show after reconnecting) + const clearShowBannerDelayTimer = useCallback(() => { + if (showBannerDelayTimeoutRef.current) { + clearTimeout(showBannerDelayTimeoutRef.current); + showBannerDelayTimeoutRef.current = null; + } + }, []); + + // Show offline/connecting toast after delay (only if still disconnected after delay) + const scheduleShowBanner = useCallback( + (connectionState: WebSocketConnectionState, attempt: number) => { + clearShowBannerDelayTimer(); + showBannerDelayTimeoutRef.current = setTimeout(() => { + show(connectionState, attempt); + showBannerDelayTimeoutRef.current = null; + }, OFFLINE_BANNER_DELAY_MS); + }, + [clearShowBannerDelayTimer, show], + ); // Clear auto-retry timer helper const clearAutoRetryTimer = useCallback(() => { @@ -88,16 +116,18 @@ export function useWebSocketHealthToast(): void { previousWsStateRef.current = newState; // If we mount/remount and the connection is already in a problematic state, - // show the toast immediately. This handles the case where a user navigates - // away from Perps and returns while the WebSocket is disconnected or reconnecting. + // show the toast after a delay to avoid flicker on quick reconnects. if (newState === WebSocketConnectionState.Disconnected) { hasExperiencedDisconnectionRef.current = true; - show(WebSocketConnectionState.Disconnected, attempt); + scheduleShowBanner( + WebSocketConnectionState.Disconnected, + attempt, + ); // Schedule auto-retry for disconnected state scheduleAutoRetry(); } else if (newState === WebSocketConnectionState.Connecting) { hasExperiencedDisconnectionRef.current = true; - show(WebSocketConnectionState.Connecting, attempt); + scheduleShowBanner(WebSocketConnectionState.Connecting, attempt); // Clear auto-retry when reconnecting (connection attempt in progress) clearAutoRetryTimer(); } @@ -113,11 +143,14 @@ export function useWebSocketHealthToast(): void { // Handle state transitions switch (newState) { case WebSocketConnectionState.Disconnected: - // Show disconnected toast if: + // Show disconnected toast after delay if: // 1. We were previously connected (direct disconnect), OR // 2. We've been trying to reconnect and gave up (max attempts reached) if (wasWsConnected || hasExperiencedDisconnectionRef.current) { - show(WebSocketConnectionState.Disconnected, attempt); + scheduleShowBanner( + WebSocketConnectionState.Disconnected, + attempt, + ); // Schedule auto-retry for disconnected state scheduleAutoRetry(); } @@ -126,13 +159,18 @@ export function useWebSocketHealthToast(): void { case WebSocketConnectionState.Connecting: // Clear auto-retry when reconnecting (connection attempt in progress) clearAutoRetryTimer(); - // Show connecting toast when reconnecting (after a disconnection) + // Show connecting toast after delay when reconnecting (after a disconnection) if (hasExperiencedDisconnectionRef.current) { - show(WebSocketConnectionState.Connecting, attempt); + scheduleShowBanner( + WebSocketConnectionState.Connecting, + attempt, + ); } break; case WebSocketConnectionState.Connected: + // Clear show-banner delay so we don't show offline toast after reconnecting + clearShowBannerDelayTimer(); // Clear auto-retry when connected clearAutoRetryTimer(); // Show connected toast only if we've experienced a disconnection before @@ -144,7 +182,8 @@ export function useWebSocketHealthToast(): void { break; default: - // DISCONNECTING state - no toast needed + // DISCONNECTING state - no toast needed, cancel any pending banner + clearShowBannerDelayTimer(); clearAutoRetryTimer(); break; } @@ -156,6 +195,7 @@ export function useWebSocketHealthToast(): void { return () => { unsubscribe?.(); + clearShowBannerDelayTimer(); clearAutoRetryTimer(); hide(); }; @@ -164,7 +204,9 @@ export function useWebSocketHealthToast(): void { isInitialized, show, hide, + scheduleShowBanner, scheduleAutoRetry, + clearShowBannerDelayTimer, clearAutoRetryTimer, ]); } diff --git a/app/components/UI/Perps/services/HyperLiquidWalletService.test.ts b/app/components/UI/Perps/services/HyperLiquidWalletService.test.ts index 8af322d133a..21dd0130ae0 100644 --- a/app/components/UI/Perps/services/HyperLiquidWalletService.test.ts +++ b/app/components/UI/Perps/services/HyperLiquidWalletService.test.ts @@ -115,16 +115,24 @@ jest.mock('../../../../core/SDKConnect/utils/DevLogger', () => ({ import type { CaipAccountId } from '@metamask/utils'; import { HyperLiquidWalletService } from './HyperLiquidWalletService'; -import { createMockInfrastructure } from '../__mocks__/serviceMocks'; +import { + createMockInfrastructure, + createMockMessenger, + createMockEvmAccount, +} from '../__mocks__/serviceMocks'; +import type { PerpsControllerMessenger } from '../controllers/PerpsController'; describe('HyperLiquidWalletService', () => { let service: HyperLiquidWalletService; let mockDeps: ReturnType; + let mockMessenger: jest.Mocked; + const mockEvmAccount = createMockEvmAccount(); beforeEach(() => { jest.clearAllMocks(); mockDeps = createMockInfrastructure(); - service = new HyperLiquidWalletService(mockDeps); + mockMessenger = createMockMessenger(); + service = new HyperLiquidWalletService(mockDeps, mockMessenger); }); describe('Constructor and Configuration', () => { @@ -133,9 +141,11 @@ describe('HyperLiquidWalletService', () => { }); it('should initialize with testnet when specified', () => { - const testnetService = new HyperLiquidWalletService(mockDeps, { - isTestnet: true, - }); + const testnetService = new HyperLiquidWalletService( + mockDeps, + mockMessenger, + { isTestnet: true }, + ); expect(testnetService.isTestnetMode()).toBe(true); }); @@ -188,9 +198,11 @@ describe('HyperLiquidWalletService', () => { }); it('should return testnet chain ID when in testnet mode', async () => { - const testnetService = new HyperLiquidWalletService(mockDeps, { - isTestnet: true, - }); + const testnetService = new HyperLiquidWalletService( + mockDeps, + mockMessenger, + { isTestnet: true }, + ); const testnetAdapter = testnetService.createWalletAdapter(); expect(testnetAdapter.getChainId).toBeDefined(); @@ -237,16 +249,15 @@ describe('HyperLiquidWalletService', () => { expect(mockDeps.debugLogger.log).toHaveBeenCalledWith( 'HyperLiquidWalletService: Signing typed data', { - address: '0x1234567890abcdef1234567890abcdef12345678', + address: mockEvmAccount.address, primaryType: 'Order', domain: mockTypedDataParams.domain, }, ); - expect( - mockDeps.controllers.keyring.signTypedMessage, - ).toHaveBeenCalledWith( + expect(mockMessenger.call).toHaveBeenCalledWith( + 'KeyringController:signTypedMessage', { - from: '0x1234567890abcdef1234567890abcdef12345678', + from: mockEvmAccount.address, data: { domain: mockTypedDataParams.domain, types: mockTypedDataParams.types, @@ -259,10 +270,18 @@ describe('HyperLiquidWalletService', () => { }); it('should throw error when no account selected', async () => { - // Mock controllers.accounts to return null (no account selected) - ( - mockDeps.controllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockReturnValueOnce(null); + // Mock messenger to return empty array (no account selected) + (mockMessenger.call as jest.Mock).mockImplementation( + (action: string) => { + if ( + action === + 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return []; + } + return undefined; + }, + ); // Creating wallet adapter should throw when no account expect(() => service.createWalletAdapter()).toThrow( @@ -271,9 +290,20 @@ describe('HyperLiquidWalletService', () => { }); it('should handle keyring controller errors', async () => { - ( - mockDeps.controllers.keyring.signTypedMessage as jest.Mock - ).mockRejectedValueOnce(new Error('Signing failed')); + (mockMessenger.call as jest.Mock).mockImplementation( + (action: string) => { + if ( + action === + 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [mockEvmAccount]; + } + if (action === 'KeyringController:signTypedMessage') { + return Promise.reject(new Error('Signing failed')); + } + return undefined; + }, + ); await expect( walletAdapter.signTypedData(mockTypedDataParams), @@ -286,10 +316,8 @@ describe('HyperLiquidWalletService', () => { it('should get current account ID for mainnet', async () => { const accountId = await service.getCurrentAccountId(); - // Uses address from mockDeps.accountUtils.getSelectedEvmAccount() - expect(accountId).toBe( - 'eip155:42161:0x1234567890abcdef1234567890abcdef12345678', - ); + // Uses address from mockMessenger's AccountTreeController:getAccountsFromSelectedAccountGroup + expect(accountId).toBe(`eip155:42161:${mockEvmAccount.address}`); }); it('should get current account ID for testnet', async () => { @@ -297,16 +325,19 @@ describe('HyperLiquidWalletService', () => { const accountId = await service.getCurrentAccountId(); - expect(accountId).toBe( - 'eip155:421614:0x1234567890abcdef1234567890abcdef12345678', - ); + expect(accountId).toBe(`eip155:421614:${mockEvmAccount.address}`); }); it('should throw error when getting account ID with no selected account', async () => { - // Mock controllers.accounts to return null (no account selected) - ( - mockDeps.controllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockReturnValueOnce(null); + // Mock messenger to return empty array (no account selected) + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return []; + } + return undefined; + }); await expect(service.getCurrentAccountId()).rejects.toThrow( 'NO_ACCOUNT_SELECTED', @@ -346,8 +377,8 @@ describe('HyperLiquidWalletService', () => { it('should get user address with default fallback', async () => { const address = await service.getUserAddressWithDefault(); - // Uses address from mockDeps.accountUtils.getSelectedEvmAccount() - expect(address).toBe('0x1234567890abcdef1234567890abcdef12345678'); + // Uses address from mockMessenger's AccountTreeController:getAccountsFromSelectedAccountGroup + expect(address).toBe(mockEvmAccount.address); }); }); @@ -377,11 +408,14 @@ describe('HyperLiquidWalletService', () => { describe('Error Handling', () => { it('should handle store state errors gracefully', async () => { - // Mock controllers.accounts to throw an error - ( - mockDeps.controllers.accounts.getSelectedEvmAccount as jest.Mock - ).mockImplementationOnce(() => { - throw new Error('Store error'); + // Mock messenger to throw an error + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + throw new Error('Store error'); + } + return undefined; }); await expect(service.getCurrentAccountId()).rejects.toThrow( @@ -405,9 +439,18 @@ describe('HyperLiquidWalletService', () => { it('should handle keyring controller initialization errors', async () => { const walletAdapter = service.createWalletAdapter(); - ( - mockDeps.controllers.keyring.signTypedMessage as jest.Mock - ).mockRejectedValueOnce(new Error('Keyring not initialized')); + // Override messenger.call for the signing call + (mockMessenger.call as jest.Mock).mockImplementation((action: string) => { + if ( + action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' + ) { + return [mockEvmAccount]; + } + if (action === 'KeyringController:signTypedMessage') { + return Promise.reject(new Error('Keyring not initialized')); + } + return undefined; + }); const mockTypedData = { domain: { diff --git a/app/components/UI/Perps/services/HyperLiquidWalletService.ts b/app/components/UI/Perps/services/HyperLiquidWalletService.ts index 250559488fc..7a6e7811d5c 100644 --- a/app/components/UI/Perps/services/HyperLiquidWalletService.ts +++ b/app/components/UI/Perps/services/HyperLiquidWalletService.ts @@ -4,9 +4,15 @@ import { type Hex, isValidHexAddress, } from '@metamask/utils'; +import { + SignTypedDataVersion, + type TypedMessageParams, +} from '@metamask/keyring-controller'; import { getChainId } from '../constants/hyperLiquidConfig'; import { PERPS_ERROR_CODES } from '../controllers/perpsErrorCodes'; import type { PerpsPlatformDependencies } from '../controllers/types'; +import type { PerpsControllerMessenger } from '../controllers/PerpsController'; +import { getSelectedEvmAccount } from '../utils/accountUtils'; /** * Service for MetaMask wallet integration with HyperLiquid SDK @@ -15,17 +21,35 @@ import type { PerpsPlatformDependencies } from '../controllers/types'; export class HyperLiquidWalletService { private isTestnet: boolean; - // Platform dependencies for account access and signing + // Platform dependencies for observability private readonly deps: PerpsPlatformDependencies; + // Messenger for inter-controller communication + private readonly messenger: PerpsControllerMessenger; + constructor( deps: PerpsPlatformDependencies, + messenger: PerpsControllerMessenger, options: { isTestnet?: boolean } = {}, ) { this.deps = deps; + this.messenger = messenger; this.isTestnet = options.isTestnet || false; } + /** + * Sign typed data via messenger + */ + private async signTypedMessage( + msgParams: TypedMessageParams, + ): Promise { + return this.messenger.call( + 'KeyringController:signTypedMessage', + msgParams, + SignTypedDataVersion.V4, + ); + } + /** * Create wallet adapter that implements AbstractViemJsonRpcAccount interface * Required by @nktkas/hyperliquid SDK for signing transactions @@ -47,8 +71,8 @@ export class HyperLiquidWalletService { }) => Promise; getChainId?: () => Promise; } { - // Get current EVM account using the injected controllers.accounts - const evmAccount = this.deps.controllers.accounts.getSelectedEvmAccount(); + // Get current EVM account using messenger + const evmAccount = getSelectedEvmAccount(this.messenger); if (!evmAccount?.address) { throw new Error(PERPS_ERROR_CODES.NO_ACCOUNT_SELECTED); @@ -73,8 +97,7 @@ export class HyperLiquidWalletService { }): Promise => { // Get FRESH account on every sign to handle account switches // This prevents race conditions where wallet adapter was created with old account - const currentEvmAccount = - this.deps.controllers.accounts.getSelectedEvmAccount(); + const currentEvmAccount = getSelectedEvmAccount(this.messenger); if (!currentEvmAccount?.address) { throw new Error(PERPS_ERROR_CODES.NO_ACCOUNT_SELECTED); @@ -99,14 +122,11 @@ export class HyperLiquidWalletService { }, ); - // Use injected controllers.keyring to sign - const signature = await this.deps.controllers.keyring.signTypedMessage( - { - from: currentAddress, - data: typedData, - }, - 'V4', - ); + // Use messenger to sign typed data + const signature = await this.signTypedMessage({ + from: currentAddress, + data: typedData, + }); return signature as Hex; }, @@ -116,10 +136,10 @@ export class HyperLiquidWalletService { } /** - * Get current account ID using the injected controllers.accounts + * Get current account ID using messenger */ public async getCurrentAccountId(): Promise { - const evmAccount = this.deps.controllers.accounts.getSelectedEvmAccount(); + const evmAccount = getSelectedEvmAccount(this.messenger); if (!evmAccount?.address) { throw new Error(PERPS_ERROR_CODES.NO_ACCOUNT_SELECTED); diff --git a/app/components/UI/Perps/utils/accountUtils.test.ts b/app/components/UI/Perps/utils/accountUtils.test.ts index 782f891e5e9..c83e730d37f 100644 --- a/app/components/UI/Perps/utils/accountUtils.test.ts +++ b/app/components/UI/Perps/utils/accountUtils.test.ts @@ -4,6 +4,8 @@ */ import { findEvmAccount, + getEvmAccountFromAccountGroup, + getSelectedEvmAccount, calculateWeightedReturnOnEquity, } from './accountUtils'; import type { InternalAccount } from '@metamask/keyring-internal-api'; @@ -184,6 +186,141 @@ describe('accountUtils', () => { }); }); + describe('getEvmAccountFromAccountGroup', () => { + it('returns object with address when EVM account found', () => { + const mockAccounts = [ + { + address: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef', + id: 'account-1', + type: 'eip155:eoa', + metadata: { + name: 'Ethereum Account', + importTime: 0, + keyring: { type: 'HD Key Tree' }, + }, + methods: [], + options: {}, + scopes: [], + }, + ] as unknown as InternalAccount[]; + + const result = getEvmAccountFromAccountGroup(mockAccounts); + + expect(result).toEqual({ + address: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef', + }); + }); + + it('returns undefined when no EVM account found', () => { + const mockAccounts = [ + { + address: '0x1234567890123456789012345678901234567890', + id: 'account-1', + type: 'btc:p2pkh', + metadata: { + name: 'Bitcoin Account', + importTime: 0, + keyring: { type: 'HD Key Tree' }, + }, + methods: [], + options: {}, + scopes: [], + }, + ] as unknown as InternalAccount[]; + + const result = getEvmAccountFromAccountGroup(mockAccounts); + + expect(result).toBeUndefined(); + }); + + it('returns undefined for empty account list', () => { + const result = getEvmAccountFromAccountGroup([]); + + expect(result).toBeUndefined(); + }); + }); + + describe('getSelectedEvmAccount', () => { + it('returns EVM account when messenger returns accounts with EVM', () => { + const mockAccounts = [ + { + address: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef', + id: 'account-1', + type: 'eip155:eoa', + metadata: { + name: 'Ethereum Account', + importTime: 0, + keyring: { type: 'HD Key Tree' }, + }, + methods: [], + options: {}, + scopes: [], + }, + ] as unknown as InternalAccount[]; + + const mockMessenger = { + call: jest.fn().mockReturnValue(mockAccounts) as jest.MockedFunction< + ( + action: 'AccountTreeController:getAccountsFromSelectedAccountGroup', + ) => InternalAccount[] + >, + }; + + const result = getSelectedEvmAccount(mockMessenger); + + expect(mockMessenger.call).toHaveBeenCalledWith( + 'AccountTreeController:getAccountsFromSelectedAccountGroup', + ); + expect(result).toEqual({ + address: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef', + }); + }); + + it('returns undefined when no EVM account in selected group', () => { + const mockAccounts = [ + { + address: '0x1234567890123456789012345678901234567890', + id: 'account-1', + type: 'btc:p2pkh', + metadata: { + name: 'Bitcoin Account', + importTime: 0, + keyring: { type: 'HD Key Tree' }, + }, + methods: [], + options: {}, + scopes: [], + }, + ] as unknown as InternalAccount[]; + + const mockMessenger = { + call: jest.fn().mockReturnValue(mockAccounts) as jest.MockedFunction< + ( + action: 'AccountTreeController:getAccountsFromSelectedAccountGroup', + ) => InternalAccount[] + >, + }; + + const result = getSelectedEvmAccount(mockMessenger); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when messenger returns empty accounts', () => { + const mockMessenger = { + call: jest.fn().mockReturnValue([]) as jest.MockedFunction< + ( + action: 'AccountTreeController:getAccountsFromSelectedAccountGroup', + ) => InternalAccount[] + >, + }; + + const result = getSelectedEvmAccount(mockMessenger); + + expect(result).toBeUndefined(); + }); + }); + describe('calculateWeightedReturnOnEquity', () => { it('returns "0" for empty array', () => { const result = calculateWeightedReturnOnEquity([]); diff --git a/app/components/UI/Perps/utils/accountUtils.ts b/app/components/UI/Perps/utils/accountUtils.ts index 5996be93d38..9e1a3b914f6 100644 --- a/app/components/UI/Perps/utils/accountUtils.ts +++ b/app/components/UI/Perps/utils/accountUtils.ts @@ -4,9 +4,6 @@ * * Note: This file contains only platform-agnostic (pure) functions * that can be used in the core monorepo. - * - * Mobile-specific functions (like getEvmAccountFromSelectedAccountGroup) - * are defined in adapters/mobileInfrastructure.ts */ import { isEvmAccountType } from '@metamask/keyring-api'; import type { InternalAccount } from '@metamask/keyring-internal-api'; @@ -27,6 +24,48 @@ export function findEvmAccount( return evmAccount || null; } +/** + * Get the EVM account from an account group (array of accounts). + * Uses findEvmAccount internally for the filtering logic. + * This is the multichain-compatible version that works with AccountTreeController. + * + * @param accounts - Array of InternalAccount from AccountTreeController + * @returns Object with address if EVM account found, undefined otherwise + */ +export function getEvmAccountFromAccountGroup( + accounts: InternalAccount[], +): { address: string } | undefined { + const evmAccount = findEvmAccount(accounts); + return evmAccount ? { address: evmAccount.address } : undefined; +} + +/** + * Messenger interface for getSelectedEvmAccount utility + * Only requires the specific messenger.call signature needed + */ +interface AccountTreeMessenger { + call: ( + action: 'AccountTreeController:getAccountsFromSelectedAccountGroup', + ) => InternalAccount[]; +} + +/** + * Get selected EVM account via messenger. + * This utility encapsulates the messenger call + filtering logic to avoid duplication + * across controllers and services. + * + * @param messenger - Any object with a call method that can invoke AccountTreeController + * @returns Object with address if EVM account found, undefined otherwise + */ +export function getSelectedEvmAccount( + messenger: AccountTreeMessenger, +): { address: string } | undefined { + const accounts = messenger.call( + 'AccountTreeController:getAccountsFromSelectedAccountGroup', + ); + return getEvmAccountFromAccountGroup(accounts); +} + /** * Interface for ROE calculation input */ diff --git a/app/components/UI/Perps/utils/rewardsUtils.ts b/app/components/UI/Perps/utils/rewardsUtils.ts index 70b6d65a150..0fb2ccad407 100644 --- a/app/components/UI/Perps/utils/rewardsUtils.ts +++ b/app/components/UI/Perps/utils/rewardsUtils.ts @@ -7,6 +7,7 @@ import { CaipAccountId, parseCaipChainId, } from '@metamask/utils'; +import { ensureError } from '../../../../util/errorUtils'; import { formatChainIdToCaip } from '@metamask/bridge-controller'; import { toChecksumHexAddress } from '@metamask/controller-utils'; import Logger from '../../../../util/Logger'; @@ -40,7 +41,7 @@ export const formatAccountToCaipAccountId = ( return toCaipAccountId(namespace, reference, normalizedAddress); } catch (error) { - Logger.error(error as Error, { + Logger.error(ensureError(error), { message: 'Rewards: Failed to format CAIP Account ID', context: 'rewardsUtils.formatAccountToCaipAccountId', address, @@ -75,7 +76,7 @@ export const handleRewardsError = ( error: unknown, context?: Record, ): string => { - Logger.error(error as Error, { + Logger.error(ensureError(error), { message: 'Rewards: Error occurred', context: 'rewardsUtils.handleRewardsError', additionalContext: context, diff --git a/app/components/UI/Ramp/Aggregator/Views/Settings/Settings.test.tsx b/app/components/UI/Ramp/Aggregator/Views/Settings/Settings.test.tsx index 64fc6daadaf..e74b8386966 100644 --- a/app/components/UI/Ramp/Aggregator/Views/Settings/Settings.test.tsx +++ b/app/components/UI/Ramp/Aggregator/Views/Settings/Settings.test.tsx @@ -85,7 +85,6 @@ jest.mock('../../hooks/useActivationKeys', () => ); const mockSetUserRegion = jest.fn(); -const mockFetchUserRegion = jest.fn(); const mockSetSelectedProvider = jest.fn(); const createMockUserRegion = (regionCode: string): UserRegion => { @@ -117,10 +116,7 @@ const mockUseRampsControllerInitialValues: ReturnType< typeof useRampsController > = { userRegion: createMockUserRegion('eu'), - userRegionLoading: false, - userRegionError: null, setUserRegion: mockSetUserRegion, - fetchUserRegion: mockFetchUserRegion, selectedProvider: null, setSelectedProvider: mockSetSelectedProvider, providers: [], diff --git a/app/components/UI/Ramp/Aggregator/components/PaymentMethodSelector.tsx b/app/components/UI/Ramp/Aggregator/components/PaymentMethodSelector.tsx index 66ab259d2b9..1d3d8c2edb5 100644 --- a/app/components/UI/Ramp/Aggregator/components/PaymentMethodSelector.tsx +++ b/app/components/UI/Ramp/Aggregator/components/PaymentMethodSelector.tsx @@ -1,6 +1,7 @@ import React, { ReactNode } from 'react'; import { View, StyleSheet } from 'react-native'; import Box from './Box'; +import { strings } from '../../../../../../locales/i18n'; import { useTheme } from '../../../../../util/theme'; import { Colors } from '../../../../../util/theme/models'; import ListItem from '../../../../../component-library/components/List/ListItem'; @@ -79,7 +80,11 @@ const PaymentMethodSelector: React.FC = ({ {name} )} - {!loading && } + {!loading && ( + + )} diff --git a/app/components/UI/Ramp/components/TokenSelection/TokenSelection.test.tsx b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.test.tsx index 5e4779323af..4358d9c0b51 100644 --- a/app/components/UI/Ramp/components/TokenSelection/TokenSelection.test.tsx +++ b/app/components/UI/Ramp/components/TokenSelection/TokenSelection.test.tsx @@ -157,9 +157,6 @@ describe('TokenSelection Component', () => { tokensLoading: false, tokensError: null, userRegion: null, - userRegionLoading: false, - userRegionError: null, - fetchUserRegion: jest.fn(), setUserRegion: jest.fn(), selectedProvider: null, setSelectedProvider: jest.fn(), @@ -285,9 +282,6 @@ describe('TokenSelection Component', () => { tokensLoading: true, tokensError: null, userRegion: null, - userRegionLoading: false, - userRegionError: null, - fetchUserRegion: jest.fn(), setUserRegion: jest.fn(), selectedProvider: null, setSelectedProvider: jest.fn(), @@ -333,9 +327,6 @@ describe('TokenSelection Component', () => { tokensLoading: false, tokensError: 'Network error', userRegion: null, - userRegionLoading: false, - userRegionError: null, - fetchUserRegion: jest.fn(), setUserRegion: jest.fn(), selectedProvider: null, setSelectedProvider: jest.fn(), @@ -393,9 +384,6 @@ describe('TokenSelection Component', () => { tokensLoading: false, tokensError: null, userRegion: null, - userRegionLoading: false, - userRegionError: null, - fetchUserRegion: jest.fn(), setUserRegion: jest.fn(), selectedProvider: null, setSelectedProvider: jest.fn(), @@ -463,9 +451,6 @@ describe('TokenSelection Component', () => { tokensLoading: false, tokensError: null, userRegion: null, - userRegionLoading: false, - userRegionError: null, - fetchUserRegion: jest.fn(), setUserRegion: jest.fn(), selectedProvider: null, setSelectedProvider: jest.fn(), @@ -537,9 +522,6 @@ describe('TokenSelection Component', () => { tokensLoading: false, tokensError: null, userRegion: null, - userRegionLoading: false, - userRegionError: null, - fetchUserRegion: jest.fn(), setUserRegion: jest.fn(), selectedProvider: null, setSelectedProvider: jest.fn(), @@ -613,9 +595,6 @@ describe('TokenSelection Component', () => { tokensLoading: false, tokensError: null, userRegion: null, - userRegionLoading: false, - userRegionError: null, - fetchUserRegion: jest.fn(), setUserRegion: jest.fn(), selectedProvider: null, setSelectedProvider: jest.fn(), diff --git a/app/components/UI/Ramp/hooks/useRampNavigation.test.ts b/app/components/UI/Ramp/hooks/useRampNavigation.test.ts index 439ac25c7d9..e0247879ecb 100644 --- a/app/components/UI/Ramp/hooks/useRampNavigation.test.ts +++ b/app/components/UI/Ramp/hooks/useRampNavigation.test.ts @@ -16,6 +16,12 @@ import { import { createEligibilityFailedModalNavigationDetails } from '../components/EligibilityFailedModal/EligibilityFailedModal'; import { createRampUnsupportedModalNavigationDetails } from '../components/RampUnsupportedModal/RampUnsupportedModal'; +const mockSetSelectedToken = jest.fn(); +jest.mock('./useRampsTokens', () => ({ + useRampsTokens: () => ({ + setSelectedToken: mockSetSelectedToken, + }), +})); jest.mock('@react-navigation/native'); jest.mock('@react-navigation/compat', () => ({ withNavigation: jest.fn((component) => component), @@ -80,6 +86,7 @@ const mockGetRampRoutingDecision = describe('useRampNavigation', () => { beforeEach(() => { jest.clearAllMocks(); + mockSetSelectedToken.mockClear(); mockUseNavigation.mockReturnValue({ navigate: mockNavigate, @@ -126,6 +133,7 @@ describe('useRampNavigation', () => { result.current.goToBuy(intent); + expect(mockSetSelectedToken).toHaveBeenCalledWith(intent.assetId); expect(mockCreateBuildQuoteNavDetails).toHaveBeenCalledWith({ assetId: intent.assetId, }); @@ -148,6 +156,7 @@ describe('useRampNavigation', () => { result.current.goToBuy(); + expect(mockSetSelectedToken).not.toHaveBeenCalled(); expect(mockCreateBuildQuoteNavDetails).not.toHaveBeenCalled(); expect(mockCreateTokenSelectionNavigationDetails).toHaveBeenCalled(); expect(mockNavigate).toHaveBeenCalledWith(...mockNavDetails); @@ -162,6 +171,7 @@ describe('useRampNavigation', () => { result.current.goToBuy(intent, { overrideUnifiedRouting: true }); + expect(mockSetSelectedToken).not.toHaveBeenCalled(); expect(mockCreateBuildQuoteNavDetails).not.toHaveBeenCalled(); expect(mockCreateRampNavigationDetails).toHaveBeenCalledWith( AggregatorRampType.BUY, @@ -186,6 +196,7 @@ describe('useRampNavigation', () => { result.current.goToBuy(intent); + expect(mockSetSelectedToken).toHaveBeenCalledWith(intent.assetId); expect(mockCreateBuildQuoteNavDetails).toHaveBeenCalledWith({ assetId: intent.assetId, }); @@ -205,6 +216,7 @@ describe('useRampNavigation', () => { result.current.goToBuy(intent); + expect(mockSetSelectedToken).not.toHaveBeenCalled(); expect(mockNavigate).toHaveBeenCalledWith(...navDetails); expect(mockCreateBuildQuoteNavDetails).not.toHaveBeenCalled(); }); @@ -220,6 +232,7 @@ describe('useRampNavigation', () => { result.current.goToBuy(intent); + expect(mockSetSelectedToken).not.toHaveBeenCalled(); expect(mockNavigate).toHaveBeenCalledWith(...navDetails); expect(mockCreateBuildQuoteNavDetails).not.toHaveBeenCalled(); }); diff --git a/app/components/UI/Ramp/hooks/useRampNavigation.ts b/app/components/UI/Ramp/hooks/useRampNavigation.ts index 8288914a19f..81c74d1d191 100644 --- a/app/components/UI/Ramp/hooks/useRampNavigation.ts +++ b/app/components/UI/Ramp/hooks/useRampNavigation.ts @@ -17,6 +17,7 @@ import { } from '../../../../reducers/fiatOrders'; import { createRampUnsupportedModalNavigationDetails } from '../components/RampUnsupportedModal/RampUnsupportedModal'; import { createEligibilityFailedModalNavigationDetails } from '../components/EligibilityFailedModal/EligibilityFailedModal'; +import { useRampsTokens } from './useRampsTokens'; enum RampMode { AGGREGATOR = 'AGGREGATOR', @@ -37,6 +38,7 @@ export const useRampNavigation = () => { const isRampsUnifiedV1Enabled = useRampsUnifiedV1Enabled(); const isRampsUnifiedV2Enabled = useRampsUnifiedV2Enabled(); const rampRoutingDecision = useSelector(getRampRoutingDecision); + const { setSelectedToken } = useRampsTokens(); const goToBuy = useCallback( ( @@ -74,6 +76,8 @@ export const useRampNavigation = () => { intent?.assetId && !overrideUnifiedRouting ) { + // TODO: Check for provider support for the token and pass params to BuildQuote to show an error modal + setSelectedToken(intent.assetId); navigation.navigate( ...createBuildQuoteNavDetails({ assetId: intent.assetId }), ); @@ -115,6 +119,7 @@ export const useRampNavigation = () => { } }, [ + setSelectedToken, navigation, isRampsUnifiedV1Enabled, isRampsUnifiedV2Enabled, diff --git a/app/components/UI/Ramp/hooks/useRampsController.test.ts b/app/components/UI/Ramp/hooks/useRampsController.test.ts index 2f2875e3297..95355af8519 100644 --- a/app/components/UI/Ramp/hooks/useRampsController.test.ts +++ b/app/components/UI/Ramp/hooks/useRampsController.test.ts @@ -21,9 +21,6 @@ jest.mock( jest.mock('./useRampsUserRegion', () => ({ useRampsUserRegion: jest.fn(() => ({ userRegion: null, - isLoading: false, - error: null, - fetchUserRegion: jest.fn(), setUserRegion: jest.fn(), })), })); @@ -108,8 +105,6 @@ describe('useRampsController', () => { expect(result.current).toMatchObject({ userRegion: null, - userRegionLoading: false, - userRegionError: null, selectedProvider: null, providers: [], providersLoading: false, @@ -127,47 +122,20 @@ describe('useRampsController', () => { paymentMethodsError: null, }); - expect(typeof result.current.fetchUserRegion).toBe('function'); expect(typeof result.current.setUserRegion).toBe('function'); expect(typeof result.current.setSelectedProvider).toBe('function'); expect(typeof result.current.setSelectedToken).toBe('function'); expect(typeof result.current.setSelectedPaymentMethod).toBe('function'); }); - it('passes options to child hooks', () => { - const store = createMockStore(); - renderHook( - () => - useRampsController({ - region: 'us-ny', - action: 'sell', - providerFilters: { - provider: 'test-provider', - crypto: 'ETH', - }, - }), - { - wrapper: wrapper(store), - }, - ); - - expect(useRampsProviders).toHaveBeenCalledWith('us-ny', { - provider: 'test-provider', - crypto: 'ETH', - }); - expect(useRampsTokens).toHaveBeenCalledWith('us-ny', 'sell'); - expect(useRampsCountries).toHaveBeenCalled(); - expect(useRampsPaymentMethods).toHaveBeenCalled(); - }); - - it('passes undefined options when not provided', () => { + it('calls child hooks', () => { const store = createMockStore(); renderHook(() => useRampsController(), { wrapper: wrapper(store), }); - expect(useRampsProviders).toHaveBeenCalledWith(undefined, undefined); - expect(useRampsTokens).toHaveBeenCalledWith(undefined, undefined); + expect(useRampsProviders).toHaveBeenCalled(); + expect(useRampsTokens).toHaveBeenCalled(); expect(useRampsCountries).toHaveBeenCalled(); expect(useRampsPaymentMethods).toHaveBeenCalled(); }); diff --git a/app/components/UI/Ramp/hooks/useRampsController.ts b/app/components/UI/Ramp/hooks/useRampsController.ts index 0c8985b6505..9b7616afdc9 100644 --- a/app/components/UI/Ramp/hooks/useRampsController.ts +++ b/app/components/UI/Ramp/hooks/useRampsController.ts @@ -16,31 +16,6 @@ import { type UseRampsPaymentMethodsResult, } from './useRampsPaymentMethods'; -/** - * Options for the useRampsController hook. - */ -export interface UseRampsControllerOptions { - /** - * Optional region code to use for providers and tokens requests. - * If not provided, uses userRegion from state. - */ - region?: string; - /** - * Optional action type ('buy' or 'sell') for tokens and countries requests. - * Defaults to 'buy'. - */ - action?: 'buy' | 'sell'; - /** - * Optional filter options for providers requests. - */ - providerFilters?: { - provider?: string | string[]; - crypto?: string | string[]; - fiat?: string | string[]; - payments?: string | string[]; - }; -} - /** * Result returned by the useRampsController hook. * This combines all ramps controller functionality into a single interface. @@ -48,9 +23,6 @@ export interface UseRampsControllerOptions { export interface UseRampsControllerResult { // User region userRegion: UseRampsUserRegionResult['userRegion']; - userRegionLoading: UseRampsUserRegionResult['isLoading']; - userRegionError: UseRampsUserRegionResult['error']; - fetchUserRegion: UseRampsUserRegionResult['fetchUserRegion']; setUserRegion: UseRampsUserRegionResult['setUserRegion']; // Selected provider @@ -86,7 +58,6 @@ export interface UseRampsControllerResult { * Composition hook that provides access to all RampsController functionality. * This hook combines all ramps-related hooks into a single entry point. * - * @param options - Optional configuration for the hook. * @returns Combined result from all ramps controller hooks. * * @example @@ -94,9 +65,6 @@ export interface UseRampsControllerResult { * const { * // User region * userRegion, - * userRegionLoading, - * userRegionError, - * fetchUserRegion, * setUserRegion, * * // Providers @@ -125,19 +93,11 @@ export interface UseRampsControllerResult { * paymentMethodsLoading, * paymentMethodsError, * - * } = useRampsController({ action: 'buy' }); + * } = useRampsController(); * ``` */ -export function useRampsController( - options?: UseRampsControllerOptions, -): UseRampsControllerResult { - const { - userRegion, - isLoading: userRegionLoading, - error: userRegionError, - fetchUserRegion, - setUserRegion, - } = useRampsUserRegion(); +export function useRampsController(): UseRampsControllerResult { + const { userRegion, setUserRegion } = useRampsUserRegion(); const { providers, @@ -145,7 +105,7 @@ export function useRampsController( setSelectedProvider, isLoading: providersLoading, error: providersError, - } = useRampsProviders(options?.region, options?.providerFilters); + } = useRampsProviders(); const { tokens, @@ -153,7 +113,7 @@ export function useRampsController( setSelectedToken, isLoading: tokensLoading, error: tokensError, - } = useRampsTokens(options?.region, options?.action); + } = useRampsTokens(); const { countries, @@ -172,9 +132,6 @@ export function useRampsController( return { // User region userRegion, - userRegionLoading, - userRegionError, - fetchUserRegion, setUserRegion, // Selected provider diff --git a/app/components/UI/Ramp/hooks/useRampsCountries.test.ts b/app/components/UI/Ramp/hooks/useRampsCountries.test.ts index d1f25fe30e3..3df3e85be3d 100644 --- a/app/components/UI/Ramp/hooks/useRampsCountries.test.ts +++ b/app/components/UI/Ramp/hooks/useRampsCountries.test.ts @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; import React from 'react'; import { useRampsCountries } from './useRampsCountries'; -import { RequestStatus, type Country } from '@metamask/ramps-controller'; +import { type Country } from '@metamask/ramps-controller'; const mockCountries: Country[] = [ { @@ -32,19 +32,19 @@ const mockCountries: Country[] = [ }, ]; -const createMockStore = (rampsControllerState = {}) => +const createMockStore = (countriesState = {}) => configureStore({ reducer: { engine: () => ({ backgroundState: { RampsController: { - userRegion: null, - selectedProvider: null, - providers: [], - tokens: null, - countries: [], - requests: {}, - ...rampsControllerState, + countries: { + data: [], + selected: null, + isLoading: false, + error: null, + ...countriesState, + }, }, }, }), @@ -77,7 +77,7 @@ describe('useRampsCountries', () => { describe('countries state', () => { it('returns countries from state', () => { - const store = createMockStore({ countries: mockCountries }); + const store = createMockStore({ data: mockCountries }); const { result } = renderHook(() => useRampsCountries(), { wrapper: wrapper(store), }); @@ -94,17 +94,9 @@ describe('useRampsCountries', () => { }); describe('loading state', () => { - it('returns isLoading true when request is loading', () => { + it('returns isLoading true when isLoading is true', () => { const store = createMockStore({ - requests: { - 'getCountries:[]': { - status: RequestStatus.LOADING, - data: null, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, + isLoading: true, }); const { result } = renderHook(() => useRampsCountries(), { wrapper: wrapper(store), @@ -112,7 +104,7 @@ describe('useRampsCountries', () => { expect(result.current.isLoading).toBe(true); }); - it('returns isLoading false when request is not loading', () => { + it('returns isLoading false when isLoading is false', () => { const store = createMockStore(); const { result } = renderHook(() => useRampsCountries(), { wrapper: wrapper(store), @@ -122,17 +114,9 @@ describe('useRampsCountries', () => { }); describe('error state', () => { - it('returns error from request state', () => { + it('returns error from state', () => { const store = createMockStore({ - requests: { - 'getCountries:[]': { - status: RequestStatus.ERROR, - data: null, - error: 'Network error', - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, + error: 'Network error', }); const { result } = renderHook(() => useRampsCountries(), { wrapper: wrapper(store), diff --git a/app/components/UI/Ramp/hooks/useRampsCountries.ts b/app/components/UI/Ramp/hooks/useRampsCountries.ts index f9d79728c85..071a8ee7b57 100644 --- a/app/components/UI/Ramp/hooks/useRampsCountries.ts +++ b/app/components/UI/Ramp/hooks/useRampsCountries.ts @@ -1,12 +1,6 @@ import { useSelector } from 'react-redux'; -import { - selectCountries, - selectCountriesRequest, -} from '../../../../selectors/rampsController'; -import { - RequestSelectorResult, - type Country, -} from '@metamask/ramps-controller'; +import { selectCountries } from '../../../../selectors/rampsController'; +import { type Country } from '@metamask/ramps-controller'; /** * Result returned by the useRampsCountries hook. @@ -33,15 +27,11 @@ export interface UseRampsCountriesResult { * @returns Countries state. */ export function useRampsCountries(): UseRampsCountriesResult { - const countries = useSelector(selectCountries); - - const { isFetching, error } = useSelector( - selectCountriesRequest, - ) as RequestSelectorResult; + const { data: countries, isLoading, error } = useSelector(selectCountries); return { countries, - isLoading: isFetching, + isLoading, error, }; } diff --git a/app/components/UI/Ramp/hooks/useRampsPaymentMethods.test.ts b/app/components/UI/Ramp/hooks/useRampsPaymentMethods.test.ts index 52ece050980..bc9a1b9436f 100644 --- a/app/components/UI/Ramp/hooks/useRampsPaymentMethods.test.ts +++ b/app/components/UI/Ramp/hooks/useRampsPaymentMethods.test.ts @@ -3,11 +3,7 @@ import { Provider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; import React from 'react'; import { useRampsPaymentMethods } from './useRampsPaymentMethods'; -import { - RequestStatus, - type UserRegion, - type PaymentMethod, -} from '@metamask/ramps-controller'; +import { type PaymentMethod } from '@metamask/ramps-controller'; import Engine from '../../../../core/Engine'; jest.mock('../../../../core/Engine', () => ({ @@ -18,48 +14,6 @@ jest.mock('../../../../core/Engine', () => ({ }, })); -const mockUserRegion: UserRegion = { - country: { - isoCode: 'US', - name: 'United States', - flag: '🇺🇸', - phone: { - prefix: '+1', - placeholder: '(XXX) XXX-XXXX', - template: 'XXX-XXX-XXXX', - }, - currency: 'USD', - supported: { buy: true, sell: true }, - }, - state: { stateId: 'CA', name: 'California' }, - regionCode: 'us-ca', -}; - -const mockSelectedToken = { - assetId: 'eip155:1/erc20:0x0000000000000000000000000000000000000000', - chainId: 'eip155:1', - symbol: 'ETH', - name: 'Ethereum', - decimals: 18, - iconUrl: 'https://example.com/eth-icon.png', - tokenSupported: true, -}; - -const mockSelectedProvider = { - id: 'provider-1', - name: 'Provider 1', - environmentType: 'PRODUCTION', - description: 'Provider 1 Description', - hqAddress: '123 Provider 1 St, City, ST 12345', - links: [], - logos: { - light: 'https://example.com/logo1-light.png', - dark: 'https://example.com/logo1-dark.png', - height: 24, - width: 79, - }, -}; - const mockPaymentMethods: PaymentMethod[] = [ { id: '/payments/debit-credit-card', @@ -77,19 +31,19 @@ const mockPaymentMethods: PaymentMethod[] = [ }, ]; -const createMockStore = (rampsControllerState = {}) => +const createMockStore = (paymentMethodsState = {}) => configureStore({ reducer: { engine: () => ({ backgroundState: { RampsController: { - userRegion: null, - selectedToken: null, - selectedProvider: null, - paymentMethods: [], - selectedPaymentMethod: null, - requests: {}, - ...rampsControllerState, + paymentMethods: { + data: [], + selected: null, + isLoading: false, + error: null, + ...paymentMethodsState, + }, }, }, }), @@ -124,7 +78,7 @@ describe('useRampsPaymentMethods', () => { describe('paymentMethods state', () => { it('returns paymentMethods from state', () => { - const store = createMockStore({ paymentMethods: mockPaymentMethods }); + const store = createMockStore({ data: mockPaymentMethods }); const { result } = renderHook(() => useRampsPaymentMethods(), { wrapper: wrapper(store), }); @@ -143,7 +97,7 @@ describe('useRampsPaymentMethods', () => { describe('selectedPaymentMethod state', () => { it('returns selectedPaymentMethod from state', () => { const store = createMockStore({ - selectedPaymentMethod: mockPaymentMethods[0], + selected: mockPaymentMethods[0], }); const { result } = renderHook(() => useRampsPaymentMethods(), { wrapper: wrapper(store), @@ -163,21 +117,9 @@ describe('useRampsPaymentMethods', () => { }); describe('loading state', () => { - it('returns isLoading true when request is loading', () => { + it('returns isLoading true when isLoading is true', () => { const store = createMockStore({ - userRegion: mockUserRegion, - selectedToken: mockSelectedToken, - selectedProvider: mockSelectedProvider, - requests: { - [`getPaymentMethods:["us-ca","usd","${mockSelectedToken.assetId}","${mockSelectedProvider.id}"]`]: - { - status: RequestStatus.LOADING, - data: null, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, + isLoading: true, }); const { result } = renderHook(() => useRampsPaymentMethods(), { wrapper: wrapper(store), @@ -187,21 +129,9 @@ describe('useRampsPaymentMethods', () => { }); describe('error state', () => { - it('returns error from request state', () => { + it('returns error from state', () => { const store = createMockStore({ - userRegion: mockUserRegion, - selectedToken: mockSelectedToken, - selectedProvider: mockSelectedProvider, - requests: { - [`getPaymentMethods:["us-ca","usd","${mockSelectedToken.assetId}","${mockSelectedProvider.id}"]`]: - { - status: RequestStatus.ERROR, - data: null, - error: 'Network error', - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, + error: 'Network error', }); const { result } = renderHook(() => useRampsPaymentMethods(), { wrapper: wrapper(store), diff --git a/app/components/UI/Ramp/hooks/useRampsPaymentMethods.ts b/app/components/UI/Ramp/hooks/useRampsPaymentMethods.ts index 2084fa4047a..8bf26ec9407 100644 --- a/app/components/UI/Ramp/hooks/useRampsPaymentMethods.ts +++ b/app/components/UI/Ramp/hooks/useRampsPaymentMethods.ts @@ -1,18 +1,7 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useSelector } from 'react-redux'; -import { - selectPaymentMethods, - selectPaymentMethodsRequest, - selectSelectedPaymentMethod, - selectSelectedProvider, - selectSelectedToken, - selectUserRegion, -} from '../../../../selectors/rampsController'; -import { - RequestSelectorResult, - type PaymentMethod, - type PaymentMethodsResponse, -} from '@metamask/ramps-controller'; +import { selectPaymentMethods } from '../../../../selectors/rampsController'; +import { type PaymentMethod } from '@metamask/ramps-controller'; import Engine from '../../../../core/Engine'; /** @@ -49,41 +38,12 @@ export interface UseRampsPaymentMethodsResult { * @returns Payment methods state. */ export function useRampsPaymentMethods(): UseRampsPaymentMethodsResult { - const paymentMethods = useSelector(selectPaymentMethods); - const selectedPaymentMethod = useSelector(selectSelectedPaymentMethod); - - const userRegion = useSelector(selectUserRegion); - const selectedToken = useSelector(selectSelectedToken); - const selectedProvider = useSelector(selectSelectedProvider); - - const regionCode = useMemo( - () => userRegion?.regionCode ?? '', - [userRegion?.regionCode], - ); - - const fiat = useMemo( - () => userRegion?.country?.currency ?? '', - [userRegion?.country?.currency], - ); - - const assetId = useMemo( - () => selectedToken?.assetId ?? '', - [selectedToken?.assetId], - ); - - const providerId = useMemo( - () => selectedProvider?.id ?? '', - [selectedProvider?.id], - ); - - const requestSelector = useMemo( - () => selectPaymentMethodsRequest(regionCode, fiat, assetId, providerId), - [regionCode, fiat, assetId, providerId], - ); - - const { isFetching, error } = useSelector( - requestSelector, - ) as RequestSelectorResult; + const { + data: paymentMethods, + selected: selectedPaymentMethod, + isLoading, + error, + } = useSelector(selectPaymentMethods); const setSelectedPaymentMethod = useCallback( (paymentMethod: PaymentMethod | null) => @@ -97,7 +57,7 @@ export function useRampsPaymentMethods(): UseRampsPaymentMethodsResult { paymentMethods, selectedPaymentMethod, setSelectedPaymentMethod, - isLoading: isFetching, + isLoading, error, }; } diff --git a/app/components/UI/Ramp/hooks/useRampsProviders.test.ts b/app/components/UI/Ramp/hooks/useRampsProviders.test.ts index 294c9c91987..b31fcd460e6 100644 --- a/app/components/UI/Ramp/hooks/useRampsProviders.test.ts +++ b/app/components/UI/Ramp/hooks/useRampsProviders.test.ts @@ -3,11 +3,7 @@ import { Provider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; import React from 'react'; import { useRampsProviders } from './useRampsProviders'; -import { - RequestStatus, - type UserRegion, - type Provider as RampProvider, -} from '@metamask/ramps-controller'; +import { type Provider as RampProvider } from '@metamask/ramps-controller'; import Engine from '../../../../core/Engine'; jest.mock('../../../../core/Engine', () => ({ @@ -18,23 +14,6 @@ jest.mock('../../../../core/Engine', () => ({ }, })); -const mockUserRegion: UserRegion = { - country: { - isoCode: 'US', - name: 'United States', - flag: '🇺🇸', - phone: { - prefix: '+1', - placeholder: '(XXX) XXX-XXXX', - template: 'XXX-XXX-XXXX', - }, - currency: 'USD', - supported: { buy: true, sell: true }, - }, - state: { stateId: 'CA', name: 'California' }, - regionCode: 'us-ca', -}; - const mockProviders: RampProvider[] = [ { id: 'provider-1', @@ -66,17 +45,19 @@ const mockProviders: RampProvider[] = [ }, ]; -const createMockStore = (rampsControllerState = {}) => +const createMockStore = (providersState = {}) => configureStore({ reducer: { engine: () => ({ backgroundState: { RampsController: { - userRegion: null, - providers: [], - selectedProvider: null, - requests: {}, - ...rampsControllerState, + providers: { + data: [], + selected: null, + isLoading: false, + error: null, + ...providersState, + }, }, }, }), @@ -109,94 +90,9 @@ describe('useRampsProviders', () => { }); }); - describe('region parameter', () => { - it('uses provided region when specified', () => { - const store = createMockStore({ - requests: { - 'getProviders:["us-ny",null,null,null,null]': { - status: RequestStatus.SUCCESS, - data: { providers: mockProviders }, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - const { result } = renderHook(() => useRampsProviders('us-ny'), { - wrapper: wrapper(store), - }); - expect(result.current.isLoading).toBe(false); - }); - - it('uses userRegion from state when region not provided', () => { - const store = createMockStore({ - userRegion: mockUserRegion, - requests: { - 'getProviders:["us-ca",null,null,null,null]': { - status: RequestStatus.SUCCESS, - data: { providers: mockProviders }, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - const { result } = renderHook(() => useRampsProviders(), { - wrapper: wrapper(store), - }); - expect(result.current.isLoading).toBe(false); - }); - - it('uses empty string when region and userRegion are not available', () => { - const store = createMockStore({ - requests: { - 'getProviders:["",null,null,null,null]': { - status: RequestStatus.SUCCESS, - data: { providers: mockProviders }, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - const { result } = renderHook(() => useRampsProviders(), { - wrapper: wrapper(store), - }); - expect(result.current.isLoading).toBe(false); - }); - }); - - describe('filterOptions parameter', () => { - it('uses filterOptions in request selector', () => { - const store = createMockStore({ - requests: { - 'getProviders:["us-ca","provider-1","ETH","USD",null]': { - status: RequestStatus.SUCCESS, - data: { providers: mockProviders }, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - const { result } = renderHook( - () => - useRampsProviders('us-ca', { - provider: 'provider-1', - crypto: 'ETH', - fiat: 'USD', - }), - { - wrapper: wrapper(store), - }, - ); - expect(result.current.isLoading).toBe(false); - }); - }); - describe('providers state', () => { it('returns providers from state', () => { - const store = createMockStore({ providers: mockProviders }); + const store = createMockStore({ data: mockProviders }); const { result } = renderHook(() => useRampsProviders(), { wrapper: wrapper(store), }); @@ -213,19 +109,11 @@ describe('useRampsProviders', () => { }); describe('loading state', () => { - it('returns isLoading true when request is loading', () => { + it('returns isLoading true when isLoading is true', () => { const store = createMockStore({ - requests: { - 'getProviders:["us-ca",null,null,null,null]': { - status: RequestStatus.LOADING, - data: null, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, + isLoading: true, }); - const { result } = renderHook(() => useRampsProviders('us-ca'), { + const { result } = renderHook(() => useRampsProviders(), { wrapper: wrapper(store), }); expect(result.current.isLoading).toBe(true); @@ -233,19 +121,11 @@ describe('useRampsProviders', () => { }); describe('error state', () => { - it('returns error from request state', () => { + it('returns error from state', () => { const store = createMockStore({ - requests: { - 'getProviders:["us-ca",null,null,null,null]': { - status: RequestStatus.ERROR, - data: null, - error: 'Network error', - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, + error: 'Network error', }); - const { result } = renderHook(() => useRampsProviders('us-ca'), { + const { result } = renderHook(() => useRampsProviders(), { wrapper: wrapper(store), }); expect(result.current.error).toBe('Network error'); @@ -254,7 +134,7 @@ describe('useRampsProviders', () => { describe('selectedProvider state', () => { it('returns selectedProvider from state', () => { - const store = createMockStore({ selectedProvider: mockProviders[0] }); + const store = createMockStore({ selected: mockProviders[0] }); const { result } = renderHook(() => useRampsProviders(), { wrapper: wrapper(store), }); diff --git a/app/components/UI/Ramp/hooks/useRampsProviders.ts b/app/components/UI/Ramp/hooks/useRampsProviders.ts index b99e216f79f..b32532e7d26 100644 --- a/app/components/UI/Ramp/hooks/useRampsProviders.ts +++ b/app/components/UI/Ramp/hooks/useRampsProviders.ts @@ -1,15 +1,7 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useSelector } from 'react-redux'; -import { - selectProviders, - selectProvidersRequest, - selectSelectedProvider, - selectUserRegion, -} from '../../../../selectors/rampsController'; -import { - RequestSelectorResult, - type Provider, -} from '@metamask/ramps-controller'; +import { selectProviders } from '../../../../selectors/rampsController'; +import { type Provider } from '@metamask/ramps-controller'; import Engine from '../../../../core/Engine'; /** @@ -25,7 +17,7 @@ export interface UseRampsProvidersResult { */ selectedProvider: Provider | null; /** - * Sets the selected provider. + * Sets the selected provider by ID. * @param provider - The provider to select, or null to clear selection. */ setSelectedProvider: (provider: Provider | null) => void; @@ -43,50 +35,27 @@ export interface UseRampsProvidersResult { * Hook to get providers state from RampsController. * This hook assumes Engine is already initialized. * - * @param region - Optional region code to use for request state. If not provided, uses userRegion from state. - * @param filterOptions - Optional filter options for the request cache key. * @returns Providers state. */ -export function useRampsProviders( - region?: string, - filterOptions?: { - provider?: string | string[]; - crypto?: string | string[]; - fiat?: string | string[]; - payments?: string | string[]; - }, -): UseRampsProvidersResult { - const providers = useSelector(selectProviders); - const selectedProvider = useSelector(selectSelectedProvider); - const userRegion = useSelector(selectUserRegion); - - const regionCode = useMemo( - () => region ?? userRegion?.regionCode ?? '', - [region, userRegion?.regionCode], - ); +export function useRampsProviders(): UseRampsProvidersResult { + const { + data: providers, + selected: selectedProvider, + isLoading, + error, + } = useSelector(selectProviders); - const requestSelector = useMemo( - () => selectProvidersRequest(regionCode, filterOptions), - [regionCode, filterOptions], + const setSelectedProvider = useCallback( + (provider: Provider | null) => + Engine.context.RampsController.setSelectedProvider(provider?.id ?? null), + [], ); - const { isFetching, error } = useSelector( - requestSelector, - ) as RequestSelectorResult<{ providers: Provider[] }>; - - const setSelectedProvider = useCallback((provider: Provider | null) => { - ( - Engine.context.RampsController.setSelectedProvider as ( - providerId: string | null, - ) => void - )(provider?.id ?? null); - }, []); - return { providers, selectedProvider, setSelectedProvider, - isLoading: isFetching, + isLoading, error, }; } diff --git a/app/components/UI/Ramp/hooks/useRampsTokens.test.ts b/app/components/UI/Ramp/hooks/useRampsTokens.test.ts index c5e84488c8d..df8527039bc 100644 --- a/app/components/UI/Ramp/hooks/useRampsTokens.test.ts +++ b/app/components/UI/Ramp/hooks/useRampsTokens.test.ts @@ -3,7 +3,6 @@ import { Provider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; import React from 'react'; import { useRampsTokens } from './useRampsTokens'; -import { RequestStatus, type UserRegion } from '@metamask/ramps-controller'; import Engine from '../../../../core/Engine'; jest.mock('../../../../core/Engine', () => ({ @@ -14,23 +13,6 @@ jest.mock('../../../../core/Engine', () => ({ }, })); -const mockUserRegion: UserRegion = { - country: { - isoCode: 'US', - name: 'United States', - flag: '🇺🇸', - phone: { - prefix: '+1', - placeholder: '(XXX) XXX-XXXX', - template: 'XXX-XXX-XXXX', - }, - currency: 'USD', - supported: { buy: true, sell: true }, - }, - state: { stateId: 'CA', name: 'California' }, - regionCode: 'us-ca', -}; - const mockSelectedToken = { assetId: 'eip155:1/erc20:0x0000000000000000000000000000000000000000', chainId: 'eip155:1', @@ -53,17 +35,19 @@ const mockTokens = { ], }; -const createMockStore = (rampsControllerState = {}) => +const createMockStore = (tokensState = {}) => configureStore({ reducer: { engine: () => ({ backgroundState: { RampsController: { - userRegion: null, - tokens: null, - selectedToken: null, - requests: {}, - ...rampsControllerState, + tokens: { + data: null, + selected: null, + isLoading: false, + error: null, + ...tokensState, + }, }, }, }), @@ -96,112 +80,9 @@ describe('useRampsTokens', () => { }); }); - describe('region parameter', () => { - it('uses provided region when specified', () => { - const store = createMockStore({ - requests: { - 'getTokens:["us-ny","buy"]': { - status: RequestStatus.SUCCESS, - data: mockTokens, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - const { result } = renderHook(() => useRampsTokens('us-ny', 'buy'), { - wrapper: wrapper(store), - }); - expect(result.current.isLoading).toBe(false); - }); - - it('uses userRegion from state when region not provided', () => { - const store = createMockStore({ - userRegion: mockUserRegion, - requests: { - 'getTokens:["us-ca","buy"]': { - status: RequestStatus.SUCCESS, - data: mockTokens, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - const { result } = renderHook(() => useRampsTokens(undefined, 'buy'), { - wrapper: wrapper(store), - }); - expect(result.current.isLoading).toBe(false); - }); - - it('uses empty string when region and userRegion are not available', () => { - const store = createMockStore({ - requests: { - 'getTokens:["","buy"]': { - status: RequestStatus.SUCCESS, - data: mockTokens, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - const { result } = renderHook(() => useRampsTokens(), { - wrapper: wrapper(store), - }); - expect(result.current.isLoading).toBe(false); - }); - }); - - describe('action parameter', () => { - it('defaults to buy when not provided', () => { - const store = createMockStore(); - const { result } = renderHook(() => useRampsTokens(), { - wrapper: wrapper(store), - }); - expect(result.current).toBeDefined(); - }); - - it('uses buy action when provided', () => { - const store = createMockStore({ - requests: { - 'getTokens:["us-ca","buy"]': { - status: RequestStatus.SUCCESS, - data: mockTokens, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - const { result } = renderHook(() => useRampsTokens('us-ca', 'buy'), { - wrapper: wrapper(store), - }); - expect(result.current.isLoading).toBe(false); - }); - - it('uses sell action when provided', () => { - const store = createMockStore({ - requests: { - 'getTokens:["us-ca","sell"]': { - status: RequestStatus.SUCCESS, - data: mockTokens, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - const { result } = renderHook(() => useRampsTokens('us-ca', 'sell'), { - wrapper: wrapper(store), - }); - expect(result.current.isLoading).toBe(false); - }); - }); - describe('tokens state', () => { it('returns tokens from state', () => { - const store = createMockStore({ tokens: mockTokens }); + const store = createMockStore({ data: mockTokens }); const { result } = renderHook(() => useRampsTokens(), { wrapper: wrapper(store), }); @@ -218,19 +99,11 @@ describe('useRampsTokens', () => { }); describe('loading state', () => { - it('returns isLoading true when request is loading', () => { + it('returns isLoading true when isLoading is true', () => { const store = createMockStore({ - requests: { - 'getTokens:["us-ca","buy"]': { - status: RequestStatus.LOADING, - data: null, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, + isLoading: true, }); - const { result } = renderHook(() => useRampsTokens('us-ca', 'buy'), { + const { result } = renderHook(() => useRampsTokens(), { wrapper: wrapper(store), }); expect(result.current.isLoading).toBe(true); @@ -238,19 +111,11 @@ describe('useRampsTokens', () => { }); describe('error state', () => { - it('returns error from request state', () => { + it('returns error from state', () => { const store = createMockStore({ - requests: { - 'getTokens:["us-ca","buy"]': { - status: RequestStatus.ERROR, - data: null, - error: 'Network error', - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, + error: 'Network error', }); - const { result } = renderHook(() => useRampsTokens('us-ca', 'buy'), { + const { result } = renderHook(() => useRampsTokens(), { wrapper: wrapper(store), }); expect(result.current.error).toBe('Network error'); @@ -259,7 +124,7 @@ describe('useRampsTokens', () => { describe('selectedToken state', () => { it('returns selectedToken from state', () => { - const store = createMockStore({ selectedToken: mockSelectedToken }); + const store = createMockStore({ selected: mockSelectedToken }); const { result } = renderHook(() => useRampsTokens(), { wrapper: wrapper(store), }); @@ -281,14 +146,15 @@ describe('useRampsTokens', () => { const { result } = renderHook(() => useRampsTokens(), { wrapper: wrapper(store), }); + const assetId = mockSelectedToken.assetId; act(() => { - result.current.setSelectedToken(mockSelectedToken.assetId); + result.current.setSelectedToken(assetId); }); expect( Engine.context.RampsController.setSelectedToken, - ).toHaveBeenCalledWith(mockSelectedToken.assetId); + ).toHaveBeenCalledWith(assetId); }); }); }); diff --git a/app/components/UI/Ramp/hooks/useRampsTokens.ts b/app/components/UI/Ramp/hooks/useRampsTokens.ts index 4174d143cf4..74c3bb2ed64 100644 --- a/app/components/UI/Ramp/hooks/useRampsTokens.ts +++ b/app/components/UI/Ramp/hooks/useRampsTokens.ts @@ -1,20 +1,12 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useSelector } from 'react-redux'; +import { selectTokens } from '../../../../selectors/rampsController'; import { - selectTokens, - selectTokensRequest, - selectSelectedToken, - selectUserRegion, -} from '../../../../selectors/rampsController'; -import { - RequestSelectorResult, - type RampsControllerState, + type RampsToken, + type TokensResponse, } from '@metamask/ramps-controller'; import Engine from '../../../../core/Engine'; -type TokensResponse = NonNullable; -type SelectedToken = RampsControllerState['selectedToken']; - /** * Result returned by the useRampsTokens hook. */ @@ -26,12 +18,12 @@ export interface UseRampsTokensResult { /** * The currently selected token, or null if none selected. */ - selectedToken: SelectedToken; + selectedToken: RampsToken | null; /** * Sets the selected token by asset ID. - * @param assetId - The asset identifier in CAIP-19 format (e.g., "eip155:1/erc20:0x...") + * @param assetId - The asset ID of the token to select. */ - setSelectedToken: (assetId?: string) => void; + setSelectedToken: (assetId: string) => void; /** * Whether the tokens request is currently loading. */ @@ -46,34 +38,18 @@ export interface UseRampsTokensResult { * Hook to get tokens state from RampsController. * This hook assumes Engine is already initialized. * - * @param region - Optional region code to use for request state. If not provided, uses userRegion from state. - * @param action - Optional action type ('buy' or 'sell'). Defaults to 'buy'. * @returns Tokens state. */ -export function useRampsTokens( - region?: string, - action: 'buy' | 'sell' = 'buy', -): UseRampsTokensResult { - const tokens = useSelector(selectTokens); - const selectedToken = useSelector(selectSelectedToken); - const userRegion = useSelector(selectUserRegion); - - const regionCode = useMemo( - () => region ?? userRegion?.regionCode ?? '', - [region, userRegion?.regionCode], - ); - - const requestSelector = useMemo( - () => selectTokensRequest(regionCode, action), - [regionCode, action], - ); - - const { isFetching, error } = useSelector( - requestSelector, - ) as RequestSelectorResult; +export function useRampsTokens(): UseRampsTokensResult { + const { + data: tokens, + selected: selectedToken, + isLoading, + error, + } = useSelector(selectTokens); const setSelectedToken = useCallback( - (assetId?: string) => + (assetId: string) => Engine.context.RampsController.setSelectedToken(assetId), [], ); @@ -82,7 +58,7 @@ export function useRampsTokens( tokens, selectedToken, setSelectedToken, - isLoading: isFetching, + isLoading, error, }; } diff --git a/app/components/UI/Ramp/hooks/useRampsUserRegion.test.ts b/app/components/UI/Ramp/hooks/useRampsUserRegion.test.ts index 89b2d3432a2..b6fb2dc498f 100644 --- a/app/components/UI/Ramp/hooks/useRampsUserRegion.test.ts +++ b/app/components/UI/Ramp/hooks/useRampsUserRegion.test.ts @@ -1,9 +1,9 @@ -import { renderHook, waitFor } from '@testing-library/react-native'; +import { renderHook, act } from '@testing-library/react-native'; import { Provider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; import React from 'react'; import { useRampsUserRegion } from './useRampsUserRegion'; -import { RequestStatus, UserRegion } from '@metamask/ramps-controller'; +import { UserRegion } from '@metamask/ramps-controller'; import Engine from '../../../../core/Engine'; const mockUserRegion: UserRegion = { @@ -44,22 +44,19 @@ jest.mock('../../../../core/Engine', () => { return { context: { RampsController: { - init: jest.fn().mockResolvedValue(mockUserRegionValue), setUserRegion: jest.fn().mockResolvedValue(mockUserRegionValue), }, }, }; }); -const createMockStore = (rampsControllerState = {}) => +const createMockStore = (userRegion: UserRegion | null = null) => configureStore({ reducer: { engine: () => ({ backgroundState: { RampsController: { - userRegion: null, - requests: {}, - ...rampsControllerState, + userRegion, }, }, }), @@ -77,24 +74,21 @@ describe('useRampsUserRegion', () => { }); describe('return value structure', () => { - it('returns userRegion, isLoading, error, fetchUserRegion, and setUserRegion', () => { + it('returns userRegion and setUserRegion', () => { const store = createMockStore(); const { result } = renderHook(() => useRampsUserRegion(), { wrapper: wrapper(store), }); expect(result.current).toMatchObject({ userRegion: null, - isLoading: false, - error: null, }); - expect(typeof result.current.fetchUserRegion).toBe('function'); expect(typeof result.current.setUserRegion).toBe('function'); }); }); describe('userRegion state', () => { it('returns userRegion from state', () => { - const store = createMockStore({ userRegion: mockUserRegion }); + const store = createMockStore(mockUserRegion); const { result } = renderHook(() => useRampsUserRegion(), { wrapper: wrapper(store), }); @@ -102,103 +96,17 @@ describe('useRampsUserRegion', () => { }); }); - describe('loading state', () => { - it('returns isLoading true when request is loading', () => { - const store = createMockStore({ - requests: { - 'init:[]': { - status: RequestStatus.LOADING, - data: null, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - const { result } = renderHook(() => useRampsUserRegion(), { - wrapper: wrapper(store), - }); - expect(result.current.isLoading).toBe(true); - }); - }); - - describe('error state', () => { - it('returns error from request state', () => { - const store = createMockStore({ - requests: { - 'init:[]': { - status: RequestStatus.ERROR, - data: null, - error: 'Network error', - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - const { result } = renderHook(() => useRampsUserRegion(), { - wrapper: wrapper(store), - }); - expect(result.current.error).toBe('Network error'); - }); - }); - - describe('fetchUserRegion', () => { - it('calls init without options when called with no arguments', async () => { - const store = createMockStore(); - const { result } = renderHook(() => useRampsUserRegion(), { - wrapper: wrapper(store), - }); - await result.current.fetchUserRegion(); - expect(Engine.context.RampsController.init).toHaveBeenCalledWith( - undefined, - ); - }); - - it('calls init with forceRefresh true when specified', async () => { - const store = createMockStore(); - const { result } = renderHook(() => useRampsUserRegion(), { - wrapper: wrapper(store), - }); - await result.current.fetchUserRegion({ forceRefresh: true }); - expect(Engine.context.RampsController.init).toHaveBeenCalledWith({ - forceRefresh: true, - }); - }); - - it('calls init with forceRefresh false when specified', async () => { + describe('setUserRegion', () => { + it('calls setUserRegion on controller', async () => { const store = createMockStore(); const { result } = renderHook(() => useRampsUserRegion(), { wrapper: wrapper(store), }); - await result.current.fetchUserRegion({ forceRefresh: false }); - expect(Engine.context.RampsController.init).toHaveBeenCalledWith({ - forceRefresh: false, - }); - }); - - it('rejects with error when init fails', async () => { - const store = createMockStore(); - const mockInit = Engine.context.RampsController.init as jest.Mock; - mockInit.mockReset(); - mockInit.mockRejectedValue(new Error('Network error')); - const { result } = renderHook(() => useRampsUserRegion(), { - wrapper: wrapper(store), + await act(async () => { + await result.current.setUserRegion('US-CA'); }); - await expect(result.current.fetchUserRegion()).rejects.toThrow( - 'Network error', - ); - }); - }); - - describe('setUserRegion', () => { - it('calls setUserRegion on controller', async () => { - const store = createMockStore(); - const { result } = renderHook(() => useRampsUserRegion(), { - wrapper: wrapper(store), - }); - await result.current.setUserRegion('US-CA'); expect(Engine.context.RampsController.setUserRegion).toHaveBeenCalledWith( 'US-CA', undefined, @@ -210,35 +118,15 @@ describe('useRampsUserRegion', () => { const { result } = renderHook(() => useRampsUserRegion(), { wrapper: wrapper(store), }); - await result.current.setUserRegion('US-CA', { forceRefresh: true }); + + await act(async () => { + await result.current.setUserRegion('US-CA', { forceRefresh: true }); + }); + expect(Engine.context.RampsController.setUserRegion).toHaveBeenCalledWith( 'US-CA', { forceRefresh: true }, ); }); }); - - describe('useEffect error handling', () => { - it('returns default state when fetchUserRegion rejects in useEffect', async () => { - const store = createMockStore(); - const mockInit = Engine.context.RampsController.init as jest.Mock; - mockInit.mockReset(); - mockInit.mockRejectedValue(new Error('Fetch failed')); - - const { result } = renderHook(() => useRampsUserRegion(), { - wrapper: wrapper(store), - }); - - await waitFor(() => { - expect(mockInit).toHaveBeenCalled(); - }); - - expect(result.current).toMatchObject({ - userRegion: null, - isLoading: false, - error: null, - }); - expect(typeof result.current.fetchUserRegion).toBe('function'); - }); - }); }); diff --git a/app/components/UI/Ramp/hooks/useRampsUserRegion.ts b/app/components/UI/Ramp/hooks/useRampsUserRegion.ts index 4c8745531bf..57789decd10 100644 --- a/app/components/UI/Ramp/hooks/useRampsUserRegion.ts +++ b/app/components/UI/Ramp/hooks/useRampsUserRegion.ts @@ -1,13 +1,9 @@ -import { useCallback, useEffect } from 'react'; +import { useCallback } from 'react'; import { useSelector } from 'react-redux'; import Engine from '../../../../core/Engine'; -import { - selectUserRegion, - selectUserRegionRequest, -} from '../../../../selectors/rampsController'; +import { selectUserRegion } from '../../../../selectors/rampsController'; import { ExecuteRequestOptions, - RequestSelectorResult, type UserRegion, } from '@metamask/ramps-controller'; @@ -15,25 +11,7 @@ import { * Result returned by the useRampsUserRegion hook. */ export interface UseRampsUserRegionResult { - /** - * The user's region object with country, state, and regionCode, or null if not loaded. - */ userRegion: UserRegion | null; - /** - * Whether the user region request is currently loading. - */ - isLoading: boolean; - /** - * The error message if the request failed, or null. - */ - error: string | null; - /** - * Manually fetch the user region from geolocation. - */ - fetchUserRegion: (options?: ExecuteRequestOptions) => Promise; - /** - * Set the user region manually (without fetching geolocation). - */ setUserRegion: ( region: string, options?: ExecuteRequestOptions, @@ -43,20 +21,9 @@ export interface UseRampsUserRegionResult { /** * Hook to get the user's region state from RampsController. * This hook assumes Engine is already initialized. - * - * @returns User region state and fetch/set functions. */ export function useRampsUserRegion(): UseRampsUserRegionResult { const userRegion = useSelector(selectUserRegion); - const { isFetching, error } = useSelector( - selectUserRegionRequest, - ) as RequestSelectorResult; - - const fetchUserRegion = useCallback( - async (options?: ExecuteRequestOptions) => - await Engine.context.RampsController.init(options), - [], - ); const setUserRegion = useCallback( (region: string, options?: ExecuteRequestOptions) => @@ -64,17 +31,8 @@ export function useRampsUserRegion(): UseRampsUserRegionResult { [], ); - useEffect(() => { - fetchUserRegion().catch(() => { - // Error is stored in state - }); - }, [fetchUserRegion]); - return { userRegion, - isLoading: isFetching, - error, - fetchUserRegion, setUserRegion, }; } diff --git a/app/components/UI/Rewards/components/EndOfSeasonClaimBottomSheet/EndOfSeasonClaimBottomSheet.test.tsx b/app/components/UI/Rewards/components/EndOfSeasonClaimBottomSheet/EndOfSeasonClaimBottomSheet.test.tsx index ec298ab52f4..3b547080f21 100644 --- a/app/components/UI/Rewards/components/EndOfSeasonClaimBottomSheet/EndOfSeasonClaimBottomSheet.test.tsx +++ b/app/components/UI/Rewards/components/EndOfSeasonClaimBottomSheet/EndOfSeasonClaimBottomSheet.test.tsx @@ -41,17 +41,21 @@ jest.mock('@react-navigation/native', () => ({ }), })); -// Mock useMetrics +// Mock useAnalytics const mockTrackEvent = jest.fn(); const mockCreateEventBuilder = jest.fn(() => ({ addProperties: jest.fn().mockReturnThis(), build: jest.fn().mockReturnValue({}), })); -jest.mock('../../../../hooks/useMetrics', () => ({ - useMetrics: () => ({ +jest.mock('../../../../hooks/useAnalytics/useAnalytics', () => ({ + useAnalytics: () => ({ trackEvent: mockTrackEvent, createEventBuilder: mockCreateEventBuilder, }), +})); + +// Mock MetaMetricsEvents (still imported from useMetrics) +jest.mock('../../../../hooks/useMetrics', () => ({ MetaMetricsEvents: { REWARDS_REWARD_VIEWED: 'REWARDS_REWARD_VIEWED', REWARDS_REWARD_CLAIMED: 'REWARDS_REWARD_CLAIMED', diff --git a/app/components/UI/Rewards/components/EndOfSeasonClaimBottomSheet/EndOfSeasonClaimBottomSheet.tsx b/app/components/UI/Rewards/components/EndOfSeasonClaimBottomSheet/EndOfSeasonClaimBottomSheet.tsx index e910e251ae9..44836c07a14 100644 --- a/app/components/UI/Rewards/components/EndOfSeasonClaimBottomSheet/EndOfSeasonClaimBottomSheet.tsx +++ b/app/components/UI/Rewards/components/EndOfSeasonClaimBottomSheet/EndOfSeasonClaimBottomSheet.tsx @@ -9,7 +9,8 @@ import BottomSheet, { BottomSheetRef, } from '../../../../../component-library/components/BottomSheets/BottomSheet'; import useRewardsToast from '../../hooks/useRewardsToast'; -import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; +import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; +import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics'; import { useNavigation } from '@react-navigation/native'; import { strings } from '../../../../../../locales/i18n'; import { TouchableOpacity } from 'react-native'; @@ -94,7 +95,7 @@ const EndOfSeasonClaimBottomSheet = ({ const tw = useTailwind(); const { showToast: showRewardsToast, RewardsToastOptions } = useRewardsToast(); - const { trackEvent, createEventBuilder } = useMetrics(); + const { trackEvent, createEventBuilder } = useAnalytics(); const { claimReward, isClaimingReward } = useClaimReward(); const { lineaTokenReward } = useLineaSeasonOneTokenReward(); const { diff --git a/app/components/UI/TokenDetails/components/AssetOverviewContent.tsx b/app/components/UI/TokenDetails/components/AssetOverviewContent.tsx index edc96effd87..9f307d76f96 100644 --- a/app/components/UI/TokenDetails/components/AssetOverviewContent.tsx +++ b/app/components/UI/TokenDetails/components/AssetOverviewContent.tsx @@ -25,7 +25,7 @@ import { } from '../../../hooks/useTokenHistoricalPrices'; import { TokenI } from '../../Tokens/types'; import { usePerpsMarketForAsset } from '../../Perps/hooks/usePerpsMarketForAsset'; -import { PerpsEventValues } from '../../Perps/constants/eventNames'; +import { PERPS_EVENT_VALUE } from '../../Perps/constants/eventNames'; import Price from '../../AssetOverview/Price'; import ChartNavigationButton from '../../AssetOverview/ChartNavigationButton'; import Balance from '../../AssetOverview/Balance'; @@ -192,7 +192,7 @@ const AssetOverviewContent: React.FC = ({ screen: Routes.PERPS.MARKET_DETAILS, params: { market: marketData, - source: PerpsEventValues.SOURCE.ASSET_DETAIL_SCREEN, + source: PERPS_EVENT_VALUE.SOURCE.ASSET_DETAIL_SCREEN, }, }); } diff --git a/app/components/Views/AssetDetails/index.tsx b/app/components/Views/AssetDetails/index.tsx index 2f56e099019..efa4f29e05b 100644 --- a/app/components/Views/AssetDetails/index.tsx +++ b/app/components/Views/AssetDetails/index.tsx @@ -63,7 +63,7 @@ import { areAddressesEqual } from '../../../util/address'; import { selectPerpsEnabledFlag } from '../../UI/Perps'; import { usePerpsMarketForAsset } from '../../UI/Perps/hooks/usePerpsMarketForAsset'; import PerpsDiscoveryBanner from '../../UI/Perps/components/PerpsDiscoveryBanner'; -import { PerpsEventValues } from '../../UI/Perps/constants/eventNames'; +import { PERPS_EVENT_VALUE } from '../../UI/Perps/constants/eventNames'; import { isTokenTrustworthyForPerps } from '../../UI/Perps/constants/perpsConfig'; import type { PerpsNavigationParamList } from '../../UI/Perps/types/navigation'; @@ -207,7 +207,7 @@ const AssetDetails = (props: InnerProps) => { screen: Routes.PERPS.MARKET_DETAILS, params: { market: marketData, - source: PerpsEventValues.SOURCE.ASSET_DETAIL_SCREEN, + source: PERPS_EVENT_VALUE.SOURCE.ASSET_DETAIL_SCREEN, }, }); } diff --git a/app/components/Views/MultichainAccounts/AddressList/AddressList.test.tsx b/app/components/Views/MultichainAccounts/AddressList/AddressList.test.tsx index 1e06dba7089..5febaf2a16c 100644 --- a/app/components/Views/MultichainAccounts/AddressList/AddressList.test.tsx +++ b/app/components/Views/MultichainAccounts/AddressList/AddressList.test.tsx @@ -9,6 +9,7 @@ import renderWithProvider from '../../../../util/test/renderWithProvider'; import { AddressList } from './AddressList'; import { MULTICHAIN_ADDRESS_ROW_QR_BUTTON_TEST_ID } from '../../../../component-library/components-temp/MultichainAccounts/MultichainAddressRow'; import { toFormattedAddress } from '../../../../util/address'; +import { EVENT_NAME } from '../../../../core/Analytics/MetaMetrics.events'; const ACCOUNT_WALLET_ID = 'entropy:wallet-id-1' as AccountWalletId; const ACCOUNT_GROUP_ID = 'entropy:wallet-id-1/1' as AccountGroupId; @@ -36,6 +37,24 @@ jest.mock('../../../../util/navigation/navUtils', () => ({ createNavigationDetails: jest.fn(), })); +const mockTrackEvent = jest.fn(); +const mockBuild = jest.fn().mockReturnValue({}); +const mockAddProperties = jest.fn().mockReturnValue({ build: mockBuild }); +const mockCreateEventBuilder = jest.fn().mockReturnValue({ + addProperties: mockAddProperties, +}); + +jest.mock('../../../hooks/useAnalytics/useAnalytics', () => ({ + useAnalytics: () => ({ + trackEvent: mockTrackEvent, + createEventBuilder: mockCreateEventBuilder, + }), +})); + +jest.mock('../../../../core/ClipboardManager', () => ({ + setString: jest.fn(), +})); + const mockEthEoaAccount = { ...createMockInternalAccount( '0x4fec2622fb662e892dd0e5060b91fa49ddcfdcb5', @@ -211,4 +230,82 @@ describe('AddressList', () => { }, ); }); + + describe('Analytics tracking', () => { + beforeEach(() => { + mockTrackEvent.mockClear(); + mockCreateEventBuilder.mockClear(); + mockAddProperties.mockClear(); + mockBuild.mockClear(); + }); + + it('tracks "Copied Address" event when copy button is pressed', async () => { + const { getAllByTestId } = renderWithAddressList(); + + // Find the copy button for the first Ethereum address + const copyButton = getAllByTestId( + 'multichain-address-row-copy-button', + )[0]; + + // Press the copy button + fireEvent.press(copyButton); + + // Wait for async operations + await new Promise(process.nextTick); + + // Verify createEventBuilder was called with correct event name + expect(mockCreateEventBuilder).toHaveBeenCalledWith( + EVENT_NAME.ADDRESS_COPIED, + ); + + // Verify addProperties was called with correct properties + expect(mockAddProperties).toHaveBeenCalledWith({ + location: 'address-list', + chain_id_caip: 'eip155:1', // CAIP format chain ID + }); + + // Verify build was called + expect(mockBuild).toHaveBeenCalled(); + + // Verify trackEvent was called with the built event + expect(mockTrackEvent).toHaveBeenCalled(); + }); + + it('tracks event with correct chain_id for different networks', async () => { + const { getAllByTestId } = renderWithAddressList(); + + // Get all copy buttons (should be multiple for different networks) + const copyButtons = getAllByTestId('multichain-address-row-copy-button'); + + // Ensure we have multiple copy buttons for different networks + expect(copyButtons.length).toBeGreaterThan(1); + + // Press the second copy button (Solana Mainnet - rendered after ETH addresses) + fireEvent.press(copyButtons[1]); + + await new Promise(process.nextTick); + + // Verify the chain_id_caip is correctly passed in CAIP format + expect(mockAddProperties).toHaveBeenCalledWith({ + location: 'address-list', + chain_id_caip: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // Solana Mainnet + }); + }); + + it('includes location property as "address-list"', async () => { + const { getAllByTestId } = renderWithAddressList(); + + const copyButton = getAllByTestId( + 'multichain-address-row-copy-button', + )[0]; + fireEvent.press(copyButton); + + await new Promise(process.nextTick); + + // Access the first call from this test (now properly cleared between tests) + const addPropertiesCall = mockAddProperties.mock.calls[0][0]; + + expect(addPropertiesCall).toHaveProperty('location', 'address-list'); + }); + }); }); diff --git a/app/components/Views/MultichainAccounts/AddressList/AddressList.tsx b/app/components/Views/MultichainAccounts/AddressList/AddressList.tsx index d12a672d23f..6544fe03e36 100644 --- a/app/components/Views/MultichainAccounts/AddressList/AddressList.tsx +++ b/app/components/Views/MultichainAccounts/AddressList/AddressList.tsx @@ -5,6 +5,7 @@ import { useNavigation } from '@react-navigation/native'; import { FlashList } from '@shopify/flash-list'; import { useStyles } from '../../../hooks/useStyles'; +import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics'; import { selectInternalAccountListSpreadByScopesByGroupId } from '../../../../selectors/multichainAccounts/accounts'; import { IconName } from '@metamask/design-system-react-native'; import MultichainAddressRow, { @@ -23,6 +24,7 @@ import ClipboardManager from '../../../../core/ClipboardManager'; import getHeaderCenterNavbarOptions from '../../../../component-library/components-temp/HeaderCenter/getHeaderCenterNavbarOptions'; import { ToastContext } from '../../../../component-library/components/Toast'; import { strings } from '../../../../../locales/i18n'; +import { EVENT_NAME } from '../../../../core/Analytics/MetaMetrics.events'; export const createAddressListNavigationDetails = createNavigationDetails( @@ -38,6 +40,7 @@ export const AddressList = () => { const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); const { toastRef } = useContext(ToastContext); + const { trackEvent, createEventBuilder } = useAnalytics(); const { groupId, title, onLoad } = useParams(); @@ -51,6 +54,15 @@ export const AddressList = () => { ({ item }: { item: AddressItem }) => { const copyAddressToClipboard = async () => { await ClipboardManager.setString(item.account.address); + + trackEvent( + createEventBuilder(EVENT_NAME.ADDRESS_COPIED) + .addProperties({ + location: 'address-list', + chain_id_caip: item.scope, + }) + .build(), + ); }; return ( { /> ); }, - [navigation, groupId, toastRef], + [navigation, groupId, toastRef, trackEvent, createEventBuilder], ); useLayoutEffect(() => { diff --git a/app/components/Views/MultichainAccounts/sheets/ShareAddress/ShareAddress.tsx b/app/components/Views/MultichainAccounts/sheets/ShareAddress/ShareAddress.tsx index 72522e6983a..04eb375a723 100644 --- a/app/components/Views/MultichainAccounts/sheets/ShareAddress/ShareAddress.tsx +++ b/app/components/Views/MultichainAccounts/sheets/ShareAddress/ShareAddress.tsx @@ -87,7 +87,11 @@ export const ShareAddress = () => { /> - + diff --git a/app/components/Views/MultichainAccounts/sheets/ShareAddressQR/ShareAddressQR.tsx b/app/components/Views/MultichainAccounts/sheets/ShareAddressQR/ShareAddressQR.tsx index 0c2723e0c52..86882c1b185 100644 --- a/app/components/Views/MultichainAccounts/sheets/ShareAddressQR/ShareAddressQR.tsx +++ b/app/components/Views/MultichainAccounts/sheets/ShareAddressQR/ShareAddressQR.tsx @@ -93,6 +93,8 @@ export const ShareAddressQR = () => { ({ + __esModule: true, + default: () => mockCopyToClipboard, + CopyClipboardAlertMessage: { + address: jest.fn().mockReturnValue('Address copied to clipboard'), + }, +})); + +jest.mock('../../../../hooks/useAnalytics/useAnalytics', () => ({ + useAnalytics: () => ({ + trackEvent: mockTrackEvent, + createEventBuilder: mockCreateEventBuilder, + }), +})); + +jest.mock('../useStyles', () => ({ + __esModule: true, + default: () => ({ + styles: { + row: {}, + badgeWrapper: {}, + boxLeft: {}, + copyContainer: {}, + addressLinkLabel: {}, + copyIconDefault: {}, + }, + }), +})); + +describe('AddressField', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const renderAddressField = (props: Partial = {}) => { + const defaultProps: ModalFieldAddress = { + type: ModalFieldType.ADDRESS, + label: TEST_LABEL, + address: TEST_ADDRESS, + }; + + return renderWithProvider(, { + state: {}, + }); + }; + + it('renders correctly with label and address', () => { + const { getByText } = renderAddressField(); + + expect(getByText(TEST_LABEL)).toBeDefined(); + }); + + describe('handleCopy', () => { + it('calls copyToClipboard with correct address and message when pressed', async () => { + const { getByTestId } = renderAddressField(); + + // Find and press the copy button + const copyButton = getByTestId('address-field-copy-button'); + fireEvent.press(copyButton); + + await new Promise(process.nextTick); + + expect(mockCopyToClipboard).toHaveBeenCalledWith( + TEST_ADDRESS, + 'Address copied to clipboard', + ); + }); + + it('tracks "ADDRESS_COPIED" event when copy button is pressed', async () => { + const { getByTestId } = renderAddressField(); + + // Find and press the copy button + const copyButton = getByTestId('address-field-copy-button'); + fireEvent.press(copyButton); + + await new Promise(process.nextTick); + + // Verify createEventBuilder was called with correct event name + expect(mockCreateEventBuilder).toHaveBeenCalledWith( + EVENT_NAME.ADDRESS_COPIED, + ); + + // Verify trackEvent was called + expect(mockTrackEvent).toHaveBeenCalled(); + }); + + it('adds correct properties with location "notification-details"', async () => { + const { getByTestId } = renderAddressField(); + + // Find and press the copy button + const copyButton = getByTestId('address-field-copy-button'); + fireEvent.press(copyButton); + + await new Promise(process.nextTick); + + // Verify addProperties was called with correct properties + const mockEventBuilder = mockCreateEventBuilder.mock.results[0].value; + expect(mockEventBuilder.addProperties).toHaveBeenCalledWith({ + location: 'notification-details', + }); + + // Verify build was called + expect(mockEventBuilder.addProperties().build).toHaveBeenCalled(); + }); + + it('calls CopyClipboardAlertMessage.address() to get message', async () => { + const { getByTestId } = renderAddressField(); + + // Find and press the copy button + const copyButton = getByTestId('address-field-copy-button'); + fireEvent.press(copyButton); + + await new Promise(process.nextTick); + + expect(CopyClipboardAlertMessage.address).toHaveBeenCalled(); + }); + }); +}); diff --git a/app/components/Views/Notifications/Details/Fields/AddressField.tsx b/app/components/Views/Notifications/Details/Fields/AddressField.tsx index 27b23f1e250..7bc145dc88f 100644 --- a/app/components/Views/Notifications/Details/Fields/AddressField.tsx +++ b/app/components/Views/Notifications/Details/Fields/AddressField.tsx @@ -19,6 +19,8 @@ import useCopyClipboard, { } from '../hooks/useCopyClipboard'; import useStyles from '../useStyles'; import { selectAvatarAccountType } from '../../../../../selectors/settings'; +import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics'; +import { EVENT_NAME } from '../../../../../core/Analytics/MetaMetrics.events'; type AddressFieldProps = ModalFieldAddress; @@ -26,9 +28,22 @@ function AddressField(props: AddressFieldProps) { const { label, address } = props; const { styles } = useStyles(); const copyToClipboard = useCopyClipboard(); + const { trackEvent, createEventBuilder } = useAnalytics(); const accountAvatarType = useSelector(selectAvatarAccountType); + const handleCopy = () => { + copyToClipboard(address, CopyClipboardAlertMessage.address()); + + trackEvent( + createEventBuilder(EVENT_NAME.ADDRESS_COPIED) + .addProperties({ + location: 'notification-details', + }) + .build(), + ); + }; + return ( {label} - copyToClipboard(address, CopyClipboardAlertMessage.address()) - } + onPress={handleCopy} hitSlop={{ top: 24, bottom: 24, left: 24, right: 24 }} style={styles.copyContainer} + testID="address-field-copy-button" > ({ + useAnalytics: () => ({ + trackEvent: mockTrackEvent, + createEventBuilder: mockCreateEventBuilder, + }), +})); const initialState = { engine: { @@ -40,12 +55,16 @@ const TestWrapper = ({ labelProps, description, descriptionProps, + analyticsLocation, + chainId, }: { accountAddress: string; label?: string | React.ReactNode; labelProps?: Partial; description?: string | React.ReactNode; descriptionProps?: Partial; + analyticsLocation?: string; + chainId?: string; }) => ( ); @@ -256,4 +277,122 @@ describe('QRAccountDisplay', () => { expect(startElements.length).toBeGreaterThan(0); expect(endElements.length).toBeGreaterThan(0); }); + + describe('Analytics tracking', () => { + it('tracks copy event when analyticsLocation is provided', async () => { + // Arrange + const analyticsLocation = 'test_screen'; + + const { getByTestId } = renderScreen( + () => ( + + ), + { name: 'QRAccountDisplay' }, + // @ts-expect-error initialBackgroundState throws error + { state: initialState }, + ); + + // Act + const copyButton = getByTestId('qr-account-display-copy-button'); + await fireEvent.press(copyButton); + + // Assert + expect(mockCreateEventBuilder).toHaveBeenCalledWith( + EVENT_NAME.ADDRESS_COPIED, + ); + expect(mockAddProperties).toHaveBeenCalledWith({ + location: analyticsLocation, + }); + expect(mockBuild).toHaveBeenCalled(); + expect(mockTrackEvent).toHaveBeenCalledWith({}); + }); + + it('does not track copy event when analyticsLocation is not provided', () => { + // Arrange + const { getByTestId } = renderScreen( + () => , + { name: 'QRAccountDisplay' }, + // @ts-expect-error initialBackgroundState throws error + { state: initialState }, + ); + + // Act + const copyButton = getByTestId('qr-account-display-copy-button'); + fireEvent.press(copyButton); + + // Assert + expect(mockCreateEventBuilder).not.toHaveBeenCalled(); + expect(mockAddProperties).not.toHaveBeenCalled(); + expect(mockBuild).not.toHaveBeenCalled(); + expect(mockTrackEvent).not.toHaveBeenCalled(); + }); + + it('includes chain_id in analytics when both analyticsLocation and chainId are provided', async () => { + // Arrange + const analyticsLocation = 'test_screen'; + const chainId = 'eip155:1'; // chainId should be in CAIP format + + const { getByTestId } = renderScreen( + () => ( + + ), + { name: 'QRAccountDisplay' }, + // @ts-expect-error initialBackgroundState throws error + { state: initialState }, + ); + + // Act + const copyButton = getByTestId('qr-account-display-copy-button'); + await fireEvent.press(copyButton); + + // Assert + expect(mockCreateEventBuilder).toHaveBeenCalledWith( + EVENT_NAME.ADDRESS_COPIED, + ); + expect(mockAddProperties).toHaveBeenCalledWith({ + location: analyticsLocation, + chain_id_caip: 'eip155:1', + }); + expect(mockBuild).toHaveBeenCalled(); + expect(mockTrackEvent).toHaveBeenCalledWith({}); + }); + + it('does not include chain_id in analytics when chainId is not provided', async () => { + // Arrange + const analyticsLocation = 'test_screen'; + + const { getByTestId } = renderScreen( + () => ( + + ), + { name: 'QRAccountDisplay' }, + // @ts-expect-error initialBackgroundState throws error + { state: initialState }, + ); + + // Act + const copyButton = getByTestId('qr-account-display-copy-button'); + await fireEvent.press(copyButton); + + // Assert + expect(mockCreateEventBuilder).toHaveBeenCalledWith( + EVENT_NAME.ADDRESS_COPIED, + ); + expect(mockAddProperties).toHaveBeenCalledWith({ + location: analyticsLocation, + }); + expect(mockBuild).toHaveBeenCalled(); + expect(mockTrackEvent).toHaveBeenCalledWith({}); + }); + }); }); diff --git a/app/components/Views/QRAccountDisplay/QRAccountDisplay.tsx b/app/components/Views/QRAccountDisplay/QRAccountDisplay.tsx index 1dac42d364d..c4ce33ca408 100644 --- a/app/components/Views/QRAccountDisplay/QRAccountDisplay.tsx +++ b/app/components/Views/QRAccountDisplay/QRAccountDisplay.tsx @@ -22,18 +22,28 @@ import { import { selectInternalAccounts } from '../../../selectors/accountsController'; import { renderAccountName } from '../../../util/address'; import { QRAccountDisplayProps } from './QRAccountDisplay.types'; +import { useAnalytics } from '../../hooks/useAnalytics/useAnalytics'; +import { EVENT_NAME } from '../../../core/Analytics/MetaMetrics.events'; const ADDRESS_PREFIX_LENGTH = 6; const ADDRESS_SUFFIX_LENGTH = 5; const QRAccountDisplay = (props: QRAccountDisplayProps) => { - const { accountAddress, label, labelProps, description, descriptionProps } = - props; + const { + accountAddress, + label, + labelProps, + description, + descriptionProps, + analyticsLocation, + chainId, + } = props; const tw = useTailwind(); const addr = accountAddress; const accounts = useSelector(selectInternalAccounts); const accountLabel = renderAccountName(addr, accounts); const { toastRef } = useContext(ToastContext); + const { trackEvent, createEventBuilder } = useAnalytics(); const addressStart = addr.substring(0, ADDRESS_PREFIX_LENGTH); const addressMiddle: string = addr.substring( ADDRESS_PREFIX_LENGTH, @@ -59,6 +69,18 @@ const QRAccountDisplay = (props: QRAccountDisplayProps) => { const handleCopyButton = async () => { showCopyNotificationToast(); await ClipboardManager.setString(accountAddress); + + // Track copy event if analytics context provided + if (analyticsLocation) { + trackEvent( + createEventBuilder(EVENT_NAME.ADDRESS_COPIED) + .addProperties({ + location: analyticsLocation, + ...(chainId && { chain_id_caip: chainId }), + }) + .build(), + ); + } }; const renderLabel = () => { diff --git a/app/components/Views/QRAccountDisplay/QRAccountDisplay.types.ts b/app/components/Views/QRAccountDisplay/QRAccountDisplay.types.ts index 69be6841b57..475b6e6d2e5 100644 --- a/app/components/Views/QRAccountDisplay/QRAccountDisplay.types.ts +++ b/app/components/Views/QRAccountDisplay/QRAccountDisplay.types.ts @@ -24,4 +24,14 @@ export interface QRAccountDisplayProps { * Optional props to pass to the Text component when description is a string */ descriptionProps?: Partial; + /** + * Optional location identifier for analytics tracking. + * When provided, tracks "Copied Address" event with this location. + */ + analyticsLocation?: string; + /** + * Optional chain ID for analytics tracking. + * Should be in CAIP format (e.g., 'eip155:1' for Ethereum mainnet). + */ + chainId?: string; } diff --git a/app/components/Views/Settings/RegionSelector/RegionSelector.test.tsx b/app/components/Views/Settings/RegionSelector/RegionSelector.test.tsx index b7f11463ea9..f7bf8cd3196 100644 --- a/app/components/Views/Settings/RegionSelector/RegionSelector.test.tsx +++ b/app/components/Views/Settings/RegionSelector/RegionSelector.test.tsx @@ -24,7 +24,6 @@ jest.mock('@react-navigation/native', () => { }); const mockSetUserRegion = jest.fn().mockResolvedValue(undefined); -const mockFetchUserRegion = jest.fn().mockResolvedValue(null); const mockSetSelectedProvider = jest.fn(); const createMockCountry = ( @@ -94,10 +93,7 @@ const mockUseRampsControllerInitialValues: ReturnType< typeof useRampsController > = { userRegion: null, - userRegionLoading: false, - userRegionError: null, setUserRegion: mockSetUserRegion, - fetchUserRegion: mockFetchUserRegion, selectedProvider: null, setSelectedProvider: mockSetSelectedProvider, providers: [], diff --git a/app/components/Views/TrendingView/TrendingView.tsx b/app/components/Views/TrendingView/TrendingView.tsx index 03677fa121e..934d4fc46b9 100644 --- a/app/components/Views/TrendingView/TrendingView.tsx +++ b/app/components/Views/TrendingView/TrendingView.tsx @@ -25,7 +25,7 @@ import Routes from '../../../constants/navigation/Routes'; import ExploreSearchBar from './components/ExploreSearchBar/ExploreSearchBar'; import QuickActions from './components/QuickActions/QuickActions'; import SectionHeader from './components/SectionHeader/SectionHeader'; -import { HOME_SECTIONS_ARRAY, SectionId } from './sections.config'; +import { useHomeSections, SectionId } from './sections.config'; import { selectBasicFunctionalityEnabled } from '../../../selectors/settings'; import BasicFunctionalityEmptyState from '../../UI/BasicFunctionality/BasicFunctionalityEmptyState/BasicFunctionalityEmptyState'; import TrendingFeedSessionManager from '../../UI/Trending/services/TrendingFeedSessionManager'; @@ -36,7 +36,12 @@ import { TrendingViewSelectorsIDs } from './TrendingView.testIds'; * Custom hook to track boolean state for each section * Returns the Set of sections with that state and callbacks to update them */ -const useSectionStateTracker = () => { +const useSectionStateTracker = ( + sections: { id: SectionId }[], +): { + sectionsWithState: Set; + callbacks: Record void>; +} => { const [activeSections, setActiveSections] = useState>( new Set(), ); @@ -44,7 +49,7 @@ const useSectionStateTracker = () => { const callbacks = useMemo(() => { const result = {} as Record void>; - HOME_SECTIONS_ARRAY.forEach((section) => { + sections.forEach((section) => { result[section.id] = (isActive: boolean) => { setActiveSections((currentSections) => { const updatedSections = new Set(currentSections); @@ -61,7 +66,7 @@ const useSectionStateTracker = () => { }); return result; - }, []); + }, [sections]); return { sectionsWithState: activeSections, callbacks }; }; @@ -78,14 +83,16 @@ export const ExploreFeed: React.FC = () => { silentRefresh: true, }); + const homeSections = useHomeSections(); + // Track which sections have empty data and which are loading const { sectionsWithState: emptySections, callbacks: emptyStateCallbacks } = - useSectionStateTracker(); + useSectionStateTracker(homeSections); const { sectionsWithState: loadingSections, callbacks: loadingStateCallbacks, - } = useSectionStateTracker(); + } = useSectionStateTracker(homeSections); const sessionManager = TrendingFeedSessionManager.getInstance(); @@ -233,7 +240,7 @@ export const ExploreFeed: React.FC = () => { > - {HOME_SECTIONS_ARRAY.map((section) => { + {homeSections.map((section) => { // Hide section visually but keep mounted so it can report when data arrives const isHidden = emptySections.has(section.id); diff --git a/app/components/Views/TrendingView/components/QuickActions/QuickActions.tsx b/app/components/Views/TrendingView/components/QuickActions/QuickActions.tsx index 1d3fba1d54d..f1b5630fcef 100644 --- a/app/components/Views/TrendingView/components/QuickActions/QuickActions.tsx +++ b/app/components/Views/TrendingView/components/QuickActions/QuickActions.tsx @@ -10,7 +10,7 @@ import { TextVariant, } from '@metamask/design-system-react-native'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; -import { SECTIONS_ARRAY, SectionId } from '../../sections.config'; +import { useSectionsArray, SectionId } from '../../sections.config'; import { TrendingViewSelectorsIDs } from '../../TrendingView.testIds'; interface QuickActionsProps { @@ -26,10 +26,9 @@ interface QuickActionsProps { const QuickActions: React.FC = ({ emptySections }) => { const navigation = useNavigation(); const tw = useTailwind(); + const sectionsArray = useSectionsArray(); - const visibleSections = SECTIONS_ARRAY.filter( - (s) => !emptySections.has(s.id), - ); + const visibleSections = sectionsArray.filter((s) => !emptySections.has(s.id)); return ( diff --git a/app/components/Views/TrendingView/hooks/useExploreSearch.test.ts b/app/components/Views/TrendingView/hooks/useExploreSearch.test.ts index a242c28774c..a4fe12bffb6 100644 --- a/app/components/Views/TrendingView/hooks/useExploreSearch.test.ts +++ b/app/components/Views/TrendingView/hooks/useExploreSearch.test.ts @@ -1,6 +1,6 @@ import { renderHook, waitFor, act } from '@testing-library/react-native'; import { useExploreSearch } from './useExploreSearch'; -import { SECTIONS_ARRAY } from '../sections.config'; +import type { SectionId } from '../sections.config'; const mockTrendingTokens = [ { assetId: '1', symbol: 'BTC', name: 'Bitcoin' }, @@ -91,6 +91,22 @@ jest.mock('../../../UI/Sites/hooks/useSiteData/useSitesData', () => ({ }), })); +// Mock useSectionsArray to return all sections for testing +const mockSectionsArray: { id: SectionId }[] = [ + { id: 'tokens' }, + { id: 'perps' }, + { id: 'predictions' }, + { id: 'sites' }, +]; + +jest.mock('../sections.config', () => { + const actual = jest.requireActual('../sections.config'); + return { + ...actual, + useSectionsArray: () => mockSectionsArray, + }; +}); + describe('useExploreSearch', () => { beforeEach(() => { jest.useFakeTimers(); @@ -217,7 +233,7 @@ describe('useExploreSearch', () => { it('processes all sections defined in config', () => { const { result } = renderHook(() => useExploreSearch('')); - SECTIONS_ARRAY.forEach((section) => { + mockSectionsArray.forEach((section) => { expect(result.current.data[section.id]).toBeDefined(); expect(result.current.isLoading[section.id]).toBeDefined(); }); @@ -227,7 +243,7 @@ describe('useExploreSearch', () => { const { result } = renderHook(() => useExploreSearch('')); expect(result.current.sectionsOrder).toEqual( - SECTIONS_ARRAY.map((s) => s.id), + mockSectionsArray.map((s) => s.id), ); }); diff --git a/app/components/Views/TrendingView/hooks/useExploreSearch.ts b/app/components/Views/TrendingView/hooks/useExploreSearch.ts index df766dc5fc9..12ff08423b0 100644 --- a/app/components/Views/TrendingView/hooks/useExploreSearch.ts +++ b/app/components/Views/TrendingView/hooks/useExploreSearch.ts @@ -1,6 +1,6 @@ import { useState, useEffect, useMemo } from 'react'; import { - SECTIONS_ARRAY, + useSectionsArray, useSectionsData, type SectionId, } from '../sections.config'; @@ -37,9 +37,10 @@ export const useExploreSearch = ( query: string, options?: ExploreSearchOptions, ): ExploreSearchResult => { + const sectionsArray = useSectionsArray(); const sectionsOrder = useMemo( - () => options?.sectionsOrder ?? SECTIONS_ARRAY.map((s) => s.id), - [options?.sectionsOrder], + () => options?.sectionsOrder ?? sectionsArray.map((s) => s.id), + [options?.sectionsOrder, sectionsArray], ); const [debouncedQuery, setDebouncedQuery] = useState(query); @@ -70,7 +71,7 @@ export const useExploreSearch = ( const shouldShowTopItems = !debouncedQuery.trim(); // Process each section generically - SECTIONS_ARRAY.forEach((section) => { + sectionsArray.forEach((section) => { const sectionData = allSectionsData[section.id]; // If we're debouncing, show loading state immediately // Otherwise, use the actual loading state from the data fetch @@ -86,7 +87,13 @@ export const useExploreSearch = ( }); return { data, isLoading, sectionsOrder }; - }, [debouncedQuery, allSectionsData, isDebouncing, sectionsOrder]); + }, [ + debouncedQuery, + allSectionsData, + isDebouncing, + sectionsOrder, + sectionsArray, + ]); return filteredResults; }; diff --git a/app/components/Views/TrendingView/sections.config.tsx b/app/components/Views/TrendingView/sections.config.tsx index 475093e1a40..4e1f65aa02f 100644 --- a/app/components/Views/TrendingView/sections.config.tsx +++ b/app/components/Views/TrendingView/sections.config.tsx @@ -2,6 +2,7 @@ import React, { PropsWithChildren, useMemo } from 'react'; import Fuse, { type FuseOptions } from 'fuse.js'; import type { NavigationProp, ParamListBase } from '@react-navigation/native'; import type { TrendingAsset } from '@metamask/assets-controllers'; +import { useSelector } from 'react-redux'; import Routes from '../../../constants/navigation/Routes'; import { strings } from '../../../../locales/i18n'; import TrendingTokenRowItem from '../../UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem'; @@ -13,6 +14,7 @@ import type { PredictMarket as PredictMarketType } from '../../UI/Predict/types' import type { PerpsNavigationParamList } from '../../UI/Perps/types/navigation'; import PredictMarketSkeleton from '../../UI/Predict/components/PredictMarketSkeleton'; import { usePredictMarketData } from '../../UI/Predict/hooks/usePredictMarketData'; +import { selectPerpsEnabledFlag } from '../../UI/Perps'; import { usePerpsMarkets } from '../../UI/Perps/hooks'; import { PerpsConnectionProvider } from '../../UI/Perps/providers/PerpsConnectionProvider'; import { PerpsStreamProvider } from '../../UI/Perps/providers/PerpsStreamManager'; @@ -310,7 +312,7 @@ export const SECTIONS_CONFIG: Record = { }; // Sorted by order on the main screen -export const HOME_SECTIONS_ARRAY: (SectionConfig & { id: SectionId })[] = [ +const HOME_SECTIONS_ARRAY: (SectionConfig & { id: SectionId })[] = [ SECTIONS_CONFIG.predictions, SECTIONS_CONFIG.tokens, SECTIONS_CONFIG.perps, @@ -318,13 +320,37 @@ export const HOME_SECTIONS_ARRAY: (SectionConfig & { id: SectionId })[] = [ ]; // Sorted by order on the QuickAction buttons and SearchResults -export const SECTIONS_ARRAY: (SectionConfig & { id: SectionId })[] = [ +const SECTIONS_ARRAY: (SectionConfig & { id: SectionId })[] = [ SECTIONS_CONFIG.tokens, SECTIONS_CONFIG.perps, SECTIONS_CONFIG.predictions, SECTIONS_CONFIG.sites, ]; +export const useHomeSections = (): (SectionConfig & { id: SectionId })[] => { + const isPerpsEnabled = useSelector(selectPerpsEnabledFlag); + + return useMemo( + () => + isPerpsEnabled + ? HOME_SECTIONS_ARRAY + : HOME_SECTIONS_ARRAY.filter((section) => section.id !== 'perps'), + [isPerpsEnabled], + ); +}; + +export const useSectionsArray = (): (SectionConfig & { id: SectionId })[] => { + const isPerpsEnabled = useSelector(selectPerpsEnabledFlag); + + return useMemo( + () => + isPerpsEnabled + ? SECTIONS_ARRAY + : SECTIONS_ARRAY.filter((section) => section.id !== 'perps'), + [isPerpsEnabled], + ); +}; + /** * Centralized hook that fetches data for all sections. * When adding a new section, add its hook call here. diff --git a/app/components/Views/WalletActions/WalletActions.tsx b/app/components/Views/WalletActions/WalletActions.tsx index 3b8336257a0..c3b880bf137 100644 --- a/app/components/Views/WalletActions/WalletActions.tsx +++ b/app/components/Views/WalletActions/WalletActions.tsx @@ -29,7 +29,7 @@ import { selectStablecoinLendingEnabledFlag, } from '../../UI/Earn/selectors/featureFlags'; import { selectPerpsEnabledFlag } from '../../UI/Perps'; -import { PerpsEventValues } from '../../UI/Perps/constants/eventNames'; +import { PERPS_EVENT_VALUE } from '../../UI/Perps/constants/eventNames'; import { selectPredictEnabledFlag } from '../../UI/Predict/selectors/featureFlags'; import { PredictEventValues } from '../../UI/Predict/constants/eventNames'; import { EARN_INPUT_VIEW_ACTIONS } from '../../UI/Earn/Views/EarnInputView/EarnInputView.types'; @@ -121,7 +121,7 @@ const WalletActions = () => { } else { navigate(Routes.PERPS.ROOT, { screen: Routes.PERPS.PERPS_HOME, - params: { source: PerpsEventValues.SOURCE.MAIN_ACTION_BUTTON }, + params: { source: PERPS_EVENT_VALUE.SOURCE.MAIN_ACTION_BUTTON }, }); } }); diff --git a/app/components/Views/confirmations/constants/alerts.ts b/app/components/Views/confirmations/constants/alerts.ts index f5258fef935..900ade78d64 100644 --- a/app/components/Views/confirmations/constants/alerts.ts +++ b/app/components/Views/confirmations/constants/alerts.ts @@ -6,6 +6,7 @@ export enum AlertKeys { BurnAddress = 'burn_address', DomainMismatch = 'domain_mismatch', GasEstimateFailed = 'gas_estimate_failed', + GasSponsorshipReserveBalance = 'gas_sponsorship_reserve_balance', InsufficientBalance = 'insufficient_balance', InsufficientPayTokenBalance = 'insufficient_pay_token_balance', InsufficientPayTokenNative = 'insufficient_pay_token_native', diff --git a/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.test.ts b/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.test.ts index 93a59885ea7..ecd78662689 100644 --- a/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.test.ts +++ b/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.test.ts @@ -21,9 +21,11 @@ import { useTokenTrustSignalAlerts } from './useTokenTrustSignalAlerts'; import { useAddressTrustSignalAlerts } from './useAddressTrustSignalAlerts'; import { useOriginTrustSignalAlerts } from './useOriginTrustSignalAlerts'; import { useGasEstimateFailedAlert } from './useGasEstimateFailedAlert'; +import { useGasSponsorshipWarningAlert } from './useGasSponsorshipWarningAlert'; jest.mock('./useBlockaidAlerts'); jest.mock('./useGasEstimateFailedAlert'); +jest.mock('./useGasSponsorshipWarningAlert'); jest.mock('./useDomainMismatchAlerts'); jest.mock('./useInsufficientBalanceAlert'); jest.mock('./useAccountTypeUpgrade'); @@ -171,6 +173,7 @@ describe('useConfirmationAlerts', () => { (useBlockaidAlerts as jest.Mock).mockReturnValue([]); (useDomainMismatchAlerts as jest.Mock).mockReturnValue([]); (useGasEstimateFailedAlert as jest.Mock).mockReturnValue([]); + (useGasSponsorshipWarningAlert as jest.Mock).mockReturnValue([]); (useInsufficientBalanceAlert as jest.Mock).mockReturnValue([]); (useAccountTypeUpgrade as jest.Mock).mockReturnValue([]); (useSignedOrSubmittedAlert as jest.Mock).mockReturnValue([]); diff --git a/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.ts b/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.ts index c725e8e0d7c..bb14c033e1d 100644 --- a/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.ts +++ b/app/components/Views/confirmations/hooks/alerts/useConfirmationAlerts.ts @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import useBlockaidAlerts from './useBlockaidAlerts'; import useDomainMismatchAlerts from './useDomainMismatchAlerts'; import { useGasEstimateFailedAlert } from './useGasEstimateFailedAlert'; +import { useGasSponsorshipWarningAlert } from './useGasSponsorshipWarningAlert'; import { useInsufficientBalanceAlert } from './useInsufficientBalanceAlert'; import { useAccountTypeUpgrade } from './useAccountTypeUpgrade'; import { useSignedOrSubmittedAlert } from './useSignedOrSubmittedAlert'; @@ -24,6 +25,7 @@ function useSignatureAlerts(): Alert[] { function useTransactionAlerts(): Alert[] { const gasEstimateFailedAlert = useGasEstimateFailedAlert(); + const gasSponsorshipWarningAlert = useGasSponsorshipWarningAlert(); const insufficientBalanceAlert = useInsufficientBalanceAlert(); const signedOrSubmittedAlert = useSignedOrSubmittedAlert(); const pendingTransactionAlert = usePendingTransactionAlert(); @@ -38,6 +40,7 @@ function useTransactionAlerts(): Alert[] { return useMemo( () => [ ...gasEstimateFailedAlert, + ...gasSponsorshipWarningAlert, ...insufficientBalanceAlert, ...batchedUnusedApprovalsAlert, ...pendingTransactionAlert, @@ -50,6 +53,7 @@ function useTransactionAlerts(): Alert[] { ], [ gasEstimateFailedAlert, + gasSponsorshipWarningAlert, insufficientBalanceAlert, batchedUnusedApprovalsAlert, pendingTransactionAlert, diff --git a/app/components/Views/confirmations/hooks/alerts/useGasSponsorshipWarningAlert.test.ts b/app/components/Views/confirmations/hooks/alerts/useGasSponsorshipWarningAlert.test.ts new file mode 100644 index 00000000000..c31a5fb8832 --- /dev/null +++ b/app/components/Views/confirmations/hooks/alerts/useGasSponsorshipWarningAlert.test.ts @@ -0,0 +1,296 @@ +import { + TransactionStatus, + TransactionType, + TransactionMeta, + SimulationData, +} from '@metamask/transaction-controller'; +import type { Hex } from '@metamask/utils'; +import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider'; +import { useGasSponsorshipWarningAlert } from './useGasSponsorshipWarningAlert'; +import { useTransactionMetadataRequest } from '../transactions/useTransactionMetadataRequest'; +import { useIsGaslessSupported } from '../gas/useIsGaslessSupported'; +import { AlertKeys } from '../../constants/alerts'; +import { RowAlertKey } from '../../components/UI/info-row/alert-row/constants'; +import { Severity } from '../../types/alerts'; +import { NETWORKS_CHAIN_ID } from '../../../../../constants/network'; + +jest.mock('../transactions/useTransactionMetadataRequest'); +jest.mock('../gas/useIsGaslessSupported'); + +const MONAD_CHAIN_ID = NETWORKS_CHAIN_ID.MONAD as Hex; +const MAINNET_CHAIN_ID = '0x1' as Hex; + +/** + * Helper type for simulation data with callTraceErrors + */ +type SimulationDataWithCallTraceErrors = SimulationData & { + callTraceErrors?: string[]; +}; + +const createMockSimulationData = ( + callTraceErrors?: string[], +): SimulationDataWithCallTraceErrors => + ({ + tokenBalanceChanges: [], + callTraceErrors, + }) as unknown as SimulationDataWithCallTraceErrors; + +const createMockTransactionMeta = ( + overrides: Partial = {}, +): TransactionMeta => + ({ + id: '1', + status: TransactionStatus.unapproved, + type: TransactionType.contractInteraction, + chainId: MONAD_CHAIN_ID, + isGasFeeSponsored: false, + simulationData: undefined, + ...overrides, + }) as unknown as TransactionMeta; + +describe('useGasSponsorshipWarningAlert', () => { + const mockUseTransactionMetadataRequest = jest.mocked( + useTransactionMetadataRequest, + ); + const mockUseIsGaslessSupported = jest.mocked(useIsGaslessSupported); + + beforeEach(() => { + jest.clearAllMocks(); + mockUseIsGaslessSupported.mockReturnValue({ + isSupported: true, + isSmartTransaction: false, + pending: false, + }); + }); + + describe('returns warning alert', () => { + it('when callTraceErrors contains reserve balance violation on Monad', () => { + const transactionMeta = createMockTransactionMeta({ + chainId: MONAD_CHAIN_ID, + isGasFeeSponsored: false, + simulationData: createMockSimulationData([ + 'Reserve balance violation detected', + ]), + }); + mockUseTransactionMetadataRequest.mockReturnValue(transactionMeta); + + const { result } = renderHookWithProvider(() => + useGasSponsorshipWarningAlert(), + ); + + expect(result.current).toHaveLength(1); + expect(result.current[0]).toMatchObject({ + isBlocking: false, + key: AlertKeys.GasSponsorshipReserveBalance, + field: RowAlertKey.EstimatedFee, + severity: Severity.Warning, + title: 'Gas sponsorship unavailable', + }); + }); + + it('with correct message containing minBalance and nativeTokenSymbol', () => { + const transactionMeta = createMockTransactionMeta({ + chainId: MONAD_CHAIN_ID, + isGasFeeSponsored: false, + simulationData: createMockSimulationData(['reserve balance violation']), + }); + mockUseTransactionMetadataRequest.mockReturnValue(transactionMeta); + + const { result } = renderHookWithProvider(() => + useGasSponsorshipWarningAlert(), + ); + + expect(result.current[0].message).toContain('10'); + expect(result.current[0].message).toContain('MON'); + }); + + it('when error message contains matcher in mixed case', () => { + const transactionMeta = createMockTransactionMeta({ + chainId: MONAD_CHAIN_ID, + isGasFeeSponsored: false, + simulationData: createMockSimulationData(['RESERVE BALANCE VIOLATION']), + }); + mockUseTransactionMetadataRequest.mockReturnValue(transactionMeta); + + const { result } = renderHookWithProvider(() => + useGasSponsorshipWarningAlert(), + ); + + expect(result.current).toHaveLength(1); + }); + }); + + describe('returns empty array', () => { + it('when transaction metadata is undefined', () => { + mockUseTransactionMetadataRequest.mockReturnValue(undefined); + + const { result } = renderHookWithProvider(() => + useGasSponsorshipWarningAlert(), + ); + + expect(result.current).toEqual([]); + }); + + it('when simulationData is undefined', () => { + const transactionMeta = createMockTransactionMeta({ + chainId: MONAD_CHAIN_ID, + simulationData: undefined, + }); + mockUseTransactionMetadataRequest.mockReturnValue(transactionMeta); + + const { result } = renderHookWithProvider(() => + useGasSponsorshipWarningAlert(), + ); + + expect(result.current).toEqual([]); + }); + + it('when callTraceErrors is undefined', () => { + const transactionMeta = createMockTransactionMeta({ + chainId: MONAD_CHAIN_ID, + simulationData: createMockSimulationData(undefined), + }); + mockUseTransactionMetadataRequest.mockReturnValue(transactionMeta); + + const { result } = renderHookWithProvider(() => + useGasSponsorshipWarningAlert(), + ); + + expect(result.current).toEqual([]); + }); + + it('when callTraceErrors is empty', () => { + const transactionMeta = createMockTransactionMeta({ + chainId: MONAD_CHAIN_ID, + simulationData: createMockSimulationData([]), + }); + mockUseTransactionMetadataRequest.mockReturnValue(transactionMeta); + + const { result } = renderHookWithProvider(() => + useGasSponsorshipWarningAlert(), + ); + + expect(result.current).toEqual([]); + }); + + it('when chain has no sponsorship warning rules configured', () => { + const transactionMeta = createMockTransactionMeta({ + chainId: MAINNET_CHAIN_ID, // Mainnet - no rules configured + isGasFeeSponsored: false, + simulationData: createMockSimulationData(['reserve balance violation']), + }); + mockUseTransactionMetadataRequest.mockReturnValue(transactionMeta); + + const { result } = renderHookWithProvider(() => + useGasSponsorshipWarningAlert(), + ); + + expect(result.current).toEqual([]); + }); + + it('when callTraceErrors do not match any configured matchers', () => { + const transactionMeta = createMockTransactionMeta({ + chainId: MONAD_CHAIN_ID, + isGasFeeSponsored: false, + simulationData: createMockSimulationData([ + 'some other error', + 'another unrelated error', + ]), + }); + mockUseTransactionMetadataRequest.mockReturnValue(transactionMeta); + + const { result } = renderHookWithProvider(() => + useGasSponsorshipWarningAlert(), + ); + + expect(result.current).toEqual([]); + }); + + it('when isGasFeeSponsored is true', () => { + const transactionMeta = createMockTransactionMeta({ + chainId: MONAD_CHAIN_ID, + isGasFeeSponsored: true, + simulationData: createMockSimulationData(['reserve balance violation']), + }); + mockUseTransactionMetadataRequest.mockReturnValue(transactionMeta); + + const { result } = renderHookWithProvider(() => + useGasSponsorshipWarningAlert(), + ); + + expect(result.current).toEqual([]); + }); + + it('when gasless is not supported on the network', () => { + mockUseIsGaslessSupported.mockReturnValue({ + isSupported: false, + isSmartTransaction: false, + pending: false, + }); + const transactionMeta = createMockTransactionMeta({ + chainId: MONAD_CHAIN_ID, + isGasFeeSponsored: false, + simulationData: createMockSimulationData(['reserve balance violation']), + }); + mockUseTransactionMetadataRequest.mockReturnValue(transactionMeta); + + const { result } = renderHookWithProvider(() => + useGasSponsorshipWarningAlert(), + ); + + expect(result.current).toEqual([]); + }); + + it('when chainId is missing from transaction metadata', () => { + const transactionMeta = createMockTransactionMeta({ + chainId: undefined as unknown as Hex, + simulationData: createMockSimulationData(['reserve balance violation']), + }); + mockUseTransactionMetadataRequest.mockReturnValue(transactionMeta); + + const { result } = renderHookWithProvider(() => + useGasSponsorshipWarningAlert(), + ); + + expect(result.current).toEqual([]); + }); + }); + + describe('error matching behavior', () => { + it('matches partial error messages containing the matcher', () => { + const transactionMeta = createMockTransactionMeta({ + chainId: MONAD_CHAIN_ID, + isGasFeeSponsored: false, + simulationData: createMockSimulationData([ + 'Transaction failed: reserve balance violation - minimum 10 MON required', + ]), + }); + mockUseTransactionMetadataRequest.mockReturnValue(transactionMeta); + + const { result } = renderHookWithProvider(() => + useGasSponsorshipWarningAlert(), + ); + + expect(result.current).toHaveLength(1); + }); + + it('matches when any error in array contains the matcher', () => { + const transactionMeta = createMockTransactionMeta({ + chainId: MONAD_CHAIN_ID, + isGasFeeSponsored: false, + simulationData: createMockSimulationData([ + 'some unrelated error', + 'reserve balance violation', + 'another error', + ]), + }); + mockUseTransactionMetadataRequest.mockReturnValue(transactionMeta); + + const { result } = renderHookWithProvider(() => + useGasSponsorshipWarningAlert(), + ); + + expect(result.current).toHaveLength(1); + }); + }); +}); diff --git a/app/components/Views/confirmations/hooks/alerts/useGasSponsorshipWarningAlert.ts b/app/components/Views/confirmations/hooks/alerts/useGasSponsorshipWarningAlert.ts new file mode 100644 index 00000000000..7f52dbb16ef --- /dev/null +++ b/app/components/Views/confirmations/hooks/alerts/useGasSponsorshipWarningAlert.ts @@ -0,0 +1,138 @@ +import { useMemo } from 'react'; +import type { Hex } from '@metamask/utils'; +import { SimulationData } from '@metamask/transaction-controller'; + +import { strings } from '../../../../../../locales/i18n'; +import { RowAlertKey } from '../../components/UI/info-row/alert-row/constants'; +import { AlertKeys } from '../../constants/alerts'; +import { Alert, Severity } from '../../types/alerts'; +import { useTransactionMetadataRequest } from '../transactions/useTransactionMetadataRequest'; +import { useIsGaslessSupported } from '../gas/useIsGaslessSupported'; +import { NETWORKS_CHAIN_ID } from '../../../../../constants/network'; + +/** + * Configuration for gas sponsorship warning rules per chain. + * Each rule defines matchers for error detection and the warning message parameters. + */ +interface SponsorshipWarningRule { + /** The localization message key for the warning */ + messageKey: string; + /** The minimum balance required for sponsorship */ + minBalance: string; + /** The native token symbol for this chain (e.g., 'MON' for Monad) */ + nativeCurrency: string; + /** Array of error message patterns to match (case-insensitive) */ + matchers: string[]; +} + +/** + * Extended SimulationData type that includes callTraceErrors. + * This field is available in transaction-controller >= 62.10.0 + */ +type SimulationDataWithCallTraceErrors = SimulationData & { + callTraceErrors?: string[]; +}; + +/** + * Rules for displaying gas sponsorship warnings based on chain-specific requirements. + * Currently configured for Monad which requires a 10 MON minimum reserve balance. + */ +const GAS_SPONSORSHIP_WARNING_RULES: Partial< + Record +> = { + [NETWORKS_CHAIN_ID.MONAD as Hex]: { + messageKey: 'alert_system.gas_sponsorship_reserve_balance.message', + minBalance: '10', + nativeCurrency: 'MON', + matchers: ['reserve balance violation'], + }, +}; + +/** + * Checks if the callTraceErrors match any sponsorship warning rules for the given chain. + * + * @param callTraceErrors - Array of error messages from simulation + * @param chainId - The chain ID of the transaction + * @returns True if a matching rule is found, false otherwise + */ +function hasGasSponsorshipWarning( + callTraceErrors: string[] | undefined, + chainId: Hex, +): boolean { + if (!callTraceErrors?.length) { + return false; + } + + const rule = GAS_SPONSORSHIP_WARNING_RULES[chainId]; + if (!rule) { + return false; + } + + const normalizedErrors = callTraceErrors.map((error) => error.toLowerCase()); + return rule.matchers.some((matcher) => + normalizedErrors.some((error) => error.includes(matcher)), + ); +} + +/** + * Hook that returns an alert when gas sponsorship fails due to reserve balance requirements. + * + * This hook checks for specific error patterns in the transaction simulation's callTraceErrors + * and displays a warning alert when sponsorship is unavailable due to insufficient reserve balance. + * + * Currently configured for Monad network which requires a minimum of 10 MON in the account + * for gas sponsorship to work. + * + * @returns An array containing a warning alert if sponsorship failed, empty array otherwise + */ +export const useGasSponsorshipWarningAlert = (): Alert[] => { + const transactionMetadata = useTransactionMetadataRequest(); + const { isSupported: isGaslessSupported } = useIsGaslessSupported(); + + const { chainId, isGasFeeSponsored, simulationData } = + transactionMetadata ?? {}; + + const callTraceErrors = ( + simulationData as SimulationDataWithCallTraceErrors | undefined + )?.callTraceErrors; + + // Use primitive boolean to avoid object reference changes on every render + const hasWarning = useMemo( + () => + chainId + ? hasGasSponsorshipWarning(callTraceErrors, chainId as Hex) + : false, + [callTraceErrors, chainId], + ); + + // Only show warning when: + // 1. We have a warning match from configured rules + // 2. Gas fee is NOT currently sponsored (the warning explains why) + // 3. Gasless is supported on this network (otherwise sponsorship wouldn't be expected) + const shouldShow = hasWarning && !isGasFeeSponsored && isGaslessSupported; + + return useMemo(() => { + if (!shouldShow || !chainId) { + return []; + } + + const rule = GAS_SPONSORSHIP_WARNING_RULES[chainId as Hex]; + if (!rule) { + return []; + } + + return [ + { + isBlocking: false, + field: RowAlertKey.EstimatedFee, + key: AlertKeys.GasSponsorshipReserveBalance, + message: strings(rule.messageKey, { + minBalance: rule.minBalance, + nativeTokenSymbol: rule.nativeCurrency, + }), + title: strings('alert_system.gas_sponsorship_reserve_balance.title'), + severity: Severity.Warning, + }, + ]; + }, [shouldShow, chainId]); +}; diff --git a/app/components/Views/confirmations/hooks/metrics/useConfirmationAlertMetrics.ts b/app/components/Views/confirmations/hooks/metrics/useConfirmationAlertMetrics.ts index 920620e67fd..1c4e1f82fd5 100644 --- a/app/components/Views/confirmations/hooks/metrics/useConfirmationAlertMetrics.ts +++ b/app/components/Views/confirmations/hooks/metrics/useConfirmationAlertMetrics.ts @@ -113,6 +113,7 @@ const ALERTS_NAME_METRICS: AlertNameMetrics = { [AlertKeys.BurnAddress]: 'burn_address', [AlertKeys.DomainMismatch]: 'domain_mismatch', [AlertKeys.GasEstimateFailed]: 'gas_estimate_failed', + [AlertKeys.GasSponsorshipReserveBalance]: 'gas_sponsorship_reserve_balance', [AlertKeys.InsufficientBalance]: 'insufficient_balance', [AlertKeys.InsufficientPayTokenBalance]: 'insufficient_funds', [AlertKeys.InsufficientPayTokenFees]: 'insufficient_funds_for_fees', diff --git a/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.test.ts b/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.test.ts index b5e35f10c29..4a03b738fc0 100644 --- a/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.test.ts +++ b/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.test.ts @@ -249,10 +249,10 @@ describe('useTransactionCustomAmount', () => { expect(updateTokenAmountMock).toHaveBeenCalledWith('61.725'); }); - it('sets quote requested metric when updateTokenAmount is called and hasSourceAmount is true', async () => { - useTransactionPayHasSourceAmountMock.mockReturnValue(true); + it('sets mm_pay_quote_requested metric only when hasSourceAmount becomes true after updateTokenAmount was called', async () => { + useTransactionPayHasSourceAmountMock.mockReturnValue(false); - const { result } = runHook(); + const { result, rerender } = runHook(); await act(async () => { result.current.updatePendingAmount('123.45'); @@ -264,29 +264,16 @@ describe('useTransactionCustomAmount', () => { result.current.updateTokenAmount(); }); - expect(setConfirmationMetricMock).toHaveBeenCalledWith({ - properties: { - mm_pay_quote_requested: true, - }, - }); - }); - - it('does not set quote requested metric when updateTokenAmount is called and hasSourceAmount is false', async () => { - useTransactionPayHasSourceAmountMock.mockReturnValue(false); + expect(setConfirmationMetricMock).not.toHaveBeenCalled(); - const { result } = runHook(); - - await act(async () => { - result.current.updatePendingAmount('123.45'); - }); - - setConfirmationMetricMock.mockClear(); + // Simulate hasSourceAmount becoming true + useTransactionPayHasSourceAmountMock.mockReturnValue(true); await act(async () => { - result.current.updateTokenAmount(); + rerender({}); }); - expect(setConfirmationMetricMock).not.toHaveBeenCalledWith({ + expect(setConfirmationMetricMock).toHaveBeenCalledWith({ properties: { mm_pay_quote_requested: true, }, diff --git a/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.ts b/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.ts index 05002528bbb..6659d310167 100644 --- a/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.ts +++ b/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.ts @@ -35,6 +35,7 @@ export function useTransactionCustomAmount({ const totals = useTransactionPayTotals(); const hasSourceAmount = useTransactionPayHasSourceAmount(); const { setConfirmationMetric } = useConfirmationMetricEvents(); + const [isTokenAmountUpdated, setIsTokenAmountUpdated] = useState(false); const debounceSetAmountDelayed = useMemo( () => @@ -154,20 +155,19 @@ export function useTransactionCustomAmount({ const updateTokenAmount = useCallback(() => { updateTokenAmountCallback(amountHuman); + setIsTokenAmountUpdated(true); + }, [amountHuman, updateTokenAmountCallback]); - if (hasSourceAmount) { + useEffect(() => { + if (isTokenAmountUpdated && hasSourceAmount) { setConfirmationMetric({ properties: { mm_pay_quote_requested: true, }, }); + setIsTokenAmountUpdated(false); } - }, [ - amountHuman, - hasSourceAmount, - setConfirmationMetric, - updateTokenAmountCallback, - ]); + }, [hasSourceAmount, isTokenAmountUpdated, setConfirmationMetric]); return { amountFiat, diff --git a/app/constants/smartTransactions.test.ts b/app/constants/smartTransactions.test.ts index 84bddad3e9a..a05b86377fe 100644 --- a/app/constants/smartTransactions.test.ts +++ b/app/constants/smartTransactions.test.ts @@ -1,6 +1,9 @@ import { NETWORKS_CHAIN_ID } from './network'; import { isProduction } from '../util/environment'; -import { getAllowedSmartTransactionsChainIds } from './smartTransactions'; +import { + getAllowedSmartTransactionsChainIds, + sanitizeOrigin, +} from './smartTransactions'; jest.mock('../util/environment', () => ({ isProduction: jest.fn(() => false), // Initially mock isProduction to return false @@ -42,4 +45,50 @@ describe('smartTransactions', () => { ]); }); }); + + describe('sanitizeOrigin', () => { + it('extracts hostname from URL with path', () => { + expect(sanitizeOrigin('https://uniswap.org/swap?token=0x123')).toBe( + 'uniswap.org', + ); + }); + + it('extracts hostname from URL with subdomain', () => { + expect(sanitizeOrigin('https://app.aave.com/#/markets')).toBe( + 'app.aave.com', + ); + }); + + it('extracts hostname from URL with port', () => { + expect(sanitizeOrigin('http://localhost:3000/test')).toBe('localhost'); + }); + + it('returns internal origin as-is', () => { + expect(sanitizeOrigin('metamask')).toBe('metamask'); + }); + + it('returns MetaMask Mobile origin as-is', () => { + expect(sanitizeOrigin('MetaMask Mobile')).toBe('MetaMask Mobile'); + }); + + it('returns RAMPS_SEND origin as-is', () => { + expect(sanitizeOrigin('RAMPS_SEND')).toBe('RAMPS_SEND'); + }); + + it('returns WalletConnect origin as-is', () => { + expect(sanitizeOrigin('wc::')).toBe('wc::'); + }); + + it('returns SDK origin as-is', () => { + expect(sanitizeOrigin('MMSDKREMOTE::abc123')).toBe('MMSDKREMOTE::abc123'); + }); + + it('returns undefined for undefined input', () => { + expect(sanitizeOrigin(undefined)).toBeUndefined(); + }); + + it('returns empty string for empty string input', () => { + expect(sanitizeOrigin('')).toBeUndefined(); + }); + }); }); diff --git a/app/constants/smartTransactions.ts b/app/constants/smartTransactions.ts index 1bfdde5ec08..760bf0f7f29 100644 --- a/app/constants/smartTransactions.ts +++ b/app/constants/smartTransactions.ts @@ -10,6 +10,31 @@ const CLIENT_ID_ANDROID = 'mobileAndroid'; export const getClientForTransactionMetadata = (): string => Device.isIos() ? CLIENT_ID_IOS : CLIENT_ID_ANDROID; +/** + * Sanitizes transaction origin for smart transaction analytics. + * - For URL origins (dApps): extracts hostname only + * - For internal origins: returns as-is + * NOTE: This is temporary and will be deprecated once we have a proper feature tracking system. + * + * @param origin - The transaction origin to sanitize + * @returns The sanitized origin (hostname for URLs, original value otherwise) + */ +export const sanitizeOrigin = (origin?: string): string | undefined => { + if (!origin) { + return undefined; + } + + try { + // Attempt to parse as URL - will throw for non-URL strings + const url = new URL(origin); + // If hostname is empty, the input wasn't a real URL (e.g., 'wc::', 'MMSDKREMOTE::') + return url.hostname || origin; + } catch { + // Not a valid URL (internal origins like 'metamask', 'RAMPS_SEND') + return origin; + } +}; + // TODO: deprecate this and use the feature flags instead const ALLOWED_SMART_TRANSACTIONS_CHAIN_IDS_DEVELOPMENT: Hex[] = [ NETWORKS_CHAIN_ID.MAINNET, diff --git a/app/core/Analytics/MetaMetrics.events.ts b/app/core/Analytics/MetaMetrics.events.ts index 06fec3892de..de6e838faaa 100644 --- a/app/core/Analytics/MetaMetrics.events.ts +++ b/app/core/Analytics/MetaMetrics.events.ts @@ -71,6 +71,7 @@ enum EVENT_NAME { // Wallet WALLET_OPENED = 'Wallet Opened', + ADDRESS_COPIED = 'Address Copied', TOKEN_ADDED = 'Token Added', COLLECTIBLE_ADDED = 'Collectible Added', NFT_DETAILS_OPENED = 'NFT Details Opened', @@ -713,6 +714,7 @@ const events = { PROCEED_ANYWAY_CLICKED: generateOpt(EVENT_NAME.PROCEED_ANYWAY_CLICKED), WALLET_OPENED: generateOpt(EVENT_NAME.WALLET_OPENED), + ADDRESS_COPIED: generateOpt(EVENT_NAME.ADDRESS_COPIED), TOKEN_ADDED: generateOpt(EVENT_NAME.TOKEN_ADDED), COLLECTIBLE_ADDED: generateOpt(EVENT_NAME.COLLECTIBLE_ADDED), NFT_DETAILS_OPENED: generateOpt(EVENT_NAME.NFT_DETAILS_OPENED), diff --git a/app/core/Engine/controllers/ramps-controller/ramps-controller-init.test.ts b/app/core/Engine/controllers/ramps-controller/ramps-controller-init.test.ts index 9a0dee09a1c..fb55eaf07c5 100644 --- a/app/core/Engine/controllers/ramps-controller/ramps-controller-init.test.ts +++ b/app/core/Engine/controllers/ramps-controller/ramps-controller-init.test.ts @@ -91,14 +91,37 @@ describe('ramps controller init', () => { it('uses initial state when initial state is passed in', () => { const initialRampsControllerState: RampsControllerState = { userRegion: createMockUserRegion('us-ca'), - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, + countries: { + data: [], + selected: null, + isLoading: false, + error: null, + }, + providers: { + data: [], + selected: null, + isLoading: false, + error: null, + }, + tokens: { + data: null, + selected: null, + isLoading: false, + error: null, + }, + paymentMethods: { + data: [], + selected: null, + isLoading: false, + error: null, + }, + quotes: { + data: null, + selected: null, + isLoading: false, + error: null, + }, requests: {}, - paymentMethods: [], - selectedPaymentMethod: null, - countries: [], }; initRequestMock.persistedState = { diff --git a/app/core/Engine/messengers/perps-controller-messenger/index.ts b/app/core/Engine/messengers/perps-controller-messenger/index.ts index 392d71f8963..42a061b0313 100644 --- a/app/core/Engine/messengers/perps-controller-messenger/index.ts +++ b/app/core/Engine/messengers/perps-controller-messenger/index.ts @@ -29,6 +29,11 @@ export function getPerpsControllerMessenger( 'NetworkController:getState', 'AuthenticationController:getBearerToken', 'RemoteFeatureFlagController:getState', + 'AccountTreeController:getAccountsFromSelectedAccountGroup', + 'KeyringController:signTypedMessage', + 'NetworkController:getNetworkClientById', + 'NetworkController:findNetworkClientIdByChainId', + 'TransactionController:addTransaction', ], events: [ 'TransactionController:transactionSubmitted', diff --git a/app/selectors/assets/assets-list.test.ts b/app/selectors/assets/assets-list.test.ts index cd6e22047af..66853f254d8 100644 --- a/app/selectors/assets/assets-list.test.ts +++ b/app/selectors/assets/assets-list.test.ts @@ -1,4 +1,8 @@ -import { AccountGroupType, AccountWalletType } from '@metamask/account-api'; +import { + AccountGroupId, + AccountGroupType, + AccountWalletType, +} from '@metamask/account-api'; import { EthAccountType, SolAccountType, @@ -733,18 +737,23 @@ describe('selectAsset', () => { }); }); - it('scopes native and staked lookups to selected account', () => { - const stateWithSecondEvm = mockState(); - const account1Id = - stateWithSecondEvm.engine.backgroundState.AccountsController - .internalAccounts.selectedAccount; + it('scopes native and staked lookups to selected account group', () => { + const baseState = mockState(); + + // Account 1 info (already exists in mockState) + const account1Id = 'd7f11451-9d79-4df4-a012-afd253443639'; + const group1Id = 'entropy:01K1TJY9QPSCKNBSVGZNG510GJ/0'; + // Create second account group with different EVM account const account2Id = '11111111-1111-1111-1111-111111111111'; const account2Address = '0x1111111111111111111111111111111111111111'; const account2AddressLowercased = account2Address.toLowerCase(); + const group2Id = 'entropy:01K1TJY9QPSCKNBSVGZNG510GJ/1'; + const walletId = 'entropy:01K1TJY9QPSCKNBSVGZNG510GJ'; - const withSelectedAccount = ( + const withSelectedGroup = ( state: RootState, + selectedGroup: AccountGroupId, selectedAccount: string, ): RootState => ({ ...state, @@ -752,6 +761,13 @@ describe('selectAsset', () => { ...state.engine, backgroundState: { ...state.engine.backgroundState, + AccountTreeController: { + ...state.engine.backgroundState.AccountTreeController, + accountTree: { + ...state.engine.backgroundState.AccountTreeController.accountTree, + selectedAccountGroup: selectedGroup, + }, + }, AccountsController: { ...state.engine.backgroundState.AccountsController, internalAccounts: { @@ -764,8 +780,8 @@ describe('selectAsset', () => { }, }); - // Add second EVM internal account into the same selected account group - stateWithSecondEvm.engine.backgroundState.AccountsController.internalAccounts.accounts[ + // Add second EVM account to AccountsController + baseState.engine.backgroundState.AccountsController.internalAccounts.accounts[ account2Id ] = { id: account2Id, @@ -783,73 +799,75 @@ describe('selectAsset', () => { }, }; - const groupId = 'entropy:01K1TJY9QPSCKNBSVGZNG510GJ/0'; - const walletId = 'entropy:01K1TJY9QPSCKNBSVGZNG510GJ'; - stateWithSecondEvm.engine.backgroundState.AccountTreeController.accountTree.wallets[ + // Create second account group with the second EVM account + baseState.engine.backgroundState.AccountTreeController.accountTree.wallets[ walletId - ].groups[groupId].accounts = [ - ...stateWithSecondEvm.engine.backgroundState.AccountTreeController - .accountTree.wallets[walletId].groups[groupId].accounts, - account2Id, - ]; + ].groups[group2Id] = { + id: group2Id, + type: AccountGroupType.MultichainAccount, + accounts: [account2Id], + metadata: { + name: 'Account Group 2', + pinned: false, + hidden: false, + entropy: { + groupIndex: 1, + }, + }, + }; - // Provide AccountTracker balances for second address on mainnet - stateWithSecondEvm.engine.backgroundState.AccountTrackerController.accountsByChainId[ + // Provide AccountTracker balances for second account on mainnet + baseState.engine.backgroundState.AccountTrackerController.accountsByChainId[ '0x1' ][account2AddressLowercased] = { balance: '0x0DE0B6B3A7640000', // 1 ETH stakedBalance: '0x1BC16D674EC80000', // 2 ETH }; - // Provide empty token lists/balances for second address to keep asset building stable - stateWithSecondEvm.engine.backgroundState.TokensController.allTokens['0x1'][ + // Provide empty token lists/balances for second address + baseState.engine.backgroundState.TokensController.allTokens['0x1'][ account2AddressLowercased ] = []; - stateWithSecondEvm.engine.backgroundState.TokensController.allTokens['0xa'][ + baseState.engine.backgroundState.TokensController.allTokens['0xa'][ account2AddressLowercased ] = []; ( - stateWithSecondEvm.engine.backgroundState.TokenBalancesController + baseState.engine.backgroundState.TokenBalancesController .tokenBalances as Record )[account2AddressLowercased] = {}; - // Sanity check: original account still resolves correctly - const stateForAccount1 = withSelectedAccount( - stateWithSecondEvm, - account1Id, - ); + // Test Group 1: should return account 1 balances + const stateForGroup1 = withSelectedGroup(baseState, group1Id, account1Id); - const stakedForAccount1 = selectAsset(stateForAccount1, { + const stakedForGroup1 = selectAsset(stateForGroup1, { address: '0x0000000000000000000000000000000000000000', chainId: '0x1', isStaked: true, }); - expect(stakedForAccount1?.balance).toBe('100'); + expect(stakedForGroup1?.balance).toBe('100'); + expect(stakedForGroup1?.balanceFiat).toBe('$240,000.00'); - // Switch selected account → balances should follow - const stateForAccount2 = withSelectedAccount( - stateWithSecondEvm, - account2Id, - ); + // Test Group 2: should return account 2 balances + const stateForGroup2 = withSelectedGroup(baseState, group2Id, account2Id); - const nativeForAccount2 = selectAsset(stateForAccount2, { + const nativeForGroup2 = selectAsset(stateForGroup2, { address: '0x0000000000000000000000000000000000000000', chainId: '0x1', isStaked: false, }); - expect(nativeForAccount2).toMatchObject({ + expect(nativeForGroup2).toMatchObject({ name: 'Ethereum', balance: '1', balanceFiat: '$2,400.00', isStaked: false, }); - const stakedForAccount2 = selectAsset(stateForAccount2, { + const stakedForGroup2 = selectAsset(stateForGroup2, { address: '0x0000000000000000000000000000000000000000', chainId: '0x1', isStaked: true, }); - expect(stakedForAccount2).toMatchObject({ + expect(stakedForGroup2).toMatchObject({ name: 'Staked Ethereum', balance: '2', balanceFiat: '$4,800.00', diff --git a/app/selectors/assets/assets-list.ts b/app/selectors/assets/assets-list.ts index a58bd92930e..a639926a3e5 100644 --- a/app/selectors/assets/assets-list.ts +++ b/app/selectors/assets/assets-list.ts @@ -4,8 +4,11 @@ import { getNativeTokenAddress, TokenListState, } from '@metamask/assets-controllers'; -import { MULTICHAIN_NETWORK_DECIMAL_PLACES } from '@metamask/multichain-network-controller'; -import { CaipChainId, Hex, hexToBigInt } from '@metamask/utils'; +import { + MULTICHAIN_NETWORK_DECIMAL_PLACES, + toEvmCaipChainId, +} from '@metamask/multichain-network-controller'; +import { CaipChainId, Hex, hexToBigInt, isCaipChainId } from '@metamask/utils'; import { createSelector } from 'reselect'; import I18n from '../../../locales/i18n'; @@ -28,10 +31,8 @@ import { } from '../../core/Multichain/constants'; import { sortAssetsWithPriority } from '../../components/UI/Tokens/util/sortAssetsWithPriority'; import { selectAllTokens } from '../tokensController'; -import { - selectSelectedInternalAccountAddress, - selectSelectedInternalAccountId, -} from '../accountsController'; +import { selectSelectedInternalAccountAddress } from '../accountsController'; +import { selectSelectedInternalAccountByScope } from '../multichainAccounts/accounts'; const getStateForAssetSelector = (state: RootState) => { const { @@ -267,7 +268,7 @@ export const selectAsset = createSelector( state.engine.backgroundState.TokenListController.tokensChainsCache, selectAllTokens, selectSelectedInternalAccountAddress, - selectSelectedInternalAccountId, + selectSelectedInternalAccountByScope, ( _state: RootState, params: { address: string; chainId: string; isStaked?: boolean }, @@ -287,37 +288,31 @@ export const selectAsset = createSelector( tokensChainsCache, allTokens, selectedAddress, - selectedAccountId, + getAccountByScope, address, chainId, isStaked, ) => { - /** - * Note: Without this, the selector would return the wrong asset for the selected account on EVM chains. - * This caused Staked Ethereum to not update when switching accounts. - * We want to apply this to EVM chains only. - */ - const shouldScopeToSelectedAccount = - Boolean(selectedAccountId) && typeof chainId === 'string' - ? chainId.startsWith('0x') - : false; + const chainIdInCaip = isCaipChainId(chainId) + ? chainId + : toEvmCaipChainId(chainId as Hex); + + // Get the account for this chain from the selected account group + const scopedAccountId = getAccountByScope(chainIdInCaip)?.id; const asset = isStaked ? stakedAssets.find( (item) => item.chainId === chainId && - (!shouldScopeToSelectedAccount || - item.accountId === selectedAccountId) && + (!scopedAccountId || item.accountId === scopedAccountId) && item.stakedAsset.assetId === address, )?.stakedAsset : assets[chainId]?.find((item: Asset & { isStaked?: boolean }) => { - // Normalize isStaked values: treat undefined as false const itemIsStaked = Boolean(item.isStaked); const targetIsStaked = Boolean(isStaked); return ( item.assetId === address && - (!shouldScopeToSelectedAccount || - item.accountId === selectedAccountId) && + (!scopedAccountId || item.accountId === scopedAccountId) && itemIsStaked === targetIsStaked ); }); diff --git a/app/selectors/featureFlagController/assetsTrendingTokens/index.test.ts b/app/selectors/featureFlagController/assetsTrendingTokens/index.test.ts index 3f49e367ff2..c912c63efbf 100644 --- a/app/selectors/featureFlagController/assetsTrendingTokens/index.test.ts +++ b/app/selectors/featureFlagController/assetsTrendingTokens/index.test.ts @@ -12,6 +12,10 @@ jest.mock('../../../core/Engine', () => ({ init: () => mockedEngine.init(), })); +jest.mock('../../../util/test/utils', () => ({ + isE2E: true, +})); + beforeEach(() => { jest.clearAllMocks(); }); diff --git a/app/selectors/featureFlagController/assetsTrendingTokens/index.ts b/app/selectors/featureFlagController/assetsTrendingTokens/index.ts index e931a1017f6..b1077b349e4 100644 --- a/app/selectors/featureFlagController/assetsTrendingTokens/index.ts +++ b/app/selectors/featureFlagController/assetsTrendingTokens/index.ts @@ -2,6 +2,7 @@ import { createSelector } from 'reselect'; import { selectRemoteFeatureFlags } from '..'; import compareVersions from 'compare-versions'; import packageJson from '../../../../package.json'; +import { isE2E } from '../../../util/test/utils'; const APP_VERSION = packageJson.version; @@ -81,6 +82,9 @@ export const isAssetsTrendingTokensFeatureEnabled = ( return evaluateAssetsTrendingTokensRemoteFlag(flagValue); }; +// We are enabling this feature flag to be enabled by default for non-E2E builds +const forcedTrueOverride = () => (!isE2E ? 'true' : undefined); + /** * Selector to check if the assets trending tokens feature flag is enabled. * Supports environment variable override (OVERRIDE_REMOTE_FEATURE_FLAGS + ASSETS_TRENDING_TOKENS_ENABLED). @@ -103,7 +107,7 @@ export const selectAssetsTrendingTokensEnabled = createSelector( return isAssetsTrendingTokensFeatureEnabled( value, - envOverride || undefined, + forcedTrueOverride() || envOverride || undefined, ); }, ); diff --git a/app/selectors/rampsController/index.test.ts b/app/selectors/rampsController/index.test.ts index 4ba6edc6125..680790fb6dd 100644 --- a/app/selectors/rampsController/index.test.ts +++ b/app/selectors/rampsController/index.test.ts @@ -1,7 +1,6 @@ import { RootState } from '../../reducers'; import { RampsControllerState, - RequestStatus, UserRegion, type Provider, type Country, @@ -9,29 +8,44 @@ import { } from '@metamask/ramps-controller'; import { selectUserRegion, - selectUserRegionRequest, - selectSelectedProvider, selectProviders, selectTokens, - selectSelectedToken, selectCountries, - selectCountriesRequest, - selectTokensRequest, - selectProvidersRequest, selectPaymentMethods, - selectSelectedPaymentMethod, - selectPaymentMethodsRequest, selectRampsControllerState, } from './index'; +const createDefaultResourceState = ( + data: TData, + selected: TSelected = null as TSelected, +) => ({ + data, + selected, + isLoading: false, + error: null, +}); + +type RampsControllerStateOverride = Partial; + const createMockState = ( - rampsController: Partial = {}, + rampsController: RampsControllerStateOverride = {}, ): RootState => ({ engine: { backgroundState: { RampsController: { userRegion: null, + countries: createDefaultResourceState([]), + providers: createDefaultResourceState( + [], + null, + ), + tokens: createDefaultResourceState(null, null), + paymentMethods: createDefaultResourceState< + PaymentMethod[], + PaymentMethod | null + >([], null), + quotes: createDefaultResourceState(null), requests: {}, ...rampsController, }, @@ -113,575 +127,127 @@ const mockPaymentMethods: PaymentMethod[] = [mockPaymentMethod]; describe('RampsController Selectors', () => { describe('selectUserRegion', () => { - it('returns user region from state', () => { - const state = createMockState({ userRegion: mockUserRegion }); - - expect(selectUserRegion(state)).toEqual(mockUserRegion); - }); - - it('returns null when user region is null', () => { - const state = createMockState({ userRegion: null }); - - expect(selectUserRegion(state)).toBeNull(); - }); - }); - - describe('selectUserRegionRequest', () => { - it('returns request state with data, isFetching, and error', () => { - const state = createMockState({ - requests: { - 'init:[]': { - status: RequestStatus.SUCCESS, - data: mockUserRegion, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - - const result = selectUserRegionRequest(state); - - expect(result).toEqual({ - data: mockUserRegion, - isFetching: false, - error: null, - }); - }); - - it('returns isFetching true when request is loading', () => { + it('returns user region when userRegion is set', () => { const state = createMockState({ - requests: { - 'init:[]': { - status: RequestStatus.LOADING, - data: null, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, + userRegion: mockUserRegion, }); - const result = selectUserRegionRequest(state); - - expect(result.isFetching).toBe(true); + const result = selectUserRegion(state); + expect(result).toEqual(mockUserRegion); }); - it('returns error when request failed', () => { + it('returns null when userRegion is null', () => { const state = createMockState({ - requests: { - 'init:[]': { - status: RequestStatus.ERROR, - data: null, - error: 'Network error', - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, + userRegion: null, }); - const result = selectUserRegionRequest(state); - - expect(result.error).toBe('Network error'); - }); - - it('returns default state when request does not exist', () => { - const state = createMockState(); - - const result = selectUserRegionRequest(state); - - expect(result).toEqual({ - data: null, - isFetching: false, - error: null, - }); - }); - }); - - describe('selectSelectedProvider', () => { - it('returns selected provider from state', () => { - const state = createMockState({ selectedProvider: mockProvider }); - - expect(selectSelectedProvider(state)).toEqual(mockProvider); - }); - - it('returns null when selected provider is null', () => { - const state = createMockState({ selectedProvider: null }); - - expect(selectSelectedProvider(state)).toBeNull(); - }); - - it('returns null when RampsController state is undefined', () => { - const state = { - engine: { - backgroundState: { - RampsController: undefined, - }, - }, - } as unknown as RootState; - - expect(selectSelectedProvider(state)).toBeNull(); + const result = selectUserRegion(state); + expect(result).toBeNull(); }); }); describe('selectProviders', () => { - it('returns providers from state', () => { - const state = createMockState({ providers: [mockProvider] }); - - expect(selectProviders(state)).toEqual([mockProvider]); - }); - - it('returns empty array when providers is null', () => { - const state = createMockState({ providers: [] }); - - expect(selectProviders(state)).toEqual([]); - }); - - it('returns empty array when providers is undefined', () => { - const state = createMockState(); - - expect(selectProviders(state)).toEqual([]); - }); - }); - - describe('selectTokens', () => { - it('returns tokens from state', () => { - const state = createMockState({ tokens: mockTokens }); - - expect(selectTokens(state)).toEqual(mockTokens); - }); - - it('returns null when tokens is null', () => { - const state = createMockState({ tokens: null }); - - expect(selectTokens(state)).toBeNull(); - }); - - it('returns null when tokens is undefined', () => { - const state = createMockState(); - - expect(selectTokens(state)).toBeNull(); - }); - }); - - describe('selectSelectedToken', () => { - it('returns selected token from state', () => { - const state = createMockState({ selectedToken: mockToken }); - - expect(selectSelectedToken(state)).toEqual(mockToken); - }); - - it('returns null when selected token is null', () => { - const state = createMockState({ selectedToken: null }); - - expect(selectSelectedToken(state)).toBeNull(); - }); - - it('returns null when RampsController state is undefined', () => { - const state = { - engine: { - backgroundState: { - RampsController: undefined, - }, - }, - } as unknown as RootState; - - expect(selectSelectedToken(state)).toBeNull(); - }); - }); - - describe('selectCountries', () => { - it('returns countries from state', () => { - const state = createMockState({ countries: mockCountries }); - - expect(selectCountries(state)).toEqual(mockCountries); - }); - - it('returns empty array when countries are not available', () => { - const state = createMockState(); - - expect(selectCountries(state)).toEqual([]); - }); - }); - - describe('selectCountriesRequest', () => { - it('returns request state', () => { - const state = createMockState({ - requests: { - 'getCountries:[]': { - status: RequestStatus.SUCCESS, - data: mockCountries, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - - const result = selectCountriesRequest(state); - - expect(result).toEqual({ - data: mockCountries, - isFetching: false, - error: null, - }); - }); - - it('returns loading state when request is in progress', () => { - const state = createMockState({ - requests: { - 'getCountries:[]': { - status: RequestStatus.LOADING, - data: null, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - - const result = selectCountriesRequest(state); - - expect(result).toEqual({ - data: null, - isFetching: true, - error: null, - }); - }); - - it('returns error state when request fails', () => { + it('returns providers resource state', () => { const state = createMockState({ - requests: { - 'getCountries:[]': { - status: RequestStatus.ERROR, - data: null, - error: 'Network error', - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, + providers: { + data: [mockProvider], + selected: mockProvider, + isLoading: false, + error: null, }, }); - const result = selectCountriesRequest(state); - - expect(result).toEqual({ - data: null, - isFetching: false, - error: 'Network error', - }); + const result = selectProviders(state); + expect(result.data).toEqual([mockProvider]); + expect(result.selected).toEqual(mockProvider); + expect(result.isLoading).toBe(false); + expect(result.error).toBeNull(); }); - it('returns default state when request does not exist', () => { + it('returns empty array when providers data is empty', () => { const state = createMockState(); - const result = selectCountriesRequest(state); - - expect(result).toEqual({ - data: null, - isFetching: false, - error: null, - }); + const result = selectProviders(state); + expect(result.data).toEqual([]); }); }); - describe('selectTokensRequest', () => { - it('returns request state for region and action', () => { - const state = createMockState({ - requests: { - 'getTokens:["us-ca","buy"]': { - status: RequestStatus.SUCCESS, - data: mockTokens, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - - const result = selectTokensRequest('us-ca', 'buy')(state); - - expect(result).toEqual({ - data: mockTokens, - isFetching: false, - error: null, - }); - }); - - it('normalizes region to lowercase and trims', () => { - const state = createMockState({ - requests: { - 'getTokens:["us-ca","buy"]': { - status: RequestStatus.SUCCESS, - data: mockTokens, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - - const result = selectTokensRequest(' US-CA ', 'buy')(state); - - expect(result.data).toEqual(mockTokens); - }); - - it('defaults to buy action when not provided', () => { + describe('selectTokens', () => { + it('returns tokens resource state', () => { const state = createMockState({ - requests: { - 'getTokens:["us-ca","buy"]': { - status: RequestStatus.SUCCESS, - data: mockTokens, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, + tokens: { + data: mockTokens, + selected: mockToken, + isLoading: false, + error: null, }, }); - const result = selectTokensRequest('us-ca')(state); - + const result = selectTokens(state); expect(result.data).toEqual(mockTokens); + expect(result.selected).toEqual(mockToken); }); - it('returns default state when request does not exist', () => { + it('returns null data when tokens is null', () => { const state = createMockState(); - const result = selectTokensRequest('us-ca', 'buy')(state); - - expect(result).toEqual({ - data: null, - isFetching: false, - error: null, - }); + const result = selectTokens(state); + expect(result.data).toBeNull(); }); }); - describe('selectProvidersRequest', () => { - it('returns request state for region', () => { - const state = createMockState({ - requests: { - 'getProviders:["us-ca",null,null,null,null]': { - status: RequestStatus.SUCCESS, - data: { providers: [mockProvider] }, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - - const result = selectProvidersRequest('us-ca')(state); - - expect(result).toEqual({ - data: { providers: [mockProvider] }, - isFetching: false, - error: null, - }); - }); - - it('normalizes region to lowercase and trims', () => { - const state = createMockState({ - requests: { - 'getProviders:["us-ca",null,null,null,null]': { - status: RequestStatus.SUCCESS, - data: { providers: [mockProvider] }, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - - const result = selectProvidersRequest(' US-CA ')(state); - - expect(result.data).toEqual({ providers: [mockProvider] }); - }); - - it('includes filter options in request key', () => { - const state = createMockState({ - requests: { - 'getProviders:["us-ca","provider-1","ETH","USD",null]': { - status: RequestStatus.SUCCESS, - data: { providers: [mockProvider] }, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - - const result = selectProvidersRequest('us-ca', { - provider: 'provider-1', - crypto: 'ETH', - fiat: 'USD', - })(state); - - expect(result.data).toEqual({ providers: [mockProvider] }); - }); - - it('handles array filter options', () => { + describe('selectCountries', () => { + it('returns countries resource state', () => { const state = createMockState({ - requests: { - 'getProviders:["us-ca",["provider-1","provider-2"],["ETH","BTC"],"USD",null]': - { - status: RequestStatus.SUCCESS, - data: { providers: [mockProvider] }, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, + countries: { + data: mockCountries, + selected: null, + isLoading: false, + error: null, }, }); - const result = selectProvidersRequest('us-ca', { - provider: ['provider-1', 'provider-2'], - crypto: ['ETH', 'BTC'], - fiat: 'USD', - })(state); - - expect(result.isFetching).toBe(false); - expect(result.error).toBeNull(); - expect(result.data).toEqual({ providers: [mockProvider] }); + const result = selectCountries(state); + expect(result.data).toEqual(mockCountries); }); - it('returns default state when request does not exist', () => { + it('returns empty array when countries are not available', () => { const state = createMockState(); - const result = selectProvidersRequest('us-ca')(state); - - expect(result).toEqual({ - data: null, - isFetching: false, - error: null, - }); + const result = selectCountries(state); + expect(result.data).toEqual([]); }); }); describe('selectPaymentMethods', () => { - it('returns payment methods from state', () => { - const state = createMockState({ paymentMethods: mockPaymentMethods }); - - expect(selectPaymentMethods(state)).toEqual(mockPaymentMethods); - }); - - it('returns empty array when payment methods are not available', () => { - const state = createMockState(); - - expect(selectPaymentMethods(state)).toEqual([]); - }); - }); - - describe('selectSelectedPaymentMethod', () => { - it('returns selected payment method from state', () => { - const state = createMockState({ - selectedPaymentMethod: mockPaymentMethod, - }); - - expect(selectSelectedPaymentMethod(state)).toEqual(mockPaymentMethod); - }); - - it('returns null when selected payment method is null', () => { - const state = createMockState({ selectedPaymentMethod: null }); - - expect(selectSelectedPaymentMethod(state)).toBeNull(); - }); - - it('returns null when RampsController state is undefined', () => { - const state = { - engine: { - backgroundState: { - RampsController: undefined, - }, - }, - } as unknown as RootState; - - expect(selectSelectedPaymentMethod(state)).toBeNull(); - }); - }); - - describe('selectPaymentMethodsRequest', () => { - it('returns request state for region, fiat, assetId, and provider', () => { + it('returns payment methods resource state', () => { const state = createMockState({ - requests: { - 'getPaymentMethods:["us-ca","usd","eip155:1/erc20:0x123","provider-1"]': - { - status: RequestStatus.SUCCESS, - data: { payments: mockPaymentMethods }, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, + paymentMethods: { + data: mockPaymentMethods, + selected: mockPaymentMethod, + isLoading: false, + error: null, }, }); - const result = selectPaymentMethodsRequest( - 'us-ca', - 'usd', - 'eip155:1/erc20:0x123', - 'provider-1', - )(state); - - expect(result).toEqual({ - data: { payments: mockPaymentMethods }, - isFetching: false, - error: null, - }); + const result = selectPaymentMethods(state); + expect(result.data).toEqual(mockPaymentMethods); + expect(result.selected).toEqual(mockPaymentMethod); }); - it('normalizes region and fiat to lowercase and trims', () => { - const state = createMockState({ - requests: { - 'getPaymentMethods:["us-ca","usd","eip155:1/erc20:0x123","provider-1"]': - { - status: RequestStatus.SUCCESS, - data: { payments: mockPaymentMethods }, - error: null, - timestamp: Date.now(), - lastFetchedAt: Date.now(), - }, - }, - }); - - const result = selectPaymentMethodsRequest( - ' US-CA ', - ' USD ', - 'eip155:1/erc20:0x123', - 'provider-1', - )(state); - - expect(result.data).toEqual({ payments: mockPaymentMethods }); - }); - - it('returns default state when request does not exist', () => { + it('returns empty array when payment methods are not available', () => { const state = createMockState(); - const result = selectPaymentMethodsRequest( - 'us-ca', - 'usd', - 'eip155:1/erc20:0x123', - 'provider-1', - )(state); - - expect(result).toEqual({ - data: null, - isFetching: false, - error: null, - }); + const result = selectPaymentMethods(state); + expect(result.data).toEqual([]); }); }); describe('selectRampsControllerState', () => { it('returns RampsController state', () => { - const rampsState: Partial = { - userRegion: mockUserRegion, - selectedProvider: mockProvider, - providers: [mockProvider], - tokens: mockTokens, - requests: {}, - }; - const state = createMockState(rampsState); - - expect(selectRampsControllerState(state)).toEqual(rampsState); + const state = createMockState(); + + expect(selectRampsControllerState(state)).toBeDefined(); }); it('returns undefined when RampsController is undefined', () => { diff --git a/app/selectors/rampsController/index.ts b/app/selectors/rampsController/index.ts index 0f8f6e6deeb..49c91cb20ec 100644 --- a/app/selectors/rampsController/index.ts +++ b/app/selectors/rampsController/index.ts @@ -1,16 +1,15 @@ import { createSelector } from 'reselect'; import { - createRequestSelector, type UserRegion, type Provider, type Country, - type PaymentMethodsResponse, - type RampsControllerState, + type PaymentMethod, + type RampsToken, + type TokensResponse, + type ResourceState, } from '@metamask/ramps-controller'; import { RootState } from '../../reducers'; -type TokensResponse = NonNullable; - /** * Selects the RampsController state from Redux. * This is a simple selector (not memoized) since it only extracts @@ -20,150 +19,70 @@ export const selectRampsControllerState = (state: RootState) => state.engine.backgroundState.RampsController; /** - * Selects the user's region from state. - * Returns UserRegion | null (UserRegion contains country, state, and regionCode). + * Default resource state for when the controller state is unavailable. */ -export const selectUserRegion = createSelector( - selectRampsControllerState, - (rampsControllerState) => rampsControllerState?.userRegion ?? null, -); - -/** - * Selects the user's selected provider from state. - */ -export const selectSelectedProvider = createSelector( - selectRampsControllerState, - (rampsControllerState) => rampsControllerState?.selectedProvider ?? null, -); +const createDefaultResourceState = ( + data: TData, + selected: TSelected = null as TSelected, +): ResourceState => ({ + data, + selected, + isLoading: false, + error: null, +}); /** - * Selects the list of providers available for the current region. + * Selects the user region from RampsController state (UserRegion | null). */ -export const selectProviders = createSelector( +export const selectUserRegion = createSelector( selectRampsControllerState, - (rampsControllerState) => rampsControllerState?.providers ?? [], + (rampsControllerState): UserRegion | null => + rampsControllerState?.userRegion ?? null, ); /** - * Selects the tokens fetched for the current region and action. + * Selects the countries resource state (data, isLoading, error). */ -export const selectTokens = createSelector( +export const selectCountries = createSelector( selectRampsControllerState, - (rampsControllerState) => rampsControllerState?.tokens ?? null, + (rampsControllerState): ResourceState => + rampsControllerState?.countries ?? + createDefaultResourceState([]), ); /** - * Selects the user's selected token from state. + * Selects the providers resource state (data, selected, isLoading, error). */ -export const selectSelectedToken = createSelector( +export const selectProviders = createSelector( selectRampsControllerState, - (rampsControllerState) => rampsControllerState?.selectedToken ?? null, + (rampsControllerState): ResourceState => + rampsControllerState?.providers ?? + createDefaultResourceState([], null), ); /** - * Selects the list of countries available for ramp actions. + * Selects the tokens resource state (data, selected, isLoading, error). */ -export const selectCountries = createSelector( +export const selectTokens = createSelector( selectRampsControllerState, - (rampsControllerState) => rampsControllerState?.countries ?? [], + ( + rampsControllerState, + ): ResourceState => + rampsControllerState?.tokens ?? + createDefaultResourceState( + null, + null, + ), ); /** - * Selects the payment methods available for the current context. + * Selects the payment methods resource state (data, selected, isLoading, error). */ export const selectPaymentMethods = createSelector( selectRampsControllerState, - (rampsControllerState) => rampsControllerState?.paymentMethods ?? [], -); - -/** - * Selects the user's selected payment method from state. - */ -export const selectSelectedPaymentMethod = createSelector( - selectRampsControllerState, - (rampsControllerState) => rampsControllerState?.selectedPaymentMethod ?? null, + ( + rampsControllerState, + ): ResourceState => + rampsControllerState?.paymentMethods ?? + createDefaultResourceState([], null), ); - -/** - * Selects the user region request state. - */ -export const selectUserRegionRequest = createRequestSelector< - RootState, - UserRegion | null ->(selectRampsControllerState, 'init', []); - -/** - * Selects the countries request state. - * - * @returns Request selector for countries. - */ -export const selectCountriesRequest = createRequestSelector< - RootState, - Country[] ->(selectRampsControllerState, 'getCountries', []); - -/** - * Selects the tokens request state for a given region and action. - * - * @param region - The region code (e.g., "us", "fr", "us-ny"). - * @param action - The ramp action type ('buy' or 'sell'). - * @returns Request selector for tokens. - */ -export const selectTokensRequest = ( - region: string, - action: 'buy' | 'sell' = 'buy', -) => - createRequestSelector( - selectRampsControllerState, - 'getTokens', - [region.toLowerCase().trim(), action], - ); - -/** - * Selects the providers request state for a given region. - * - * @param region - The region code (e.g., "us", "fr", "us-ny"). - * @param options - Optional filter options for the request cache key. - * @returns Request selector for providers. - */ -export const selectProvidersRequest = ( - region: string, - options?: { - provider?: string | string[]; - crypto?: string | string[]; - fiat?: string | string[]; - payments?: string | string[]; - }, -) => - createRequestSelector( - selectRampsControllerState, - 'getProviders', - [ - region.toLowerCase().trim(), - options?.provider, - options?.crypto, - options?.fiat, - options?.payments, - ], - ); - -/** - * Selects the payment methods request state for a given context. - * - * @param region - The region code (e.g., "us", "fr", "us-ny"). - * @param fiat - The fiat currency code (e.g., "usd", "eur"). - * @param assetId - The asset ID in CAIP-19 format. - * @param provider - The provider ID. - * @returns Request selector for payment methods. - */ -export const selectPaymentMethodsRequest = ( - region: string, - fiat: string, - assetId: string, - provider: string, -) => - createRequestSelector( - selectRampsControllerState, - 'getPaymentMethods', - [region.toLowerCase().trim(), fiat.toLowerCase().trim(), assetId, provider], - ); diff --git a/app/store/migrations/115.test.ts b/app/store/migrations/115.test.ts index 22a8c6d45d8..74320805698 100644 --- a/app/store/migrations/115.test.ts +++ b/app/store/migrations/115.test.ts @@ -20,7 +20,6 @@ const mockCaptureException = captureException as jest.MockedFunction< describe(`Migration ${migrationVersion}`, () => { beforeEach(() => { jest.clearAllMocks(); - // Default implementation: use the actual hasProperty function mockHasProperty.mockImplementation( jest.requireActual('@metamask/utils').hasProperty, ); @@ -127,20 +126,16 @@ describe(`Migration ${migrationVersion}`, () => { const actualHasProperty = jest.requireActual('@metamask/utils').hasProperty; - // Allow the first two calls (in ensureValidState) to succeed, - // then throw on the third call (first call in the try block) mockHasProperty - .mockImplementationOnce(actualHasProperty) // ensureValidState: check for 'engine' - .mockImplementationOnce(actualHasProperty) // ensureValidState: check for 'backgroundState' + .mockImplementationOnce(actualHasProperty) + .mockImplementationOnce(actualHasProperty) .mockImplementationOnce(() => { throw new Error('Test error'); }); const result = migrate(state); - // State should be returned unchanged (migration catches the error) expect(result).toStrictEqual(state); - // Verify the error was captured expect(mockCaptureException).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining('Migration 115 failed'), diff --git a/app/store/migrations/117.test.ts b/app/store/migrations/117.test.ts new file mode 100644 index 00000000000..999d4ed0521 --- /dev/null +++ b/app/store/migrations/117.test.ts @@ -0,0 +1,271 @@ +import migrate, { migrationVersion } from './117'; + +jest.mock('@sentry/react-native', () => ({ + captureException: jest.fn(), +})); + +jest.mock('./util', () => ({ + ensureValidState: jest.fn((_state: unknown, _version: number) => true), +})); + +const mockedEnsureValidState = jest.mocked( + jest.requireMock<{ ensureValidState: (s: unknown, v: number) => boolean }>( + './util', + ).ensureValidState, +); + +describe(`Migration ${migrationVersion}`, () => { + beforeEach(() => { + jest.clearAllMocks(); + mockedEnsureValidState.mockReturnValue(true); + }); + + it('returns state unchanged if state is invalid', () => { + mockedEnsureValidState.mockReturnValue(false); + const invalidState = null; + const result = migrate(invalidState); + expect(result).toBe(invalidState); + }); + + it('returns state unchanged if RampsController is missing', () => { + const state = { + engine: { + backgroundState: {}, + }, + }; + const result = migrate(state); + expect(result).toStrictEqual(state); + }); + + it('returns state unchanged if RampsController is not an object', () => { + const state = { + engine: { + backgroundState: { + RampsController: 'invalid', + }, + }, + }; + const result = migrate(state); + expect(result).toStrictEqual(state); + }); + + it('migrates legacy providers array and selectedProvider to ResourceState', () => { + const providers = [{ id: '/providers/test', name: 'Test' }]; + const selectedProvider = providers[0]; + const state = { + engine: { + backgroundState: { + RampsController: { + providers, + selectedProvider, + }, + }, + }, + }; + + const result = migrate(state) as typeof state; + + expect( + result.engine.backgroundState.RampsController.providers, + ).toStrictEqual({ + data: providers, + selected: selectedProvider, + isLoading: false, + error: null, + }); + expect( + (result.engine.backgroundState.RampsController as Record) + .selectedProvider, + ).toBeUndefined(); + }); + + it('migrates legacy tokens and selectedToken to ResourceState', () => { + const tokens = { topTokens: [], allTokens: [] }; + const state = { + engine: { + backgroundState: { + RampsController: { + tokens, + selectedToken: null, + }, + }, + }, + }; + + const result = migrate(state) as typeof state; + + expect(result.engine.backgroundState.RampsController.tokens).toStrictEqual({ + data: tokens, + selected: null, + isLoading: false, + error: null, + }); + expect( + (result.engine.backgroundState.RampsController as Record) + .selectedToken, + ).toBeUndefined(); + }); + + it('migrates legacy paymentMethods array and selectedPaymentMethod to ResourceState', () => { + const paymentMethods = [{ id: '/payments/card', paymentType: 'card' }]; + const selectedPaymentMethod = paymentMethods[0]; + const state = { + engine: { + backgroundState: { + RampsController: { + paymentMethods, + selectedPaymentMethod, + }, + }, + }, + }; + + const result = migrate(state) as typeof state; + + expect( + result.engine.backgroundState.RampsController.paymentMethods, + ).toStrictEqual({ + data: paymentMethods, + selected: selectedPaymentMethod, + isLoading: false, + error: null, + }); + expect( + (result.engine.backgroundState.RampsController as Record) + .selectedPaymentMethod, + ).toBeUndefined(); + }); + + it('migrates legacy countries array to ResourceState', () => { + const countries = [{ isoCode: 'US', name: 'United States' }]; + const state = { + engine: { + backgroundState: { + RampsController: { + countries, + }, + }, + }, + }; + + const result = migrate(state) as typeof state; + + expect( + result.engine.backgroundState.RampsController.countries, + ).toStrictEqual({ + data: countries, + selected: null, + isLoading: false, + error: null, + }); + }); + + it('migrates legacy quotes object to ResourceState', () => { + const quotes = { success: [], sorted: [], error: [], customActions: [] }; + const state = { + engine: { + backgroundState: { + RampsController: { + quotes, + }, + }, + }, + }; + + const result = migrate(state) as typeof state; + + expect(result.engine.backgroundState.RampsController.quotes).toStrictEqual({ + data: quotes, + selected: null, + isLoading: false, + error: null, + }); + }); + + it('wraps null tokens in ResourceState', () => { + const state = { + engine: { + backgroundState: { + RampsController: { + tokens: null, + selectedToken: null, + }, + }, + }, + }; + + const result = migrate(state) as typeof state; + + expect(result.engine.backgroundState.RampsController.tokens).toStrictEqual({ + data: null, + selected: null, + isLoading: false, + error: null, + }); + }); + + it('wraps null quotes in ResourceState', () => { + const state = { + engine: { + backgroundState: { + RampsController: { + quotes: null, + }, + }, + }, + }; + + const result = migrate(state) as typeof state; + + expect(result.engine.backgroundState.RampsController.quotes).toStrictEqual({ + data: null, + selected: null, + isLoading: false, + error: null, + }); + }); + + it('leaves userRegion untouched (handled by migration 116)', () => { + const userRegion = 'us-ca'; + const state = { + engine: { + backgroundState: { + RampsController: { + userRegion, + providers: [], + }, + }, + }, + }; + + const result = migrate(state) as typeof state; + + expect(result.engine.backgroundState.RampsController.userRegion).toBe( + userRegion, + ); + }); + + it('leaves already-ResourceState fields unchanged', () => { + const existingProviders = { + data: [], + selected: null, + isLoading: false, + error: null, + }; + const state = { + engine: { + backgroundState: { + RampsController: { + providers: existingProviders, + }, + }, + }, + }; + + const result = migrate(state) as typeof state; + + expect(result.engine.backgroundState.RampsController.providers).toBe( + existingProviders, + ); + }); +}); diff --git a/app/store/migrations/117.ts b/app/store/migrations/117.ts new file mode 100644 index 00000000000..c59fca42b7e --- /dev/null +++ b/app/store/migrations/117.ts @@ -0,0 +1,114 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { ensureValidState } from './util'; +import { captureException } from '@sentry/react-native'; + +export const migrationVersion = 117; + +function isResourceStateShape(value: unknown): boolean { + return ( + value !== null && + typeof value === 'object' && + hasProperty(value as object, 'data') && + hasProperty(value as object, 'isLoading') + ); +} + +function createDefaultResourceState( + data: TData, + selected: TSelected = null as TSelected, +): { + data: TData; + selected: TSelected; + isLoading: boolean; + error: string | null; +} { + return { + data, + selected, + isLoading: false, + error: null, + }; +} + +/** + * Migration 117: Migrate RampsController legacy state to ResourceState shape + * + * RampsController was updated to use nested ResourceState for providers, tokens, + * paymentMethods, countries, and quotes. Legacy state had e.g. providers as an + * array and selectedProvider at top level. This migration normalizes those fields + * to { data, selected, isLoading, error } and removes top-level selectedProvider, + * selectedToken, selectedPaymentMethod. userRegion is handled by migration 116. + * + * @param state - The persisted Redux state (with engine.backgroundState inflated) + * @returns The migrated Redux state + */ +export default function migrate(state: unknown): unknown { + if (!ensureValidState(state, migrationVersion)) { + return state; + } + + try { + if (!hasProperty(state.engine.backgroundState, 'RampsController')) { + return state; + } + + const ramps = state.engine.backgroundState.RampsController as Record< + string, + unknown + >; + + if (!isObject(ramps)) { + return state; + } + + const selectedProvider = hasProperty(ramps, 'selectedProvider') + ? (ramps.selectedProvider as unknown) + : null; + const selectedToken = hasProperty(ramps, 'selectedToken') + ? (ramps.selectedToken as unknown) + : null; + const selectedPaymentMethod = hasProperty(ramps, 'selectedPaymentMethod') + ? (ramps.selectedPaymentMethod as unknown) + : null; + + delete ramps.selectedProvider; + delete ramps.selectedToken; + delete ramps.selectedPaymentMethod; + + if (Array.isArray(ramps.providers)) { + ramps.providers = createDefaultResourceState( + ramps.providers, + selectedProvider ?? null, + ); + } + + if (ramps.tokens == null || !isResourceStateShape(ramps.tokens)) { + ramps.tokens = createDefaultResourceState( + ramps.tokens ?? null, + selectedToken ?? null, + ); + } + + if (Array.isArray(ramps.paymentMethods)) { + ramps.paymentMethods = createDefaultResourceState( + ramps.paymentMethods, + selectedPaymentMethod ?? null, + ); + } + + if (Array.isArray(ramps.countries)) { + ramps.countries = createDefaultResourceState(ramps.countries, null); + } + + if (ramps.quotes == null || !isResourceStateShape(ramps.quotes)) { + ramps.quotes = createDefaultResourceState(ramps.quotes ?? null, null); + } + + return state; + } catch (error) { + captureException( + new Error(`Migration ${migrationVersion} failed: ${error}`), + ); + return state; + } +} diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts index 5816fe5fcc1..ad3a4713dbf 100644 --- a/app/store/migrations/index.ts +++ b/app/store/migrations/index.ts @@ -117,6 +117,7 @@ import migration113 from './113'; import migration114 from './114'; import migration115 from './115'; import migration116 from './116'; +import migration117 from './117'; // Add migrations above this line import { ControllerStorage } from '../persistConfig'; @@ -253,6 +254,7 @@ export const migrationList: MigrationsList = { 114: migration114, 115: migration115, 116: migration116, + 117: migration117, }; // Enable both synchronous and asynchronous migrations diff --git a/app/util/logs/__snapshots__/index.test.ts.snap b/app/util/logs/__snapshots__/index.test.ts.snap index 373f7b71013..28f2fa4fc8f 100644 --- a/app/util/logs/__snapshots__/index.test.ts.snap +++ b/app/util/logs/__snapshots__/index.test.ts.snap @@ -587,14 +587,37 @@ exports[`logs :: generateStateLogs Sanitized SeedlessOnboardingController State "syncQueue": {}, }, "RampsController": { - "countries": [], - "paymentMethods": [], - "providers": [], + "countries": { + "data": [], + "error": null, + "isLoading": false, + "selected": null, + }, + "paymentMethods": { + "data": [], + "error": null, + "isLoading": false, + "selected": null, + }, + "providers": { + "data": [], + "error": null, + "isLoading": false, + "selected": null, + }, + "quotes": { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, "requests": {}, - "selectedPaymentMethod": null, - "selectedProvider": null, - "selectedToken": null, - "tokens": null, + "tokens": { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, "userRegion": null, }, "RemoteFeatureFlagController": { @@ -1382,14 +1405,37 @@ exports[`logs :: generateStateLogs generates a valid json export 1`] = ` "syncQueue": {}, }, "RampsController": { - "countries": [], - "paymentMethods": [], - "providers": [], + "countries": { + "data": [], + "error": null, + "isLoading": false, + "selected": null, + }, + "paymentMethods": { + "data": [], + "error": null, + "isLoading": false, + "selected": null, + }, + "providers": { + "data": [], + "error": null, + "isLoading": false, + "selected": null, + }, + "quotes": { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, "requests": {}, - "selectedPaymentMethod": null, - "selectedProvider": null, - "selectedToken": null, - "tokens": null, + "tokens": { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, "userRegion": null, }, "RemoteFeatureFlagController": { diff --git a/app/util/smart-transactions/smart-publish-hook.ts b/app/util/smart-transactions/smart-publish-hook.ts index c618aefeaca..369edea7480 100644 --- a/app/util/smart-transactions/smart-publish-hook.ts +++ b/app/util/smart-transactions/smart-publish-hook.ts @@ -30,7 +30,10 @@ import { addSwapsTransaction } from '../swaps/swaps-transactions'; import { Hex } from '@metamask/utils'; import { getTransactionById, isLegacyTransaction } from '../transactions'; import { ORIGIN_METAMASK } from '@metamask/controller-utils'; -import { getClientForTransactionMetadata } from '../../constants/smartTransactions'; +import { + getClientForTransactionMetadata, + sanitizeOrigin, +} from '../../constants/smartTransactions'; type AllowedActions = never; @@ -447,6 +450,7 @@ class SmartTransactionHook { signedTx.metadata = { txType: transactionMeta.type, client: getClientForTransactionMetadata(), + origin: sanitizeOrigin(transactionMeta.origin), }; } return signedTx; @@ -459,6 +463,7 @@ class SmartTransactionHook { metadata: { txType: this.#transactionMeta.type, client: getClientForTransactionMetadata(), + origin: sanitizeOrigin(this.#transactionMeta.origin), }, }, ]; @@ -472,6 +477,7 @@ class SmartTransactionHook { metadata: { txType: this.#transactionMeta.type, client: getClientForTransactionMetadata(), + origin: sanitizeOrigin(this.#transactionMeta.origin), }, })); } diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json index cd6a45e9794..db459cd2eb3 100644 --- a/app/util/test/initial-background-state.json +++ b/app/util/test/initial-background-state.json @@ -753,14 +753,37 @@ "accountMeta": {} }, "RampsController": { - "countries": [], - "paymentMethods": [], "userRegion": null, - "selectedProvider": null, - "providers": [], - "selectedPaymentMethod": null, - "selectedToken": null, - "tokens": null, + "countries": { + "data": [], + "selected": null, + "isLoading": false, + "error": null + }, + "providers": { + "data": [], + "selected": null, + "isLoading": false, + "error": null + }, + "tokens": { + "data": null, + "selected": null, + "isLoading": false, + "error": null + }, + "paymentMethods": { + "data": [], + "selected": null, + "isLoading": false, + "error": null + }, + "quotes": { + "data": null, + "selected": null, + "isLoading": false, + "error": null + }, "requests": {} } } diff --git a/app/util/transactions/hooks/delegation-7702-publish.ts b/app/util/transactions/hooks/delegation-7702-publish.ts index d106a327f33..0cf08fb9a74 100644 --- a/app/util/transactions/hooks/delegation-7702-publish.ts +++ b/app/util/transactions/hooks/delegation-7702-publish.ts @@ -40,7 +40,10 @@ import { import { NetworkClientId } from '@metamask/network-controller'; import { toHex } from '@metamask/controller-utils'; import { isE2ETest, stripSingleLeadingZero } from '../util'; -import { getClientForTransactionMetadata } from '../../../constants/smartTransactions'; +import { + getClientForTransactionMetadata, + sanitizeOrigin, +} from '../../../constants/smartTransactions'; // Test chain ID (Sepolia) used in E2E tests to match the delegation package's test contract configuration const SEPOLIA_CHAIN_ID = '0xaa36a7'; @@ -197,6 +200,7 @@ export class Delegation7702PublishHook { metadata: { txType: transactionMeta.type, client: getClientForTransactionMetadata(), + origin: sanitizeOrigin(transactionMeta.origin), }, }; diff --git a/appwright/README.md b/appwright/README.md index 0b3c71d674f..9c765728af3 100644 --- a/appwright/README.md +++ b/appwright/README.md @@ -462,6 +462,9 @@ BROWSERSTACK_IOS_CLEAN_APP_URL=bs://your-clean-ios-app-id TEST_SRP_1="your test recovery phrase 1" TEST_SRP_2="your test recovery phrase 2" TEST_SRP_3="your test recovery phrase 3" +BROWSERSTACK_USERNAME='YOUR_BS_USERNAME' +BROWSERSTACK_ACCESS_KEY='YOUR_BS_ACCESS_KEY' +E2E_PASSWORD='WALLET_PASSWORD' // 1Password # Test Passwords (can be found in 1Password) TEST_PASSWORD_LOGIN="your test password" diff --git a/appwright/config/teams-config.js b/appwright/config/teams-config.js new file mode 100644 index 00000000000..8144e6e0fe0 --- /dev/null +++ b/appwright/config/teams-config.js @@ -0,0 +1,109 @@ +/** + * Team Configuration for Performance Tests + * + * This file defines teams and their Slack group IDs for notifications. + * Tests are tagged directly using Playwright's tag syntax: + * + * test('My test', { tag: '@swap-bridge-dev-team' }, async ({ ... }) => { ... }); + * + * The slackGroupId is used to generate proper Slack mentions: + */ + +const TEAMS = { + '@swap-bridge-dev-team': { + name: 'Swap & Bridge Dev Team', + slackGroupId: null, // S04NGHK3U9Z + }, + '@metamask-onboarding-team': { + name: 'Onboarding Team', + slackGroupId: null, // S090QC71NQ2 + }, + '@metamask-mobile-platform': { + name: 'Mobile Platform Team', + slackGroupId: null, // S04EF225J1M + }, + '@mm-perps-engineering-team': { + name: 'Perps Engineering Team', + slackGroupId: null, // S094DMAQNCV + }, + '@accounts-team': { + name: 'Accounts Team', + slackGroupId: null, // S05NSFC03GF + }, + '@assets-dev-team': { + name: 'Assets Dev Team', + slackGroupId: null, // S09C9U4K953 + }, + '@team-predict': { + name: 'Predict Team', + slackGroupId: null, // S095BEYMASG + }, + '@performance-team': { + name: 'Performance Team', + slackGroupId: null, + }, +}; + +// Default team when no tag is specified +const DEFAULT_TEAM_TAG = '@performance-team'; + +/** + * Get team configuration by team tag + * @param {string} teamTag - The team tag (e.g., '@swap-bridge-dev-team') + * @returns {Object|null} Team configuration or null if not found + */ +function getTeamConfig(teamTag) { + return TEAMS[teamTag] || null; +} + +/** + * Extract team tag from test tags array + * @param {Array} tags - Array of test tags + * @returns {string} Team tag or default team tag + */ +function extractTeamTag(tags) { + if (!tags || !Array.isArray(tags)) { + return DEFAULT_TEAM_TAG; + } + + // Find the first tag that matches a team + for (const tag of tags) { + if (TEAMS[tag]) { + return tag; + } + } + + return DEFAULT_TEAM_TAG; +} + +/** + * Generate Slack mention format for a team + * @param {Object} teamConfig - Team configuration object + * @param {string} teamTag - The team tag + * @returns {string} Slack mention string or fallback to tag + */ +function generateSlackMention(teamConfig, teamTag) { + if (teamConfig?.slackGroupId) { + // Format: + const displayName = teamTag.replace('@', ''); + return ``; + } + // Fallback to just the tag if no Slack group ID + return teamTag; +} + +/** + * Get full team info from test tags + * @param {Array} tags - Array of test tags + * @returns {Object} Object containing teamId, teamName, and slackMention + */ +export function getTeamInfoFromTags(tags) { + const teamTag = extractTeamTag(tags); + const teamConfig = getTeamConfig(teamTag); + + return { + teamId: teamTag, + teamName: teamConfig?.name || teamTag, + slackMention: generateSlackMention(teamConfig, teamTag), + }; +} diff --git a/appwright/fixtures/performance-test.js b/appwright/fixtures/performance-test.js index 9e70521243d..e3184e8eb50 100644 --- a/appwright/fixtures/performance-test.js +++ b/appwright/fixtures/performance-test.js @@ -1,6 +1,7 @@ import { test as base } from 'appwright'; import { PerformanceTracker } from '../reporters/PerformanceTracker.js'; import QualityGatesValidator from '../utils/QualityGatesValidator.js'; +import { getTeamInfoFromTags } from '../config/teams-config.js'; import { markQualityGateFailure, hasQualityGateFailure, @@ -28,6 +29,15 @@ export const test = base.extend({ const performanceTracker = new PerformanceTracker(); + // Get team info from test tags (e.g., { tag: '@swap-bridge-dev-team' }) + const testTags = testInfo.tags || []; + const teamInfo = getTeamInfoFromTags(testTags); + performanceTracker.setTeamInfo(teamInfo); + + console.log( + `👥 Test assigned to team: ${teamInfo.teamName} (${teamInfo.teamId})`, + ); + // Provide the tracker to the test await use(performanceTracker); @@ -89,12 +99,16 @@ export const test = base.extend({ if (sessionId) { // Store session data as a test attachment for the reporter to find + // Include team info and tags in session data await testInfo.attach('session-data', { body: JSON.stringify({ sessionId, testTitle: testInfo.title, + testFilePath: testInfo.file || '', + tags: testTags, projectName: testInfo.project.name, timestamp: new Date().toISOString(), + team: teamInfo, }), contentType: 'application/json', }); diff --git a/appwright/reporters/PerformanceTracker.js b/appwright/reporters/PerformanceTracker.js index 0769beb52f7..7263361fc1a 100644 --- a/appwright/reporters/PerformanceTracker.js +++ b/appwright/reporters/PerformanceTracker.js @@ -4,6 +4,15 @@ import { BrowserStackCredentials } from '../utils/BrowserStackCredentials.js'; export class PerformanceTracker { constructor() { this.timers = []; + this.teamInfo = null; + } + + /** + * Set the team information for this test + * @param {Object} teamInfo - Team info object with teamId, teamName, slackId, slackMention + */ + setTeamInfo(teamInfo) { + this.teamInfo = teamInfo; } addTimers(...timers) { @@ -122,6 +131,7 @@ export class PerformanceTracker { steps: [], timestamp: new Date().toISOString(), thresholdMarginPercent: THRESHOLD_MARGIN_PERCENT, + team: this.teamInfo, // Include team info in metrics }; let totalSeconds = 0; let totalThresholdMs = 0; diff --git a/appwright/reporters/custom-reporter.js b/appwright/reporters/custom-reporter.js index 99af41c3ff6..187cd9285c2 100644 --- a/appwright/reporters/custom-reporter.js +++ b/appwright/reporters/custom-reporter.js @@ -2,6 +2,7 @@ import { PerformanceTracker } from './PerformanceTracker'; import { AppProfilingDataHandler } from './AppProfilingDataHandler'; import QualityGatesValidator from '../utils/QualityGatesValidator'; +import { getTeamInfoFromTags } from '../config/teams-config.js'; import { clearQualityGateFailures } from '../utils/QualityGateError.js'; import fs from 'fs'; import path from 'path'; @@ -12,6 +13,7 @@ class CustomReporter { this.sessions = []; // Array to store all session data this.processedTests = new Set(); // Track processed tests to avoid duplicates this.qualityGatesValidator = new QualityGatesValidator(); + this.failedTestsByTeam = {}; // Track failed tests grouped by team } // We'll skip the onStdOut and onStdErr methods since the list reporter will handle those @@ -41,7 +43,23 @@ class CustomReporter { } this.processedTests.add(testId); + // Get team info from test tags (e.g., { tag: '@swap-bridge-dev-team' }) + // Tags can be in test.tags (Playwright 1.42+) or extracted from test title annotations + let testTags = test.tags || []; + + // If tags is not an array, try to get from other sources + if (!Array.isArray(testTags)) { + testTags = []; + } + + const testFilePath = test?.location?.file || ''; + const teamInfo = getTeamInfoFromTags(testTags); + console.log(`\n🔍 Processing test: ${test.title} (${result.status})`); + console.log(`👥 Team: ${teamInfo.teamName} (${teamInfo.teamId})`); + console.log( + `🏷️ Tags: ${testTags.length > 0 ? testTags.join(', ') : 'none (using default team)'}`, + ); const sessionAttachment = result.attachments.find( (att) => att.name === 'session-data', @@ -54,6 +72,7 @@ class CustomReporter { ...sessionData, testStatus: result.status, testDuration: result.duration, + team: sessionData.team || teamInfo, // Use team from session data or fallback }); } catch (error) { console.log(`❌ Error parsing session data: ${error.message}`); @@ -73,14 +92,45 @@ class CustomReporter { this.sessions.push({ sessionId, testTitle: test.title, + testFilePath, testStatus: result.status, testDuration: result.duration, timestamp: new Date().toISOString(), + team: teamInfo, }); } } } + // Track failed tests by team for Slack notification + // Only include actual failures (failed, timedOut), not skipped or interrupted tests + const isActualFailure = + result.status === 'failed' || result.status === 'timedOut'; + if (isActualFailure) { + const teamId = teamInfo.teamId; + const sessionIdFromAnnotation = result.annotations?.find( + (a) => a.type === 'sessionId', + )?.description; + if (!this.failedTestsByTeam[teamId]) { + this.failedTestsByTeam[teamId] = { + team: teamInfo, + tests: [], + }; + } + this.failedTestsByTeam[teamId].tests.push({ + testName: test.title, + testFilePath, + tags: testTags, + status: result.status, + duration: result.duration, + projectName, + sessionId: sessionIdFromAnnotation || null, + // Will be populated later with quality gates info if available + qualityGates: null, + failureReason: null, + }); + } + // Look for metrics in the attachments (including fallback metrics) const metricsAttachment = result.attachments.find( (att) => att.name && att.name.includes('performance-metrics'), @@ -105,11 +155,13 @@ class CustomReporter { // Create metrics entry with proper handling for both regular and fallback metrics const metricsEntry = { testName: test.title, + testFilePath, + tags: testTags, ...metrics, }; - // Always mark failed tests appropriately - if (result.status !== 'passed') { + // Mark actual failures (not skipped or interrupted tests) + if (result.status === 'failed' || result.status === 'timedOut') { metricsEntry.testFailed = true; metricsEntry.failureReason = result.status; } @@ -118,6 +170,11 @@ class CustomReporter { const deviceInfo = this.getDeviceInfo(test, result); metricsEntry.device = deviceInfo; + // Ensure team info is included (from metrics or fallback) + if (!metricsEntry.team) { + metricsEntry.team = teamInfo; + } + // For fallback metrics, ensure we have proper structure for reporting if (isFallbackMetrics) { // Convert test duration to seconds if not already @@ -149,29 +206,78 @@ class CustomReporter { ), ); } + + // Update failed test entry with quality gates info if this test failed + if (metricsEntry.testFailed) { + const updates = { qualityGates: qualityGatesResult }; + if ( + qualityGatesResult.hasThresholds && + !qualityGatesResult.passed + ) { + updates.failureReason = 'quality_gates_exceeded'; + updates.qualityGatesViolations = qualityGatesResult.violations; + } else { + updates.failureReason = + metricsEntry.failureReason || 'test_error'; + } + this.updateFailedTestEntry( + teamInfo.teamId, + test.title, + projectName, + updates, + ); + } } this.metrics.push(metricsEntry); } catch (error) { console.error('Error processing metrics:', error); } - } else if (result.status !== 'passed') { - // For failed tests without metrics, create a basic entry + } else if (result.status === 'failed' || result.status === 'timedOut') { + // For actual failed tests without metrics, create a basic entry + // Skip creating entries for skipped/interrupted tests console.log(`⚠️ Test failed without metrics, creating basic entry`); const deviceInfo = this.getDeviceInfo(test, result); const basicEntry = { testName: test.title, + testFilePath, + tags: testTags, total: result.duration / 1000, device: deviceInfo, steps: [], testFailed: true, failureReason: result.status, note: 'Test failed - no performance metrics collected', + team: teamInfo, }; this.metrics.push(basicEntry); + + // Update failed test entry with failure reason (no quality gates since no metrics) + this.updateFailedTestEntry(teamInfo.teamId, test.title, projectName, { + failureReason: result.status, + }); + } + } + + /** + * Update a failed test entry with additional information + * @param {string} teamId - The team ID + * @param {string} testTitle - The test title + * @param {string} projectName - The project name + * @param {Object} updates - Object containing properties to update + */ + updateFailedTestEntry(teamId, testTitle, projectName, updates) { + if (!this.failedTestsByTeam[teamId]) { + return; + } + const failedTest = this.failedTestsByTeam[teamId].tests.find( + (t) => t.testName === testTitle && t.projectName === projectName, + ); + if (failedTest) { + Object.assign(failedTest, updates); } } @@ -208,7 +314,6 @@ class CustomReporter { }; } - // Last resort fallback return { name: 'Unknown', osVersion: 'Unknown', @@ -216,6 +321,25 @@ class CustomReporter { }; } + /** + * Try to extract device info from profiling data metadata + * @param {Object} profilingData - The profiling data object + * @returns {Object|null} Device info or null + */ + getDeviceInfoFromProfiling(profilingData) { + if ( + profilingData?.metadata?.device && + profilingData?.metadata?.os_version + ) { + return { + name: profilingData.metadata.device, + osVersion: profilingData.metadata.os_version, + provider: 'browserstack', + }; + } + return null; + } + /** * Safely access nested object properties with null/undefined protection * @param {Object} obj - The object to access @@ -310,14 +434,27 @@ class CustomReporter { ); if (videoURL) { session.videoURL = videoURL; + } else { + // Fallback: build URL from session details when getVideoURL fails (e.g. test timed out, video not ready) + const appProfilingHandlerForUrl = new AppProfilingDataHandler(); + const sessionDetails = + await appProfilingHandlerForUrl.getSessionDetails( + session.sessionId, + ); + if (sessionDetails?.buildId) { + session.videoURL = `https://app-automate.browserstack.com/builds/${sessionDetails.buildId}/sessions/${session.sessionId}`; + console.log( + `✅ Fallback: built recording URL from session details for ${session.testTitle}`, + ); + } } // Fetch profiling data from BrowserStack API + const appProfilingHandler = new AppProfilingDataHandler(); try { console.log( `🔍 Fetching profiling data for ${session.testTitle}...`, ); - const appProfilingHandler = new AppProfilingDataHandler(); const profilingResult = await appProfilingHandler.fetchCompleteProfilingData( session.sessionId, @@ -412,8 +549,26 @@ class CustomReporter { (session) => session.testTitle === metric.testName, ); - // Use device info from session if available, otherwise keep the existing device info - const deviceInfo = matchingSession?.deviceInfo || metric.device; + // Determine device info with fallbacks: + // 1. Use existing device info if valid (not Unknown) + // 2. Try to get from profiling data metadata + // 3. Keep Unknown as last resort + let deviceInfo = metric.device; + + if ( + deviceInfo?.name === 'Unknown' && + matchingSession?.profilingData + ) { + const profilingDeviceInfo = this.getDeviceInfoFromProfiling( + matchingSession.profilingData, + ); + if (profilingDeviceInfo) { + deviceInfo = profilingDeviceInfo; + console.log( + `📱 Device info recovered from profiling: ${deviceInfo.name} (${deviceInfo.osVersion})`, + ); + } + } return { ...metric, @@ -1143,6 +1298,57 @@ class CustomReporter { ); fs.writeFileSync(csvPath, csvRows.join('\n')); console.log(`✅ Performance CSV report saved: ${csvPath}`); + + // Generate failed tests by team report for Slack notifications + if (Object.keys(this.failedTestsByTeam).length > 0) { + // Normalize failureReason: if test has failed quality gates, use quality_gates_exceeded + for (const teamData of Object.values(this.failedTestsByTeam)) { + for (const test of teamData.tests) { + if ( + test.qualityGates && + test.qualityGates.hasThresholds && + !test.qualityGates.passed + ) { + test.failureReason = 'quality_gates_exceeded'; + } + } + } + + const failedTestsReport = { + timestamp: new Date().toISOString(), + totalFailedTests: Object.values(this.failedTestsByTeam).reduce( + (acc, team) => acc + team.tests.length, + 0, + ), + teamsAffected: Object.keys(this.failedTestsByTeam).length, + failedTestsByTeam: this.failedTestsByTeam, + }; + + const failedTestsPath = path.join( + reportsDir, + 'failed-tests-by-team.json', + ); + fs.writeFileSync( + failedTestsPath, + JSON.stringify(failedTestsReport, null, 2), + ); + console.log(`🚨 Failed tests by team report saved: ${failedTestsPath}`); + console.log( + ` Total failed tests: ${failedTestsReport.totalFailedTests}`, + ); + console.log(` Teams affected: ${failedTestsReport.teamsAffected}`); + + // Log which teams have failed tests + for (const [teamId, teamData] of Object.entries( + this.failedTestsByTeam, + )) { + console.log( + ` - ${teamData.team.teamName}: ${teamData.tests.length} failed test(s)`, + ); + } + } else { + console.log(`✅ No failed tests to report by team`); + } } catch (error) { console.error('Error generating performance report:', error); } diff --git a/appwright/tests/performance/login/asset-balances.spec.js b/appwright/tests/performance/login/asset-balances.spec.js index a22dee895cb..2ecb6ac29c4 100644 --- a/appwright/tests/performance/login/asset-balances.spec.js +++ b/appwright/tests/performance/login/asset-balances.spec.js @@ -9,25 +9,26 @@ import { PerformanceLogin, PerformanceAssetLoading } from '../../../tags.js'; /* Scenario: Aggregated Balance Loading Time, SRP 1 + SRP 2 + SRP 3 */ test.describe(`${PerformanceLogin} ${PerformanceAssetLoading}`, () => { - test('Aggregated Balance Loading Time, SRP 1 + SRP 2 + SRP 3', async ({ - device, - performanceTracker, - }) => { - WalletMainScreen.device = device; - TabBarModal.device = device; - LoginScreen.device = device; + test( + 'Aggregated Balance Loading Time, SRP 1 + SRP 2 + SRP 3', + { tag: '@assets-dev-team' }, + async ({ device, performanceTracker }) => { + WalletMainScreen.device = device; + TabBarModal.device = device; + LoginScreen.device = device; - await login(device); + await login(device); - const balanceStableTimer = new TimerHelper( - 'Time since the user navigates to wallet tab until the balance stabilizes', - { ios: 25000, android: 40000 }, - device, - ); - await balanceStableTimer.measure( - async () => await WalletMainScreen.waitForBalanceToStabilize(), - ); - performanceTracker.addTimer(balanceStableTimer); - // Quality gates validation is performed by the reporter when generating reports - }); -}); // End describe + const balanceStableTimer = new TimerHelper( + 'Time since the user navigates to wallet tab until the balance stabilizes', + { ios: 25000, android: 40000 }, + device, + ); + await balanceStableTimer.measure( + async () => await WalletMainScreen.waitForBalanceToStabilize(), + ); + performanceTracker.addTimer(balanceStableTimer); + // Quality gates validation is performed by the reporter when generating reports + }, + ); +}); diff --git a/appwright/tests/performance/login/asset-view.spec.js b/appwright/tests/performance/login/asset-view.spec.js index 3c824daf78f..09346c275f7 100644 --- a/appwright/tests/performance/login/asset-view.spec.js +++ b/appwright/tests/performance/login/asset-view.spec.js @@ -25,57 +25,58 @@ import { PerformanceLogin, PerformanceAssetLoading } from '../../../tags.js'; /* Scenario 8: Asset View, SRP 1 + SRP 2 + SRP 3 */ test.describe(`${PerformanceLogin} ${PerformanceAssetLoading}`, () => { - test('Asset View, SRP 1 + SRP 2 + SRP 3', async ({ - device, - performanceTracker, - }, testInfo) => { - WelcomeScreen.device = device; - TermOfUseScreen.device = device; - OnboardingScreen.device = device; - CreateNewWalletScreen.device = device; - MetaMetricsScreen.device = device; - OnboardingSucessScreen.device = device; - OnboardingSheet.device = device; - WalletAccountModal.device = device; - SkipAccountSecurityModal.device = device; - ImportFromSeedScreen.device = device; - CreatePasswordScreen.device = device; - WalletMainScreen.device = device; - AccountListComponent.device = device; - AddAccountModal.device = device; - TokenOverviewScreen.device = device; - CommonScreen.device = device; - WalletActionModal.device = device; - NetworksScreen.device = device; + test( + 'Asset View, SRP 1 + SRP 2 + SRP 3', + { tag: '@assets-dev-team' }, + async ({ device, performanceTracker }, testInfo) => { + WelcomeScreen.device = device; + TermOfUseScreen.device = device; + OnboardingScreen.device = device; + CreateNewWalletScreen.device = device; + MetaMetricsScreen.device = device; + OnboardingSucessScreen.device = device; + OnboardingSheet.device = device; + WalletAccountModal.device = device; + SkipAccountSecurityModal.device = device; + ImportFromSeedScreen.device = device; + CreatePasswordScreen.device = device; + WalletMainScreen.device = device; + AccountListComponent.device = device; + AddAccountModal.device = device; + TokenOverviewScreen.device = device; + CommonScreen.device = device; + WalletActionModal.device = device; + NetworksScreen.device = device; - LoginScreen.device = device; - WalletMainScreen.device = device; - AccountListComponent.device = device; - AddAccountModal.device = device; + LoginScreen.device = device; + WalletMainScreen.device = device; + AccountListComponent.device = device; + AddAccountModal.device = device; - TokenOverviewScreen.device = device; - CommonScreen.device = device; - WalletActionModal.device = device; - await login(device); + TokenOverviewScreen.device = device; + CommonScreen.device = device; + WalletActionModal.device = device; + await login(device); - const assetViewScreen = new TimerHelper( - 'Time since the user clicks on the asset view button until the user sees the token overview screen', - { ios: 600, android: 600 }, - device, - ); + const assetViewScreen = new TimerHelper( + 'Time since the user clicks on the asset view button until the user sees the token overview screen', + { ios: 600, android: 600 }, + device, + ); - await WalletMainScreen.tapNetworkNavBar(); - await NetworksScreen.selectNetwork('Ethereum'); + await WalletMainScreen.tapNetworkNavBar(); + await NetworksScreen.selectNetwork('Ethereum'); - await WalletMainScreen.tapOnToken('USDC'); - await assetViewScreen.measure(async () => { - await TokenOverviewScreen.isTokenOverviewVisible(); - await TokenOverviewScreen.isTodaysChangeVisible(); - await TokenOverviewScreen.isSendButtonVisible(); - }); + await WalletMainScreen.tapOnToken('USDC'); + await assetViewScreen.measure(async () => { + await TokenOverviewScreen.isTokenOverviewVisible(); + await TokenOverviewScreen.isTodaysChangeVisible(); + await TokenOverviewScreen.isSendButtonVisible(); + }); - performanceTracker.addTimer(assetViewScreen); + performanceTracker.addTimer(assetViewScreen); - await performanceTracker.attachToTest(testInfo); - }); -}); // End describe + await performanceTracker.attachToTest(testInfo); + }, + ); +}); diff --git a/appwright/tests/performance/login/cross-chain-swap-flow.spec.js b/appwright/tests/performance/login/cross-chain-swap-flow.spec.js index 0d006b0edc6..bf87155a170 100644 --- a/appwright/tests/performance/login/cross-chain-swap-flow.spec.js +++ b/appwright/tests/performance/login/cross-chain-swap-flow.spec.js @@ -16,45 +16,46 @@ import { PerformanceLogin, PerformanceSwaps } from '../../../tags.js'; /* Scenario 7: Cross-chain swap flow - ETH to SOL - 50+ accounts, SRP 1 + SRP 2 + SRP 3 */ test.describe(`${PerformanceLogin} ${PerformanceSwaps}`, () => { - test('Cross-chain swap flow - ETH to SOL - 50+ accounts, SRP 1 + SRP 2 + SRP 3', async ({ - device, - performanceTracker, - }, testInfo) => { - LoginScreen.device = device; - WalletMainScreen.device = device; - AccountListComponent.device = device; - AddAccountModal.device = device; - WalletActionModal.device = device; - TabBarModal.device = device; - WalletMainScreen.device = device; - AccountListComponent.device = device; - AddAccountModal.device = device; - NetworkEducationModal.device = device; - NetworksScreen.device = device; - BridgeScreen.device = device; - await login(device); - - const timer1 = new TimerHelper( - 'Time since the user clicks on the "Swap" button until the swap page is loaded', - { ios: 1100, android: 2200 }, - device, - ); - - await WalletMainScreen.tapSwapButton(); - await timer1.measure(() => BridgeScreen.isVisible()); - - await BridgeScreen.selectNetworkAndTokenTo('Solana', 'SOL'); - await BridgeScreen.enterSourceTokenAmount('1'); - - const timer2 = new TimerHelper( - 'Time since the user enters the amount until the quote is displayed', - { ios: 9000, android: 7000 }, - device, - ); - - await timer2.measure(() => BridgeScreen.isQuoteDisplayed()); - - performanceTracker.addTimers(timer1, timer2); - await performanceTracker.attachToTest(testInfo); - }); -}); // End describe + test( + 'Cross-chain swap flow - ETH to SOL - 50+ accounts, SRP 1 + SRP 2 + SRP 3', + { tag: '@swap-bridge-dev-team' }, + async ({ device, performanceTracker }, testInfo) => { + LoginScreen.device = device; + WalletMainScreen.device = device; + AccountListComponent.device = device; + AddAccountModal.device = device; + WalletActionModal.device = device; + TabBarModal.device = device; + WalletMainScreen.device = device; + AccountListComponent.device = device; + AddAccountModal.device = device; + NetworkEducationModal.device = device; + NetworksScreen.device = device; + BridgeScreen.device = device; + await login(device); + + const timer1 = new TimerHelper( + 'Time since the user clicks on the "Swap" button until the swap page is loaded', + { ios: 1100, android: 2200 }, + device, + ); + + await WalletMainScreen.tapSwapButton(); + await timer1.measure(() => BridgeScreen.isVisible()); + + await BridgeScreen.selectNetworkAndTokenTo('Solana', 'SOL'); + await BridgeScreen.enterSourceTokenAmount('1'); + + const timer2 = new TimerHelper( + 'Time since the user enters the amount until the quote is displayed', + { ios: 9000, android: 7000 }, + device, + ); + + await timer2.measure(() => BridgeScreen.isQuoteDisplayed()); + + performanceTracker.addTimers(timer1, timer2); + await performanceTracker.attachToTest(testInfo); + }, + ); +}); diff --git a/appwright/tests/performance/login/eth-swap-flow.spec.js b/appwright/tests/performance/login/eth-swap-flow.spec.js index dc4b60d092e..84f06e06f96 100644 --- a/appwright/tests/performance/login/eth-swap-flow.spec.js +++ b/appwright/tests/performance/login/eth-swap-flow.spec.js @@ -13,43 +13,44 @@ import { PerformanceLogin, PerformanceSwaps } from '../../../tags.js'; /* Scenario 6: Swap flow - ETH to LINK, SRP 1 + SRP 2 + SRP 3 */ test.describe(`${PerformanceLogin} ${PerformanceSwaps}`, () => { - test('Swap flow - ETH to LINK, SRP 1 + SRP 2 + SRP 3', async ({ - device, - performanceTracker, - }, testInfo) => { - LoginScreen.device = device; - - WalletMainScreen.device = device; - AccountListComponent.device = device; - AddAccountModal.device = device; - WalletActionModal.device = device; - TabBarModal.device = device; - WalletMainScreen.device = device; - AccountListComponent.device = device; - AddAccountModal.device = device; - BridgeScreen.device = device; - await login(device); - - const swapLoadTimer = new TimerHelper( - 'Time since the user clicks on the "Swap" button until the swap page is loaded', - { ios: 2000, android: 2500 }, - device, - ); - - await WalletMainScreen.tapSwapButton(); - await swapLoadTimer.measure(() => BridgeScreen.isVisible()); - - const swapTimer = new TimerHelper( - 'Time since the user enters the amount until the quote is displayed', - { ios: 9000, android: 7000 }, - device, - ); - await BridgeScreen.selectNetworkAndTokenTo('Ethereum', 'LINK'); - await BridgeScreen.enterSourceTokenAmount('1'); - - await swapTimer.measure(() => BridgeScreen.isQuoteDisplayed()); - - performanceTracker.addTimers(swapLoadTimer, swapTimer); - await performanceTracker.attachToTest(testInfo); - }); -}); // End describe + test( + 'Swap flow - ETH to LINK, SRP 1 + SRP 2 + SRP 3', + { tag: '@swap-bridge-dev-team' }, + async ({ device, performanceTracker }, testInfo) => { + LoginScreen.device = device; + + WalletMainScreen.device = device; + AccountListComponent.device = device; + AddAccountModal.device = device; + WalletActionModal.device = device; + TabBarModal.device = device; + WalletMainScreen.device = device; + AccountListComponent.device = device; + AddAccountModal.device = device; + BridgeScreen.device = device; + await login(device); + + const swapLoadTimer = new TimerHelper( + 'Time since the user clicks on the "Swap" button until the swap page is loaded', + { ios: 2000, android: 2500 }, + device, + ); + + await WalletMainScreen.tapSwapButton(); + await swapLoadTimer.measure(() => BridgeScreen.isVisible()); + + const swapTimer = new TimerHelper( + 'Time since the user enters the amount until the quote is displayed', + { ios: 9000, android: 7000 }, + device, + ); + await BridgeScreen.selectNetworkAndTokenTo('Ethereum', 'LINK'); + await BridgeScreen.enterSourceTokenAmount('1'); + + await swapTimer.measure(() => BridgeScreen.isQuoteDisplayed()); + + performanceTracker.addTimers(swapLoadTimer, swapTimer); + await performanceTracker.attachToTest(testInfo); + }, + ); +}); diff --git a/appwright/tests/performance/login/import-multiple-srps.spec.js b/appwright/tests/performance/login/import-multiple-srps.spec.js index 3272fddac47..8429cffc329 100644 --- a/appwright/tests/performance/login/import-multiple-srps.spec.js +++ b/appwright/tests/performance/login/import-multiple-srps.spec.js @@ -17,21 +17,22 @@ import { PerformanceLogin, PerformanceAccountList } from '../../../tags.js'; /* Scenario 4: Import SRP with +50 accounts, SRP 1, SRP 2, SRP 3 */ test.describe(`${PerformanceLogin} ${PerformanceAccountList}`, () => { - test('Import SRP with +50 accounts, SRP 1, SRP 2, SRP 3', async ({ - device, - performanceTracker, - }) => { - LoginScreen.device = device; - WalletMainScreen.device = device; - AccountListComponent.device = device; - AddAccountModal.device = device; - WalletActionModal.device = device; - TabBarModal.device = device; - test.setTimeout(1800000); - await login(device); + test( + 'Import SRP with +50 accounts, SRP 1, SRP 2, SRP 3', + { tag: '@accounts-team' }, + async ({ device, performanceTracker }) => { + LoginScreen.device = device; + WalletMainScreen.device = device; + AccountListComponent.device = device; + AddAccountModal.device = device; + WalletActionModal.device = device; + TabBarModal.device = device; + test.setTimeout(1800000); + await login(device); - const timers = await importSRPFlow(device, process.env.TEST_SRP_2, false); - timers.forEach((timer) => performanceTracker.addTimer(timer)); - // Quality gates validation is performed by the reporter when generating reports - }); -}); // End describe + const timers = await importSRPFlow(device, process.env.TEST_SRP_2, false); + timers.forEach((timer) => performanceTracker.addTimer(timer)); + // Quality gates validation is performed by the reporter when generating reports + }, + ); +}); diff --git a/appwright/tests/performance/login/launch-times/cold-start-to-login.spec.js b/appwright/tests/performance/login/launch-times/cold-start-to-login.spec.js index 57af1660eac..3b69d9869f2 100644 --- a/appwright/tests/performance/login/launch-times/cold-start-to-login.spec.js +++ b/appwright/tests/performance/login/launch-times/cold-start-to-login.spec.js @@ -23,41 +23,42 @@ import LoginScreen from '../../../../../wdio/screen-objects/LoginScreen.js'; import { PerformanceLogin, PerformanceLaunch } from '../../../../tags.js'; test.describe(`${PerformanceLogin} ${PerformanceLaunch}`, () => { - test('Cold Start: Measure ColdStart To Login Screen', async ({ - device, - performanceTracker, - }, testInfo) => { - WelcomeScreen.device = device; - TermOfUseScreen.device = device; - OnboardingScreen.device = device; - CreateNewWalletScreen.device = device; - MetaMetricsScreen.device = device; - OnboardingSucessScreen.device = device; - OnboardingSheet.device = device; - WalletAccountModal.device = device; - SkipAccountSecurityModal.device = device; - ImportFromSeedScreen.device = device; - CreatePasswordScreen.device = device; - WalletMainScreen.device = device; - AccountListComponent.device = device; - AddAccountModal.device = device; - WalletActionModal.device = device; - ConfirmationScreen.device = device; - AmountScreen.device = device; - LoginScreen.device = device; - await login(device); - await WalletMainScreen.waitForBalanceToStabilize(); - // await importSRPFlow(device, process.env.TEST_SRP_2); - // await importSRPFlow(device, process.env.TEST_SRP_3); - await AppwrightGestures.terminateApp(device); - await AppwrightGestures.activateApp(device); - const timer1 = new TimerHelper( - 'Time since the the app is launched, until login screen appears', - { ios: 1000, android: 3000 }, - device, - ); - await timer1.measure(() => LoginScreen.waitForScreenToDisplay()); - performanceTracker.addTimer(timer1); - await performanceTracker.attachToTest(testInfo); - }); -}); // End describe + test( + 'Cold Start: Measure ColdStart To Login Screen', + { tag: '@metamask-mobile-platform' }, + async ({ device, performanceTracker }, testInfo) => { + WelcomeScreen.device = device; + TermOfUseScreen.device = device; + OnboardingScreen.device = device; + CreateNewWalletScreen.device = device; + MetaMetricsScreen.device = device; + OnboardingSucessScreen.device = device; + OnboardingSheet.device = device; + WalletAccountModal.device = device; + SkipAccountSecurityModal.device = device; + ImportFromSeedScreen.device = device; + CreatePasswordScreen.device = device; + WalletMainScreen.device = device; + AccountListComponent.device = device; + AddAccountModal.device = device; + WalletActionModal.device = device; + ConfirmationScreen.device = device; + AmountScreen.device = device; + LoginScreen.device = device; + await login(device); + await WalletMainScreen.waitForBalanceToStabilize(); + // await importSRPFlow(device, process.env.TEST_SRP_2); + // await importSRPFlow(device, process.env.TEST_SRP_3); + await AppwrightGestures.terminateApp(device); + await AppwrightGestures.activateApp(device); + const timer1 = new TimerHelper( + 'Time since the the app is launched, until login screen appears', + { ios: 3000, android: 3000 }, + device, + ); + await timer1.measure(() => LoginScreen.waitForScreenToDisplay()); + performanceTracker.addTimer(timer1); + await performanceTracker.attachToTest(testInfo); + }, + ); +}); diff --git a/appwright/tests/performance/login/launch-times/warm-start-login-to-wallet.spec.skip.js b/appwright/tests/performance/login/launch-times/warm-start-login-to-wallet.spec.skip.js index ed5b455cf12..3d589cb8005 100644 --- a/appwright/tests/performance/login/launch-times/warm-start-login-to-wallet.spec.skip.js +++ b/appwright/tests/performance/login/launch-times/warm-start-login-to-wallet.spec.skip.js @@ -15,23 +15,23 @@ import { PerformanceLogin, PerformanceLaunch } from '../../../../tags.js'; // There is a bug in this flow specifically on the samsung s23 device. test.describe(`${PerformanceLogin} ${PerformanceLaunch}`, () => { - test.skip('Measure Warm Start: Login To Wallet Screen', async ({ - device, - performanceTracker, - }, testInfo) => { - AddressBarScreen.device = device; - BrowserScreen.device = device; - TabBarModal.device = device; - LoginScreen.device = device; - WalletMainScreen.device = device; - ExternalWebsitesScreen.device = device; - AccountApprovalModal.device = device; + test.skip( + 'Measure Warm Start: Login To Wallet Screen', + { tag: '@metamask-mobile-platform' }, + async ({ device, performanceTracker }, testInfo) => { + AddressBarScreen.device = device; + BrowserScreen.device = device; + TabBarModal.device = device; + LoginScreen.device = device; + WalletMainScreen.device = device; + ExternalWebsitesScreen.device = device; + AccountApprovalModal.device = device; - await login(device); + await login(device); - await TabBarModal.tapBrowserButton(); + await TabBarModal.tapBrowserButton(); - /* + /* These steps are too flaky. Commenting out for now. // await BrowserScreen.isScreenContentDisplayed(); @@ -47,20 +47,21 @@ test.describe(`${PerformanceLogin} ${PerformanceLaunch}`, () => { // await AccountApprovalModal.tapConnectButtonByText(); // console.log('Waiting for 30 seconds'); */ - await TabBarModal.tapWalletButton(); - await AppwrightGestures.backgroundApp(device, 30); - await AppwrightGestures.activateApp(device); - await LoginScreen.waitForScreenToDisplay(); - await login(device); + await TabBarModal.tapWalletButton(); + await AppwrightGestures.backgroundApp(device, 30); + await AppwrightGestures.activateApp(device); + await LoginScreen.waitForScreenToDisplay(); + await login(device); - const timer1 = new TimerHelper( - 'Time since the user clicks on unlock button, until the app unlocks', - { ios: 1000, android: 1000 }, - device, - ); - await timer1.measure(() => WalletMainScreen.isVisible()); + const timer1 = new TimerHelper( + 'Time since the user clicks on unlock button, until the app unlocks', + { ios: 1000, android: 1000 }, + device, + ); + await timer1.measure(() => WalletMainScreen.isVisible()); - performanceTracker.addTimer(timer1); - await performanceTracker.attachToTest(testInfo); - }); -}); // End describe + performanceTracker.addTimer(timer1); + await performanceTracker.attachToTest(testInfo); + }, + ); +}); diff --git a/appwright/tests/performance/login/launch-times/warm-start-to-login.spec.skip.js b/appwright/tests/performance/login/launch-times/warm-start-to-login.spec.skip.js index 95ba864e296..019cc5674bc 100644 --- a/appwright/tests/performance/login/launch-times/warm-start-to-login.spec.skip.js +++ b/appwright/tests/performance/login/launch-times/warm-start-to-login.spec.skip.js @@ -18,32 +18,33 @@ import { PerformanceLogin, PerformanceLaunch } from '../../../../tags.js'; // There is a bug in this flow specifically on the samsung s23 device. test.describe(`${PerformanceLogin} ${PerformanceLaunch}`, () => { - test('Measure Warm Start: Warm Start to Login Screen', async ({ - device, - performanceTracker, - }, testInfo) => { - AddressBarScreen.device = device; - BrowserScreen.device = device; - TabBarModal.device = device; - LoginScreen.device = device; - WalletMainScreen.device = device; - ExternalWebsitesScreen.device = device; - AccountApprovalModal.device = device; + test( + 'Measure Warm Start: Warm Start to Login Screen', + { tag: '@metamask-mobile-platform' }, + async ({ device, performanceTracker }, testInfo) => { + AddressBarScreen.device = device; + BrowserScreen.device = device; + TabBarModal.device = device; + LoginScreen.device = device; + WalletMainScreen.device = device; + ExternalWebsitesScreen.device = device; + AccountApprovalModal.device = device; - await login(device); + await login(device); - const timer1 = new TimerHelper( - 'Time since the user open the app again and the login screen appears', - { ios: 1500, android: 1500 }, - device, - ); - await AppwrightGestures.backgroundApp(device, 30); - await AppwrightGestures.activateApp(device); - await timer1.measure(async () => { - await LoginScreen.waitForScreenToDisplay(); - }); + const timer1 = new TimerHelper( + 'Time since the user open the app again and the login screen appears', + { ios: 1500, android: 1500 }, + device, + ); + await AppwrightGestures.backgroundApp(device, 30); + await AppwrightGestures.activateApp(device); + await timer1.measure(async () => { + await LoginScreen.waitForScreenToDisplay(); + }); - performanceTracker.addTimer(timer1); - await performanceTracker.attachToTest(testInfo); - }); -}); // End describe + performanceTracker.addTimer(timer1); + await performanceTracker.attachToTest(testInfo); + }, + ); +}); diff --git a/appwright/tests/performance/login/perps-add-funds.spec.js b/appwright/tests/performance/login/perps-add-funds.spec.js index a65dc31b221..d4454b42c77 100644 --- a/appwright/tests/performance/login/perps-add-funds.spec.js +++ b/appwright/tests/performance/login/perps-add-funds.spec.js @@ -30,55 +30,59 @@ async function screensSetup(device) { /* Scenario 5: Perps add funds */ test.describe(PerformancePreps, () => { - test('Perps add funds', async ({ device, performanceTracker }, testInfo) => { - test.setTimeout(10 * 60 * 1000); // 10 minutes + test( + 'Perps add funds', + { tag: '@mm-perps-engineering-team' }, + async ({ device, performanceTracker }, testInfo) => { + test.setTimeout(10 * 60 * 1000); // 10 minutes - const selectPerpsMainScreenTimer = new TimerHelper( - 'Select Perps Main Screen', - { ios: 1500, android: 2500 }, - device, - ); - const openAddFundsTimer = new TimerHelper( - 'Open Add Funds', - { ios: 5000, android: 4500 }, - device, - ); - const getQuoteTimer = new TimerHelper( - 'Get Quote', - { ios: 6000, android: 7000 }, - device, - ); - await screensSetup(device); + const selectPerpsMainScreenTimer = new TimerHelper( + 'Select Perps Main Screen', + { ios: 1500, android: 2500 }, + device, + ); + const openAddFundsTimer = new TimerHelper( + 'Open Add Funds', + { ios: 5000, android: 4500 }, + device, + ); + const getQuoteTimer = new TimerHelper( + 'Get Quote', + { ios: 6000, android: 7000 }, + device, + ); + await screensSetup(device); - await login(device); - await TabBarModal.tapActionButton(); + await login(device); + await TabBarModal.tapActionButton(); - // Open Perps Main Screen - await selectPerpsMainScreenTimer.measure(() => - WalletActionModal.tapPerpsButton(), - ); + // Open Perps Main Screen + await selectPerpsMainScreenTimer.measure(() => + WalletActionModal.tapPerpsButton(), + ); - // Skip tutorial - await PerpsTutorialScreen.tapSkip(); + // Skip tutorial + await PerpsTutorialScreen.tapSkip(); - await PerpsTutorialScreen.tapAddFunds(); - // Open Add Funds flow - await openAddFundsTimer.measure(async () => { - await PerpsDepositScreen.isAmountInputVisible(); - }); + await PerpsTutorialScreen.tapAddFunds(); + // Open Add Funds flow + await openAddFundsTimer.measure(async () => { + await PerpsDepositScreen.isAmountInputVisible(); + }); - await PerpsDepositScreen.fillUsdAmount(5); - // Get quote - await getQuoteTimer.measure(async () => { - await PerpsDepositScreen.isAddFundsVisible(); - await PerpsDepositScreen.isTotalVisible(); - }); + await PerpsDepositScreen.fillUsdAmount(5); + // Get quote + await getQuoteTimer.measure(async () => { + await PerpsDepositScreen.isAddFundsVisible(); + await PerpsDepositScreen.isTotalVisible(); + }); - performanceTracker.addTimers( - selectPerpsMainScreenTimer, - openAddFundsTimer, - getQuoteTimer, - ); - await performanceTracker.attachToTest(testInfo); - }); -}); // End describe + performanceTracker.addTimers( + selectPerpsMainScreenTimer, + openAddFundsTimer, + getQuoteTimer, + ); + await performanceTracker.attachToTest(testInfo); + }, + ); +}); diff --git a/appwright/tests/performance/login/perps-position-management.spec.js b/appwright/tests/performance/login/perps-position-management.spec.js index 0175ff20979..48bb64cfd62 100644 --- a/appwright/tests/performance/login/perps-position-management.spec.js +++ b/appwright/tests/performance/login/perps-position-management.spec.js @@ -42,93 +42,94 @@ async function screensSetup(device) { /* Scenario 5: Perps onboarding + add funds 10 USD ARB.USDC + Open Position + Close Position */ test.describe(PerformancePreps, () => { - test('Perps open position and close it', async ({ - device, - performanceTracker, - }, testInfo) => { - test.setTimeout(10 * 60 * 1000); // 10 minutes - - const selectPerpsMainScreenTimer = new TimerHelper( - 'Perps tutorial screen visible', - { ios: 2000, android: 2000 }, - device, - ); - - const selectMarketTimer = new TimerHelper( - 'Market list screen visible', - { ios: 7500, android: 7500 }, - device, - ); - const openOrderScreenTimer = new TimerHelper( - 'Open Order Screen', - { ios: 1500, android: 1500 }, - device, - ); - const openPositionTimer = new TimerHelper( - 'Position opened', - { ios: 10500, android: 20000 }, - device, - ); - - const MarketDetailsScreenTimer = new TimerHelper( - 'Market Details Screen', - { ios: 10000, android: 10000 }, - device, - ); - - await screensSetup(device); - await login(device); - - // Perps requires independent account for each device to avoid clashes when running tests in parallel - await selectAccountDevice(device, testInfo); - - await TabBarModal.tapActionButton(); - await WalletActionModal.tapPerpsButton(); - await selectPerpsMainScreenTimer.measure(async () => { - await PerpsTutorialScreen.isContainerDisplayed(); - }); - - await PerpsTutorialScreen.tapSkip(); - await selectMarketTimer.measure(async () => { - await PerpsMarketListView.isHeaderVisible(); - }); - - await PerpsMarketListView.selectMarket('BTC'); - - await MarketDetailsScreenTimer.measure( - async () => await PerpsPositionDetailsView.isContainerDisplayed(), - ); - // Check if there's an existing position and close it before continuing - if (await PerpsPositionDetailsView.isPositionOpen()) { - console.log( - '⚠️ Position already open, closing it before continuing with the test...', + test( + 'Perps open position and close it', + { tag: '@mm-perps-engineering-team' }, + async ({ device, performanceTracker }, testInfo) => { + test.setTimeout(10 * 60 * 1000); // 10 minutes + + const selectPerpsMainScreenTimer = new TimerHelper( + 'Perps tutorial screen visible', + { ios: 2000, android: 2000 }, + device, ); + + const selectMarketTimer = new TimerHelper( + 'Market list screen visible', + { ios: 7500, android: 7500 }, + device, + ); + const openOrderScreenTimer = new TimerHelper( + 'Open Order Screen', + { ios: 1500, android: 1500 }, + device, + ); + const openPositionTimer = new TimerHelper( + 'Position opened', + { ios: 10500, android: 20000 }, + device, + ); + + const MarketDetailsScreenTimer = new TimerHelper( + 'Market Details Screen', + { ios: 10000, android: 10000 }, + device, + ); + + await screensSetup(device); + await login(device); + + // Perps requires independent account for each device to avoid clashes when running tests in parallel + await selectAccountDevice(device, testInfo); + + await TabBarModal.tapActionButton(); + await WalletActionModal.tapPerpsButton(); + await selectPerpsMainScreenTimer.measure(async () => { + await PerpsTutorialScreen.isContainerDisplayed(); + }); + + await PerpsTutorialScreen.tapSkip(); + await selectMarketTimer.measure(async () => { + await PerpsMarketListView.isHeaderVisible(); + }); + + await PerpsMarketListView.selectMarket('BTC'); + + await MarketDetailsScreenTimer.measure( + async () => await PerpsPositionDetailsView.isContainerDisplayed(), + ); + // Check if there's an existing position and close it before continuing + if (await PerpsPositionDetailsView.isPositionOpen()) { + console.log( + '⚠️ Position already open, closing it before continuing with the test...', + ); + await PerpsPositionDetailsView.closePositionWithRetry(); + console.log('✅ Existing position closed successfully'); + } + + await PerpsMarketDetailsView.tapLongButton(); + // Open Position + await openOrderScreenTimer.measure(async () => + PerpsOrderView.checkOrderScreenVisible(), + ); + + await PerpsOrderView.setLeverage(40); + await PerpsOrderView.tapPlaceOrder(); + + await openPositionTimer.measure( + async () => await PerpsPositionDetailsView.isPositionOpen(), + ); + await PerpsPositionDetailsView.closePositionWithRetry(); - console.log('✅ Existing position closed successfully'); - } - - await PerpsMarketDetailsView.tapLongButton(); - // Open Position - await openOrderScreenTimer.measure(async () => - PerpsOrderView.checkOrderScreenVisible(), - ); - - await PerpsOrderView.setLeverage(40); - await PerpsOrderView.tapPlaceOrder(); - - await openPositionTimer.measure( - async () => await PerpsPositionDetailsView.isPositionOpen(), - ); - - await PerpsPositionDetailsView.closePositionWithRetry(); - - performanceTracker.addTimers( - selectPerpsMainScreenTimer, - selectMarketTimer, - openOrderScreenTimer, - openPositionTimer, - MarketDetailsScreenTimer, - ); - await performanceTracker.attachToTest(testInfo); - }); -}); // End describe + + performanceTracker.addTimers( + selectPerpsMainScreenTimer, + selectMarketTimer, + openOrderScreenTimer, + openPositionTimer, + MarketDetailsScreenTimer, + ); + await performanceTracker.attachToTest(testInfo); + }, + ); +}); diff --git a/appwright/tests/performance/login/predict/predict-available-balance.spec.js b/appwright/tests/performance/login/predict/predict-available-balance.spec.js index 03fb0445ed1..f9117ff7472 100644 --- a/appwright/tests/performance/login/predict/predict-available-balance.spec.js +++ b/appwright/tests/performance/login/predict/predict-available-balance.spec.js @@ -22,42 +22,43 @@ import { PerformancePredict } from '../../../../tags.js'; * 1. Total time from tapping Predict button until available balance is displayed */ test.describe(PerformancePredict, () => { - test('Predict Available Balance - Load Time Performance', async ({ - device, - performanceTracker, - }, testInfo) => { - // Setup screen objects with device - LoginScreen.device = device; - WalletMainScreen.device = device; - TabBarModal.device = device; - WalletActionModal.device = device; - PredictMarketListScreen.device = device; + test( + 'Predict Available Balance - Load Time Performance', + { tag: '@team-predict' }, + async ({ device, performanceTracker }, testInfo) => { + // Setup screen objects with device + LoginScreen.device = device; + WalletMainScreen.device = device; + TabBarModal.device = device; + WalletActionModal.device = device; + PredictMarketListScreen.device = device; - // Login to the app - await login(device); - await TabBarModal.tapActionButton(); - await WalletActionModal.tapPredictButton(); + // Login to the app + await login(device); + await TabBarModal.tapActionButton(); + await WalletActionModal.tapPredictButton(); - // Timer 1: Navigate to Predict tab and wait for available balance to load - const timer1 = new TimerHelper( - 'Time since user taps Predict button in Action modal until Available Balance is displayed', - { ios: 4500, android: 8000 }, - device, - ); - timer1.start(); - await PredictMarketListScreen.isBalanceCardDisplayed(); - await PredictMarketListScreen.isAvailableBalanceDisplayed(); - timer1.stop(); + // Timer 1: Navigate to Predict tab and wait for available balance to load + const timer1 = new TimerHelper( + 'Time since user taps Predict button in Action modal until Available Balance is displayed', + { ios: 4500, android: 8000 }, + device, + ); + timer1.start(); + await PredictMarketListScreen.isBalanceCardDisplayed(); + await PredictMarketListScreen.isAvailableBalanceDisplayed(); + timer1.stop(); - // Add timer to performance tracker - await performanceTracker.addTimer(timer1); + // Add timer to performance tracker + await performanceTracker.addTimer(timer1); - // Attach performance metrics to test report - await performanceTracker.attachToTest(testInfo); + // Attach performance metrics to test report + await performanceTracker.attachToTest(testInfo); - console.log('✅ Predict Available Balance Performance Test completed'); - console.log( - `📊 Total Time to Available Balance: ${timer1.getDuration()}ms`, - ); - }); -}); // End describe + console.log('✅ Predict Available Balance Performance Test completed'); + console.log( + `📊 Total Time to Available Balance: ${timer1.getDuration()}ms`, + ); + }, + ); +}); diff --git a/appwright/tests/performance/login/predict/predict-deposit.spec.js b/appwright/tests/performance/login/predict/predict-deposit.spec.js index dabfe5bffb0..c3bd3e5145f 100644 --- a/appwright/tests/performance/login/predict/predict-deposit.spec.js +++ b/appwright/tests/performance/login/predict/predict-deposit.spec.js @@ -29,104 +29,105 @@ import { PerformancePredict } from '../../../../tags.js'; * 6. Time to verify deposit info (fees, amount) appears */ test.describe(PerformancePredict, () => { - test('Predict Deposit - Complete Flow Performance', async ({ - device, - performanceTracker, - }, testInfo) => { - // Setup screen objects with device - LoginScreen.device = device; - WalletMainScreen.device = device; - TabBarModal.device = device; - WalletActionModal.device = device; - PredictMarketListScreen.device = device; - PredictDepositScreen.device = device; - PredictConfirmationScreen.device = device; + test( + 'Predict Deposit - Complete Flow Performance', + { tag: '@team-predict' }, + async ({ device, performanceTracker }, testInfo) => { + // Setup screen objects with device + LoginScreen.device = device; + WalletMainScreen.device = device; + TabBarModal.device = device; + WalletActionModal.device = device; + PredictMarketListScreen.device = device; + PredictDepositScreen.device = device; + PredictConfirmationScreen.device = device; - // Login to the app - await login(device); - await TabBarModal.tapActionButton(); + // Login to the app + await login(device); + await TabBarModal.tapActionButton(); - // Timer 1: Navigate to Predict tab - const timer1 = new TimerHelper( - 'Time since user taps Predict button until Predict Market List is displayed', - { ios: 2300, android: 2600 }, - device, - ); - await timer1.measure(async () => { - await WalletActionModal.tapPredictButton(); - await PredictMarketListScreen.isContainerDisplayed(); - }); + // Timer 1: Navigate to Predict tab + const timer1 = new TimerHelper( + 'Time since user taps Predict button until Predict Market List is displayed', + { ios: 2300, android: 2600 }, + device, + ); + await timer1.measure(async () => { + await WalletActionModal.tapPredictButton(); + await PredictMarketListScreen.isContainerDisplayed(); + }); - // Timer 2: Open deposit screen - const timer2 = new TimerHelper( - 'Time since user taps Add Funds button until Deposit screen is displayed', - { ios: 7000, android: 12000 }, - device, - ); - await timer2.measure(async () => { - await PredictMarketListScreen.tapAddFundsButton(); - await PredictDepositScreen.isAmountInputVisible(); - }); + // Timer 2: Open deposit screen + const timer2 = new TimerHelper( + 'Time since user taps Add Funds button until Deposit screen is displayed', + { ios: 7000, android: 12000 }, + device, + ); + await timer2.measure(async () => { + await PredictMarketListScreen.tapAddFundsButton(); + await PredictDepositScreen.isAmountInputVisible(); + }); - // Timer 3: Change default asset - const timer3 = new TimerHelper( - 'Time since user taps Pay With button until select payment method modal appears', - { ios: 4500, android: 2900 }, - device, - ); - await timer3.measure(async () => { - await PredictDepositScreen.tapPayWith(); - // Wait for asset selection modal to appear - await PredictDepositScreen.isSelectPaymentVisible(); - }); + // Timer 3: Change default asset + const timer3 = new TimerHelper( + 'Time since user taps Pay With button until select payment method modal appears', + { ios: 4500, android: 2900 }, + device, + ); + await timer3.measure(async () => { + await PredictDepositScreen.tapPayWith(); + // Wait for asset selection modal to appear + await PredictDepositScreen.isSelectPaymentVisible(); + }); - // Timer 4: Search, select and enter amount for asseet to pay - const timer4 = new TimerHelper( - 'Time user search, select and enter amount for asseet to pay', - { ios: 15000, android: 12000 }, - device, - ); - await timer4.measure(async () => { - // Search for USDC and select the first visible option - await PredictDepositScreen.searchToken('USDC'); - await PredictDepositScreen.tapEthereumFilter(); - await PredictDepositScreen.tapFirstUsdc('USDC'); - await PredictDepositScreen.fillUsdAmount('1'); - }); + // Timer 4: Search, select and enter amount for asseet to pay + const timer4 = new TimerHelper( + 'Time user search, select and enter amount for asseet to pay', + { ios: 15000, android: 12000 }, + device, + ); + await timer4.measure(async () => { + // Search for USDC and select the first visible option + await PredictDepositScreen.searchToken('USDC'); + await PredictDepositScreen.tapEthereumFilter(); + await PredictDepositScreen.tapFirstUsdc('USDC'); + await PredictDepositScreen.fillUsdAmount('1'); + }); - // Timer 5: Proceed to confirmation screen - const timer5 = new TimerHelper( - 'Time since user taps Continue until Confirmation screen is displayed', - { ios: 4800, android: 9000 }, - device, - ); - await timer5.measure(async () => { - await PredictDepositScreen.tapContinue(); - await PredictConfirmationScreen.verifyDepositAmount('1'); - await PredictConfirmationScreen.verifyFeesDisplayed(); - }); + // Timer 5: Proceed to confirmation screen + const timer5 = new TimerHelper( + 'Time since user taps Continue until Confirmation screen is displayed', + { ios: 4800, android: 9000 }, + device, + ); + await timer5.measure(async () => { + await PredictDepositScreen.tapContinue(); + await PredictConfirmationScreen.verifyDepositAmount('1'); + await PredictConfirmationScreen.verifyFeesDisplayed(); + }); - // Add all timers to performance tracker - performanceTracker.addTimers(timer1, timer2, timer3, timer4, timer5); + // Add all timers to performance tracker + performanceTracker.addTimers(timer1, timer2, timer3, timer4, timer5); - // Attach performance metrics to test report - await performanceTracker.attachToTest(testInfo); + // Attach performance metrics to test report + await performanceTracker.attachToTest(testInfo); - console.log('✅ Predict Deposit Performance Test completed'); - console.log(`📊 Navigate to Predict: ${timer1.getDuration()}ms`); - console.log(`📊 Open Deposit Screen: ${timer2.getDuration()}ms`); - console.log(`📊 Change Asset: ${timer3.getDuration()}ms`); - console.log(`📊 Enter Amount: ${timer4.getDuration()}ms`); - console.log(`📊 Open Confirmation: ${timer5.getDuration()}ms`); + console.log('✅ Predict Deposit Performance Test completed'); + console.log(`📊 Navigate to Predict: ${timer1.getDuration()}ms`); + console.log(`📊 Open Deposit Screen: ${timer2.getDuration()}ms`); + console.log(`📊 Change Asset: ${timer3.getDuration()}ms`); + console.log(`📊 Enter Amount: ${timer4.getDuration()}ms`); + console.log(`📊 Open Confirmation: ${timer5.getDuration()}ms`); - console.log( - `📊 Total Time: ${ - timer1.getDuration() + - timer2.getDuration() + - timer3.getDuration() + - timer4.getDuration() + - timer5.getDuration() - }ms`, - ); - }); -}); // End describe + console.log( + `📊 Total Time: ${ + timer1.getDuration() + + timer2.getDuration() + + timer3.getDuration() + + timer4.getDuration() + + timer5.getDuration() + }ms`, + ); + }, + ); +}); diff --git a/appwright/tests/performance/login/predict/predict-market-details.spec.js b/appwright/tests/performance/login/predict/predict-market-details.spec.js index 9ce98389a1a..913520399c8 100644 --- a/appwright/tests/performance/login/predict/predict-market-details.spec.js +++ b/appwright/tests/performance/login/predict/predict-market-details.spec.js @@ -24,82 +24,91 @@ import { PerformancePredict } from '../../../../tags.js'; * 5. Time to load and verify Outcomes tab content */ test.describe(PerformancePredict, () => { - test('Predict Market Details - Load Time Performance', async ({ - device, - performanceTracker, - }, testInfo) => { - // Setup screen objects with device - LoginScreen.device = device; - WalletMainScreen.device = device; - TabBarModal.device = device; - WalletActionModal.device = device; - PredictMarketListScreen.device = device; - PredictDetailsScreen.device = device; + test( + 'Predict Market Details - Load Time Performance', + { tag: '@team-predict' }, + async ({ device, performanceTracker }, testInfo) => { + // Setup screen objects with device + LoginScreen.device = device; + WalletMainScreen.device = device; + TabBarModal.device = device; + WalletActionModal.device = device; + PredictMarketListScreen.device = device; + PredictDetailsScreen.device = device; - // Login to the app - await login(device); - console.log('Tap Action Button'); - await TabBarModal.tapActionButton(); - console.log('Tapped Action Button'); + // Login to the app + await login(device); + await TabBarModal.tapActionButton(); - // Timer 2: Open predictions tab (threshold: 5000ms + 10% = 5500ms) - const timer2 = new TimerHelper( - 'Time since user taps Predict button until Predict Market List is displayed', - { ios: 2800, android: 4000 }, - device, - ); - await WalletActionModal.tapPredictButton(); - await timer2.measure(async () => { - await PredictMarketListScreen.isContainerDisplayed(); - }); + // Timer 2: Open predictions tab (threshold: 5000ms + 10% = 5500ms) + const timer2 = new TimerHelper( + 'Time since user taps Predict button until Predict Market List is displayed', + { ios: 2800, android: 4000 }, + device, + ); + await WalletActionModal.tapPredictButton(); + await timer2.measure(async () => { + await PredictMarketListScreen.isContainerDisplayed(); + }); - // Timer 3: Open market details (threshold: 5000ms + 10% = 5500ms) - const timer3 = new TimerHelper( - 'Time since user taps market card until Market Details screen is visible', - { ios: 17000, android: 13000 }, - device, - ); - await PredictMarketListScreen.tapMarketCard('trending', 2); // second card to avoid flakiness for a promoted card - await timer3.measure(async () => { - await PredictDetailsScreen.isVisible(); - }); + const timer3 = new TimerHelper( + 'Time since user taps market card until Market Details screen is visible', + { ios: 17000, android: 13000 }, + device, + ); + await PredictMarketListScreen.tapMarketCard('trending', 2); // second card to avoid flakiness for a promoted card + await timer3.measure(async () => { + await PredictDetailsScreen.isVisible(); + }); - // Timer 4: Load About tab (threshold: 3000ms + 10% = 3300ms) - const timer4 = new TimerHelper( - 'Time since user taps About tab until About tab content is loaded and Volume text is visible', - { ios: 7800, android: 7800 }, - device, - ); - await PredictDetailsScreen.tapAboutTab(); - await timer4.measure(async () => { - await PredictDetailsScreen.isAboutTabContentDisplayed(); - await PredictDetailsScreen.verifyVolumeTextDisplayed(); - }); + const timer4 = new TimerHelper( + 'Time since user taps About tab until About tab content is loaded and Volume text is visible', + { ios: 7800, android: 7800 }, + device, + ); + await PredictDetailsScreen.tapAboutTab(); + await timer4.measure(async () => { + await PredictDetailsScreen.isAboutTabContentDisplayed(); + await PredictDetailsScreen.verifyVolumeTextDisplayed(); + }); - // Timer 5: Load Outcomes tab (threshold: 3000ms + 10% = 3300ms) - const timer5 = new TimerHelper( - 'Time since user taps Outcomes tab until Outcomes tab content is loaded and Yes/No options are visible', - { ios: 6000, android: 6000 }, - device, - ); - await PredictDetailsScreen.tapOutcomesTab(); - await timer5.measure(async () => { - await PredictDetailsScreen.isOutcomesTabContentDisplayed(); - }); + const timersToAdd = [timer2, timer3, timer4]; + let totalDuration = + timer2.getDuration() + timer3.getDuration() + timer4.getDuration(); - // Add all timers to performance tracker - performanceTracker.addTimers(timer2, timer3, timer4, timer5); + if (await PredictDetailsScreen.hasOutcomesTab()) { + // Yes/No-only markets have no Outcomes tab + const timer5 = new TimerHelper( + 'Time since user taps Outcomes tab until Outcomes tab content is loaded and Yes/No options are visible', + { ios: 6000, android: 6000 }, + device, + ); + await PredictDetailsScreen.tapOutcomesTab(); + await timer5.measure(async () => { + await PredictDetailsScreen.isOutcomesTabContentDisplayed(); + }); + timersToAdd.push(timer5); + totalDuration += timer5.getDuration(); + } else { + console.log( + '⏭️ Outcomes tab not present (Yes/No-only market); skipping Timer 5', + ); + } - // Attach performance metrics to test report - await performanceTracker.attachToTest(testInfo); + // Add all timers to performance tracker + performanceTracker.addTimers(...timersToAdd); - console.log('✅ Predict Market Details Performance Test completed'); - console.log(`📊 Modal to Market List: ${timer2.getDuration()}ms`); - console.log(`📊 Market List to Details: ${timer3.getDuration()}ms`); - console.log(`📊 About Tab Load: ${timer4.getDuration()}ms`); - console.log(`📊 Outcomes Tab Load: ${timer5.getDuration()}ms`); - console.log( - `📊 Total Time: ${timer2.getDuration() + timer3.getDuration() + timer4.getDuration() + timer5.getDuration()}ms`, - ); - }); -}); // End describe + // Attach performance metrics to test report + await performanceTracker.attachToTest(testInfo); + + console.log('✅ Predict Market Details Performance Test completed'); + console.log(`📊 Modal to Market List: ${timer2.getDuration()}ms`); + console.log(`📊 Market List to Details: ${timer3.getDuration()}ms`); + console.log(`📊 About Tab Load: ${timer4.getDuration()}ms`); + if (timersToAdd.length > 3) { + console.log(`📊 Outcomes Tab Load: ${timersToAdd[3].getDuration()}ms`); + } + console.log(`📊 Total Time: ${totalDuration}ms`); + }, + ); +}); diff --git a/appwright/tests/performance/onboarding/import-wallet.spec.js b/appwright/tests/performance/onboarding/import-wallet.spec.js index 69a31510898..36640e22a91 100644 --- a/appwright/tests/performance/onboarding/import-wallet.spec.js +++ b/appwright/tests/performance/onboarding/import-wallet.spec.js @@ -22,109 +22,116 @@ import { PerformanceOnboarding } from '../../../tags.js'; /* Scenario 4: Imported wallet with +50 accounts */ test.describe(PerformanceOnboarding, () => { - test.setTimeout(180000); - test('Onboarding Import SRP with +50 accounts, SRP 3', async ({ - device, - performanceTracker, - }, testInfo) => { - WelcomeScreen.device = device; - TermOfUseScreen.device = device; - OnboardingScreen.device = device; - CreateNewWalletScreen.device = device; - MetaMetricsScreen.device = device; - OnboardingSucessScreen.device = device; - OnboardingSheet.device = device; - WalletAccountModal.device = device; - SkipAccountSecurityModal.device = device; - WalletMainScreen.device = device; - ImportFromSeedScreen.device = device; - CreatePasswordScreen.device = device; - const timer1 = new TimerHelper( - 'Time since the user clicks on "Create new wallet" button until "Social sign up" is visible', - { ios: 1000, android: 1800 }, - device, - ); - const timer2 = new TimerHelper( - 'Time since the user clicks on "Import using SRP" button until SRP field is displayed', - { ios: 1000, android: 1500 }, - device, - ); - const timer3 = new TimerHelper( - 'Time since the user clicks on "Continue" button on SRP screen until Password fields are visible', - { ios: 2500, android: 1800 }, - device, - ); - const timer4 = new TimerHelper( - 'Time since the user clicks on "Create Password" button until Metrics screen is displayed', - { ios: 1000, android: 1600 }, - device, - ); - const timer5 = new TimerHelper( - 'Time since the user clicks on "I agree" button on Metrics screen until Onboarding Success screen is visible', - { ios: 2200, android: 1700 }, - device, - ); - const timer6 = new TimerHelper( - 'Time since the user clicks on "Done" button until feature sheet is visible', - { ios: 2500, android: 3100 }, - device, - ); - const timer7 = new TimerHelper( - 'Time since the user clicks on "Not now" button On feature sheet until native token is visible', - { ios: 35000, android: 40000 }, - device, - ); + test.setTimeout(240000); + test( + 'Onboarding Import SRP with +50 accounts, SRP 3', + { tag: '@metamask-onboarding-team' }, + async ({ device, performanceTracker }, testInfo) => { + WelcomeScreen.device = device; + TermOfUseScreen.device = device; + OnboardingScreen.device = device; + CreateNewWalletScreen.device = device; + MetaMetricsScreen.device = device; + OnboardingSucessScreen.device = device; + OnboardingSheet.device = device; + WalletAccountModal.device = device; + SkipAccountSecurityModal.device = device; + WalletMainScreen.device = device; + ImportFromSeedScreen.device = device; + CreatePasswordScreen.device = device; + const timer1 = new TimerHelper( + 'Time since the user clicks on "Create new wallet" button until "Social sign up" is visible', + { ios: 1000, android: 1800 }, + device, + ); + const timer2 = new TimerHelper( + 'Time since the user clicks on "Import using SRP" button until SRP field is displayed', + { ios: 1000, android: 1500 }, + device, + ); + const timer3 = new TimerHelper( + 'Time since the user clicks on "Continue" button on SRP screen until Password fields are visible', + { ios: 2500, android: 1800 }, + device, + ); + const timer4 = new TimerHelper( + 'Time since the user clicks on "Create Password" button until Metrics screen is displayed', + { ios: 1000, android: 1600 }, + device, + ); + const timer5 = new TimerHelper( + 'Time since the user clicks on "I agree" button on Metrics screen until Onboarding Success screen is visible', + { ios: 2200, android: 1700 }, + device, + ); + const timer6 = new TimerHelper( + 'Time since the user clicks on "Done" button until feature sheet is visible', + { ios: 2500, android: 3100 }, + device, + ); + const timer7 = new TimerHelper( + 'Time since the user clicks on "Not now" button On feature sheet until native token is visible', + { ios: 90000, android: 90000 }, + device, + ); - await OnboardingScreen.tapHaveAnExistingWallet(); - await timer1.measure(async () => await OnboardingSheet.isVisible()); + await OnboardingScreen.tapHaveAnExistingWallet(); + await timer1.measure(async () => await OnboardingSheet.isVisible()); - await OnboardingSheet.tapImportSeedButton(); - await timer2.measure( - async () => await ImportFromSeedScreen.isScreenTitleVisible(), - ); + await OnboardingSheet.tapImportSeedButton(); + await timer2.measure( + async () => await ImportFromSeedScreen.isScreenTitleVisible(), + ); - await ImportFromSeedScreen.typeSecretRecoveryPhrase( - process.env.TEST_SRP_3, - true, - ); - await ImportFromSeedScreen.tapImportScreenTitleToDismissKeyboard(); + await ImportFromSeedScreen.typeSecretRecoveryPhrase( + process.env.TEST_SRP_3, + true, + ); + await ImportFromSeedScreen.tapImportScreenTitleToDismissKeyboard(); - await ImportFromSeedScreen.tapContinueButton(); - await timer3.measure(async () => await CreatePasswordScreen.isVisible()); + await ImportFromSeedScreen.tapContinueButton(); + await timer3.measure(async () => await CreatePasswordScreen.isVisible()); - await CreatePasswordScreen.enterPassword(getPasswordForScenario('import')); - await CreatePasswordScreen.reEnterPassword( - getPasswordForScenario('import'), - ); - await CreatePasswordScreen.tapIUnderstandCheckBox(); - await CreatePasswordScreen.tapCreatePasswordButton(); + await CreatePasswordScreen.enterPassword( + getPasswordForScenario('import'), + ); + await CreatePasswordScreen.reEnterPassword( + getPasswordForScenario('import'), + ); + await CreatePasswordScreen.tapIUnderstandCheckBox(); + await CreatePasswordScreen.tapCreatePasswordButton(); - await timer4.measure( - async () => await MetaMetricsScreen.isScreenTitleVisible(), - ); + await timer4.measure( + async () => await MetaMetricsScreen.isScreenTitleVisible(), + ); - await MetaMetricsScreen.tapIAgreeButton(); - await timer5.measure(async () => await OnboardingSucessScreen.isVisible()); + await MetaMetricsScreen.tapIAgreeButton(); + await timer5.measure( + async () => await OnboardingSucessScreen.isVisible(), + ); - await OnboardingSucessScreen.tapDone(); - await timer6.measure( - async () => await checkPredictionsModalIsVisible(device), - ); + await OnboardingSucessScreen.tapDone(); + await timer6.measure( + async () => await checkPredictionsModalIsVisible(device), + ); - await dissmissPredictionsModal(device); - await timer7.measure(async () => { - await WalletMainScreen.isTokenVisible('SOL'); - }); + await dissmissPredictionsModal(device); + await timer7.measure(async () => { + await WalletMainScreen.isTokenVisible('SOL'); + await WalletMainScreen.isTokenVisible('BTC'); + await WalletMainScreen.isTokenVisible('TRX'); + }); - performanceTracker.addTimers( - timer1, - timer2, - timer3, - timer4, - timer5, - timer6, - timer7, - ); - await performanceTracker.attachToTest(testInfo); - }); -}); // End describe + performanceTracker.addTimers( + timer1, + timer2, + timer3, + timer4, + timer5, + timer6, + timer7, + ); + await performanceTracker.attachToTest(testInfo); + }, + ); +}); diff --git a/appwright/tests/performance/onboarding/imported-wallet-account-creation.spec.js b/appwright/tests/performance/onboarding/imported-wallet-account-creation.spec.js index 1b4f817b9c5..cd0cd472d33 100644 --- a/appwright/tests/performance/onboarding/imported-wallet-account-creation.spec.js +++ b/appwright/tests/performance/onboarding/imported-wallet-account-creation.spec.js @@ -17,44 +17,45 @@ import { /* Scenario 1: Imported wallet with 50+ accounts + account creation */ test.describe(`${PerformanceOnboarding} ${PerformanceAccountList}`, () => { - test.skip('Account creation with 50+ accounts, SRP 1 + SRP 2 + SRP 3', async ({ - device, - performanceTracker, - }, testInfo) => { - LoginScreen.device = device; - WalletMainScreen.device = device; - AccountListComponent.device = device; - AddAccountModal.device = device; - WalletActionModal.device = device; - TabBarModal.device = device; - WalletMainScreen.device = device; - AccountListComponent.device = device; - AddAccountModal.device = device; - AddNewHdAccountComponent.device = device; - await onboardingFlowImportSRP(device, process.env.TEST_SRP_2); - - // await importSRPFlow(device, process.env.TEST_SRP_2); - // await importSRPFlow(device, process.env.TEST_SRP_3); - - const screen1Timer = new TimerHelper( - 'Time since the user clicks on "Account list" button until the account list is visible', - ); - - const screen3Timer = new TimerHelper( - 'Time since the user clicks on new account created until the Token list is visible', - ); - - await WalletMainScreen.tapIdenticon(); - await screen1Timer.measure(() => - AccountListComponent.isComponentDisplayed(), - ); - - await AccountListComponent.tapCreateAccountButton(); - await screen3Timer.measure(async () => { - await WalletMainScreen.isTokenVisible('SOL'); - }); - - performanceTracker.addTimers(screen1Timer, screen3Timer); - await performanceTracker.attachToTest(testInfo); - }); -}); // End describe + test.skip( + 'Account creation with 50+ accounts, SRP 1 + SRP 2 + SRP 3', + { tag: '@metamask-onboarding-team' }, + async ({ device, performanceTracker }, testInfo) => { + LoginScreen.device = device; + WalletMainScreen.device = device; + AccountListComponent.device = device; + AddAccountModal.device = device; + WalletActionModal.device = device; + TabBarModal.device = device; + WalletMainScreen.device = device; + AccountListComponent.device = device; + AddAccountModal.device = device; + AddNewHdAccountComponent.device = device; + await onboardingFlowImportSRP(device, process.env.TEST_SRP_2); + + // await importSRPFlow(device, process.env.TEST_SRP_2); + // await importSRPFlow(device, process.env.TEST_SRP_3); + + const screen1Timer = new TimerHelper( + 'Time since the user clicks on "Account list" button until the account list is visible', + ); + + const screen3Timer = new TimerHelper( + 'Time since the user clicks on new account created until the Token list is visible', + ); + + await WalletMainScreen.tapIdenticon(); + await screen1Timer.measure(() => + AccountListComponent.isComponentDisplayed(), + ); + + await AccountListComponent.tapCreateAccountButton(); + await screen3Timer.measure(async () => { + await WalletMainScreen.isTokenVisible('SOL'); + }); + + performanceTracker.addTimers(screen1Timer, screen3Timer); + await performanceTracker.attachToTest(testInfo); + }, + ); +}); diff --git a/appwright/tests/performance/onboarding/launch-times/cold-start-after-wallet-import.spec.js b/appwright/tests/performance/onboarding/launch-times/cold-start-after-wallet-import.spec.js index c4970063516..136c059542a 100644 --- a/appwright/tests/performance/onboarding/launch-times/cold-start-after-wallet-import.spec.js +++ b/appwright/tests/performance/onboarding/launch-times/cold-start-after-wallet-import.spec.js @@ -23,49 +23,50 @@ import LoginScreen from '../../../../../wdio/screen-objects/LoginScreen.js'; import { PerformanceOnboarding, PerformanceLaunch } from '../../../../tags.js'; test.describe(`${PerformanceOnboarding} ${PerformanceLaunch}`, () => { - test('Cold Start after importing a wallet', async ({ - device, - performanceTracker, - }, testInfo) => { - WelcomeScreen.device = device; - TermOfUseScreen.device = device; - OnboardingScreen.device = device; - CreateNewWalletScreen.device = device; - MetaMetricsScreen.device = device; - OnboardingSucessScreen.device = device; - OnboardingSheet.device = device; - WalletAccountModal.device = device; - SkipAccountSecurityModal.device = device; - ImportFromSeedScreen.device = device; - CreatePasswordScreen.device = device; - WalletMainScreen.device = device; - AccountListComponent.device = device; - AddAccountModal.device = device; - WalletActionModal.device = device; - ConfirmationScreen.device = device; - AmountScreen.device = device; - MultichainAccountEducationModal.device = device; - LoginScreen.device = device; - WalletActionModal.device = device; - await onboardingFlowImportSRP(device, process.env.TEST_SRP_3); - // await importSRPFlow(device, process.env.TEST_SRP_2); - // await importSRPFlow(device, process.env.TEST_SRP_3); - await AppwrightGestures.terminateApp(device); - await AppwrightGestures.activateApp(device); - await LoginScreen.waitForScreenToDisplay(); - await login(device, { - scenarioType: 'onboarding', - skipIntro: true, - }); // Skip intro screens on second login + test( + 'Cold Start after importing a wallet', + { tag: '@metamask-mobile-platform' }, + async ({ device, performanceTracker }, testInfo) => { + WelcomeScreen.device = device; + TermOfUseScreen.device = device; + OnboardingScreen.device = device; + CreateNewWalletScreen.device = device; + MetaMetricsScreen.device = device; + OnboardingSucessScreen.device = device; + OnboardingSheet.device = device; + WalletAccountModal.device = device; + SkipAccountSecurityModal.device = device; + ImportFromSeedScreen.device = device; + CreatePasswordScreen.device = device; + WalletMainScreen.device = device; + AccountListComponent.device = device; + AddAccountModal.device = device; + WalletActionModal.device = device; + ConfirmationScreen.device = device; + AmountScreen.device = device; + MultichainAccountEducationModal.device = device; + LoginScreen.device = device; + WalletActionModal.device = device; + await onboardingFlowImportSRP(device, process.env.TEST_SRP_3); + // await importSRPFlow(device, process.env.TEST_SRP_2); + // await importSRPFlow(device, process.env.TEST_SRP_3); + await AppwrightGestures.terminateApp(device); + await AppwrightGestures.activateApp(device); + await LoginScreen.waitForScreenToDisplay(); + await login(device, { + scenarioType: 'onboarding', + skipIntro: true, + }); // Skip intro screens on second login - const timer1 = await WalletMainScreen.isMenuButtonVisible(); - timer1.changeName( - 'Time since the user clicks on unlock button, until the app unlocks', - { ios: 2000, android: 2000 }, - device, - ); + const timer1 = await WalletMainScreen.isMenuButtonVisible(); + timer1.changeName( + 'Time since the user clicks on unlock button, until the app unlocks', + { ios: 2000, android: 2000 }, + device, + ); - performanceTracker.addTimer(timer1); - await performanceTracker.attachToTest(testInfo); - }); -}); // End describe + performanceTracker.addTimer(timer1); + await performanceTracker.attachToTest(testInfo); + }, + ); +}); diff --git a/appwright/tests/performance/onboarding/launch-times/cold-start-to-onboarding.spec.js b/appwright/tests/performance/onboarding/launch-times/cold-start-to-onboarding.spec.js index c88574a0dd7..fb9d449ac36 100644 --- a/appwright/tests/performance/onboarding/launch-times/cold-start-to-onboarding.spec.js +++ b/appwright/tests/performance/onboarding/launch-times/cold-start-to-onboarding.spec.js @@ -4,19 +4,20 @@ import OnboardingScreen from '../../../../../wdio/screen-objects/Onboarding/Onbo import { PerformanceOnboarding, PerformanceLaunch } from '../../../../tags.js'; test.describe(`${PerformanceOnboarding} ${PerformanceLaunch}`, () => { - test('Measure Cold Start To Onboarding Screen', async ({ - device, - performanceTracker, - }, testInfo) => { - OnboardingScreen.device = device; - const timer1 = new TimerHelper( - 'Time since the the app is installed, until onboarding screen appears', - { ios: 3000, android: 3900 }, - device, - ); - await timer1.measure(() => OnboardingScreen.isScreenTitleVisible()); + test( + 'Measure Cold Start To Onboarding Screen', + { tag: '@metamask-mobile-platform' }, + async ({ device, performanceTracker }, testInfo) => { + OnboardingScreen.device = device; + const timer1 = new TimerHelper( + 'Time since the the app is installed, until onboarding screen appears', + { ios: 3000, android: 3900 }, + device, + ); + await timer1.measure(() => OnboardingScreen.isScreenTitleVisible()); - performanceTracker.addTimer(timer1); - await performanceTracker.attachToTest(testInfo); - }); -}); // End describe + performanceTracker.addTimer(timer1); + await performanceTracker.attachToTest(testInfo); + }, + ); +}); diff --git a/appwright/tests/performance/onboarding/new-wallet-account-creation.spec.js b/appwright/tests/performance/onboarding/new-wallet-account-creation.spec.js index 722a2f5ab5d..f969eab38cf 100644 --- a/appwright/tests/performance/onboarding/new-wallet-account-creation.spec.js +++ b/appwright/tests/performance/onboarding/new-wallet-account-creation.spec.js @@ -22,87 +22,88 @@ import { /* Scenario 2: Account creation after fresh install */ test.describe(`${PerformanceOnboarding} ${PerformanceAccountList}`, () => { - test('Account creation after fresh install', async ({ - device, - performanceTracker, - }, testInfo) => { - WelcomeScreen.device = device; - TermOfUseScreen.device = device; - OnboardingScreen.device = device; - CreateNewWalletScreen.device = device; - MetaMetricsScreen.device = device; - OnboardingSucessScreen.device = device; - OnboardingSheet.device = device; - WalletAccountModal.device = device; - SkipAccountSecurityModal.device = device; - WalletMainScreen.device = device; - AccountListComponent.device = device; - CreatePasswordScreen.device = device; - await OnboardingScreen.tapCreateNewWalletButton(); - await OnboardingSheet.isVisible(); - - await OnboardingSheet.tapImportSeedButton(); - await CreateNewWalletScreen.isNewAccountScreenFieldsVisible(); - - await CreateNewWalletScreen.inputPasswordInFirstField( - getPasswordForScenario('onboarding'), - ); - await CreateNewWalletScreen.inputConfirmPasswordField( - getPasswordForScenario('onboarding'), - ); - - await CreatePasswordScreen.tapIUnderstandCheckBox(); - - await CreatePasswordScreen.tapCreatePasswordButton(); - - await CreateNewWalletScreen.tapRemindMeLater(); - - await MetaMetricsScreen.isScreenTitleVisible(); - - await MetaMetricsScreen.tapContinueButton(); - await OnboardingSucessScreen.isVisible(); - - await OnboardingSucessScreen.tapDone(); - - await dissmissPredictionsModal(device); - - await WalletMainScreen.isMainWalletViewVisible(); - - // await WalletMainScreen.isTokenVisible('SOL'); // TODO: skipped since locator is no longer reachable - - const screen1Timer = new TimerHelper( - 'Time since the user clicks on "Account list" button until the account list is visible', - { ios: 1000, android: 3000 }, - device, - ); - const screen2Timer = new TimerHelper( - 'Time since the user clicks on "Create account" button until the account is in the account list', - { ios: 1300, android: 2000 }, - device, - ); - const screen3Timer = new TimerHelper( - 'Time since the user clicks on new account created until the Token list is visible', - { ios: 3000, android: 3000 }, - device, - ); - - await WalletMainScreen.tapIdenticon(); - await screen1Timer.measure(() => - AccountListComponent.isComponentDisplayed(), - ); - - await AccountListComponent.waitForSyncingToComplete(); - await AccountListComponent.tapCreateAccountButton(); - await screen2Timer.measure(() => - AccountListComponent.isAccountDisplayed('Account 2', 30000), - ); - - await AccountListComponent.tapOnAccountByName('Account 2'); - await screen3Timer.measure(async () => { - await WalletMainScreen.checkActiveAccount('Account 2'); - }); - - performanceTracker.addTimers(screen1Timer, screen2Timer, screen3Timer); - await performanceTracker.attachToTest(testInfo); - }); -}); // End describe + test( + 'Account creation after fresh install', + { tag: '@metamask-onboarding-team' }, + async ({ device, performanceTracker }, testInfo) => { + WelcomeScreen.device = device; + TermOfUseScreen.device = device; + OnboardingScreen.device = device; + CreateNewWalletScreen.device = device; + MetaMetricsScreen.device = device; + OnboardingSucessScreen.device = device; + OnboardingSheet.device = device; + WalletAccountModal.device = device; + SkipAccountSecurityModal.device = device; + WalletMainScreen.device = device; + AccountListComponent.device = device; + CreatePasswordScreen.device = device; + await OnboardingScreen.tapCreateNewWalletButton(); + await OnboardingSheet.isVisible(); + + await OnboardingSheet.tapImportSeedButton(); + await CreateNewWalletScreen.isNewAccountScreenFieldsVisible(); + + await CreateNewWalletScreen.inputPasswordInFirstField( + getPasswordForScenario('onboarding'), + ); + await CreateNewWalletScreen.inputConfirmPasswordField( + getPasswordForScenario('onboarding'), + ); + + await CreatePasswordScreen.tapIUnderstandCheckBox(); + + await CreatePasswordScreen.tapCreatePasswordButton(); + + await CreateNewWalletScreen.tapRemindMeLater(); + + await MetaMetricsScreen.isScreenTitleVisible(); + + await MetaMetricsScreen.tapContinueButton(); + await OnboardingSucessScreen.isVisible(); + + await OnboardingSucessScreen.tapDone(); + + await dissmissPredictionsModal(device); + + await WalletMainScreen.isMainWalletViewVisible(); + + // await WalletMainScreen.isTokenVisible('SOL'); // TODO: skipped since locator is no longer reachable + + const screen1Timer = new TimerHelper( + 'Time since the user clicks on "Account list" button until the account list is visible', + { ios: 3000, android: 3000 }, + device, + ); + const screen2Timer = new TimerHelper( + 'Time since the user clicks on "Create account" button until the account is in the account list', + { ios: 1300, android: 2000 }, + device, + ); + const screen3Timer = new TimerHelper( + 'Time since the user clicks on new account created until the Token list is visible', + { ios: 3000, android: 3000 }, + device, + ); + + await WalletMainScreen.tapIdenticon(); + await screen1Timer.measure(() => + AccountListComponent.isComponentDisplayed(), + ); + + await AccountListComponent.waitForSyncingToComplete(); + await AccountListComponent.tapCreateAccountButton(); + await screen2Timer.measure(() => + AccountListComponent.isAccountDisplayed('Account 2', 30000), + ); + + await AccountListComponent.tapOnAccountByName('Account 2'); + await screen3Timer.measure(async () => { + await WalletMainScreen.checkActiveAccount('Account 2'); + }); + + performanceTracker.addTimers(screen1Timer, screen2Timer, screen3Timer); + await performanceTracker.attachToTest(testInfo); + }, + ); +}); diff --git a/e2e/selectors/Browser/TestSnaps.selectors.ts b/e2e/selectors/Browser/TestSnaps.selectors.ts index 5cb4aeedc30..c73980ba39e 100644 --- a/e2e/selectors/Browser/TestSnaps.selectors.ts +++ b/e2e/selectors/Browser/TestSnaps.selectors.ts @@ -71,6 +71,7 @@ export const TestSnapViewSelectorWebIDS = { showPreinstalledDialogButton: 'showPreinstalledDialog', getWebSocketState: 'getWebSocketState', getChainIdButton: 'sendEthprovider', + getGenesisHashButton: 'sendGenesisBlockEthProvider', getAccountsButton: 'sendEthproviderAccounts', personalSignButton: 'signPersonalSignMessage', sendWasmMessageButton: 'sendWasmMessage', diff --git a/e2e/specs/snaps/test-snap-ethereum-provider.spec.ts b/e2e/specs/snaps/test-snap-ethereum-provider.spec.ts index e7c1a9e782d..a4865757386 100644 --- a/e2e/specs/snaps/test-snap-ethereum-provider.spec.ts +++ b/e2e/specs/snaps/test-snap-ethereum-provider.spec.ts @@ -12,6 +12,7 @@ import { confirmationFeatureFlags, remoteFeatureMultichainAccountsAccountDetailsV2, } from '../../../tests/api-mocking/mock-responses/feature-flags-mocks'; +import { mockGenesisBlocks } from './mocks'; jest.setTimeout(150_000); @@ -27,6 +28,8 @@ describe(FlaskBuildTests('Ethereum Provider Snap Tests'), () => { ...Object.assign({}, ...confirmationFeatureFlags), ...remoteFeatureMultichainAccountsAccountDetailsV2(false), }); + + await mockGenesisBlocks(mockServer); }, }, async () => { @@ -77,21 +80,24 @@ describe(FlaskBuildTests('Ethereum Provider Snap Tests'), () => { // Check other networks. await TestSnaps.selectInDropdown('networkDropDown', 'Ethereum'); - await TestSnaps.tapButton('getChainIdButton'); - await TestSnaps.checkResultSpan('ethereumProviderResultSpan', '"0x1"'); + await TestSnaps.tapButton('getGenesisHashButton'); + await TestSnaps.checkResultSpanIncludes( + 'ethereumProviderResultSpan', + '"0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"', + ); await TestSnaps.selectInDropdown('networkDropDown', 'Linea'); - await TestSnaps.tapButton('getChainIdButton'); - await TestSnaps.checkResultSpan( + await TestSnaps.tapButton('getGenesisHashButton'); + await TestSnaps.checkResultSpanIncludes( 'ethereumProviderResultSpan', - '"0xe708"', + '"0xb6762a65689107b2326364aefc18f94cda413209fab35c00d4af51eaa20ffbc6"', ); await TestSnaps.selectInDropdown('networkDropDown', 'Sepolia'); - await TestSnaps.tapButton('getChainIdButton'); - await TestSnaps.checkResultSpan( + await TestSnaps.tapButton('getGenesisHashButton'); + await TestSnaps.checkResultSpanIncludes( 'ethereumProviderResultSpan', - '"0xaa36a7"', + '"0x25a5cc106eea7138acab33231d7160d69cb777ee0c2c553fcddf5138993e6dd9"', ); }, ); diff --git a/e2e/tags.js b/e2e/tags.js index d03143ff337..34c4907f871 100644 --- a/e2e/tags.js +++ b/e2e/tags.js @@ -9,62 +9,62 @@ const smokeTags = { smokeAccounts: { tag: 'SmokeAccounts:', description: - 'Tests account security and multi-account management within the wallet. Covers Secret Recovery Phrase (SRP) protection flows including the reveal quiz validation in Settings, SRP export from both Settings and account action menus, and wallet details credential display. Also tests multi-account workflows: creating new HD wallet accounts, adding QR-based hardware wallet accounts, importing accounts via private key, account switching and selection, account renaming, and managing account visibility in the account list. Integrates with the AccountSelector and RevealPrivateCredential components. Related to SmokeWalletPlatform for multi-SRP architecture and SmokeIdentity for account sync features.', + 'Tests account security and multi-account management within the wallet. Covers Secret Recovery Phrase (SRP) protection flows including the reveal quiz validation in Settings, SRP export from both Settings and account action menus, and wallet details credential display. Also tests multi-account workflows: creating new HD wallet accounts, adding QR-based hardware wallet accounts, importing accounts via private key, account switching and selection, account renaming, and managing account visibility in the account list. Integrates with the AccountSelector and RevealPrivateCredential components. When changes touch multi-SRP architecture, account list, or SRP export flows, also select SmokeWalletPlatform and SmokeIdentity. Related to SmokeWalletPlatform for multi-SRP architecture and SmokeIdentity for account sync features.', }, smokeConfirmations: { tag: 'SmokeConfirmations:', description: - 'Tests the transaction and signature confirmation UI system. Covers signature request types: personal_sign messages, Sign-In with Ethereum (SIWE/EIP-4361), and typed data signing (EIP-712 V1/V3/V4). Tests Blockaid security alert integration for detecting malicious signature requests. Validates smart contract interactions including contract deployment, method calls, and token approvals (ERC-20 approve, increaseAllowance, ERC-721/ERC-1155 setApprovalForAll). Tests transaction sending for native tokens (ETH), ERC-20 tokens, and Solana SPL tokens. Covers gas fee customization (EIP-1559 and legacy), transaction simulation previews, and advanced EIP-7702 account abstraction features like batch transactions and gas fee token payments. Also tests per-dApp network selection within confirmations. Integrates with SmokeTrade for swap/bridge confirmations and SmokeNetworkExpansion for cross-chain transactions.', + 'Tests the transaction and signature confirmation UI system. Covers signature request types: personal_sign messages, Sign-In with Ethereum (SIWE/EIP-4361), and typed data signing (EIP-712 V1/V3/V4). Tests Blockaid security alert integration for detecting malicious signature requests. Validates smart contract interactions including contract deployment, method calls, and token approvals (ERC-20 approve, increaseAllowance, ERC-721/ERC-1155 setApprovalForAll). Tests transaction sending for native tokens (ETH), ERC-20 tokens, and Solana SPL tokens. Covers gas fee customization (EIP-1559 and legacy), transaction simulation previews, and advanced EIP-7702 account abstraction features like batch transactions and gas fee token payments. Also tests per-dApp network selection within confirmations. Swap and bridge flows (SmokeTrade) require confirmations—when selecting SmokeTrade for swap/bridge, also select SmokeConfirmations. Solana transaction/signing flows (SmokeNetworkExpansion) also hit confirmations—when selecting SmokeNetworkExpansion for Solana flows, also select SmokeConfirmations. Integrates with SmokeNetworkExpansion for cross-chain transactions.', }, smokeIdentity: { tag: 'SmokeIdentity:', description: - 'Tests MetaMask Identity and cross-device synchronization via the Profile Sync Controller. Covers account syncing features: enabling/disabling sync via settings toggle, multi-SRP account synchronization, automatic account discovery with balance detection to find used accounts, adding and renaming accounts with sync propagation, and proper exclusion of imported (non-HD) accounts from sync. Also tests contact/address book syncing: contact sync toggle, creating and syncing user contacts, and verifying contact persistence after app restart and across devices. Tests the backup and sync onboarding flow. Related to SmokeAccounts for account management and SmokeWalletPlatform for multi-SRP architecture.', + 'Tests MetaMask Identity and cross-device synchronization via the Profile Sync Controller. Covers account syncing features: enabling/disabling sync via settings toggle, multi-SRP account synchronization, automatic account discovery with balance detection to find used accounts, adding and renaming accounts with sync propagation, and proper exclusion of imported (non-HD) accounts from sync. Also tests contact/address book syncing: contact sync toggle, creating and syncing user contacts, and verifying contact persistence after app restart and across devices. Tests the backup and sync onboarding flow. When changes touch account sync or multi-SRP flows, also select SmokeAccounts and SmokeWalletPlatform. Related to SmokeAccounts for account management and SmokeWalletPlatform for multi-SRP architecture.', }, smokeNetworkAbstractions: { tag: 'SmokeNetworkAbstractions:', description: - 'Tests the network management and selection layer. Covers the Network Manager UI: viewing available networks, selecting/deselecting networks, managing network enabled state, and the network selector bottom sheet. Tests adding popular networks (Ethereum, Linea, Polygon, Arbitrum, etc.) and custom RPC networks. Validates multi-chain token filtering to show relevant tokens based on the selected network for both EVM chains and Solana. Tests the chain permission system for dApps: granting/revoking chain access, handling dApp chain switch requests, and modifying permitted chains per dApp. Also covers notification settings flows. Integrates with SmokeNetworkExpansion for multi-chain provider support and SmokeMultiChainAPI for session-based permissions.', + 'Tests the network management and selection layer. Covers the Network Manager UI: viewing available networks, selecting/deselecting networks, managing network enabled state, and the network selector bottom sheet. Tests adding popular networks (Ethereum, Linea, Polygon, Arbitrum, etc.) and custom RPC networks. Validates multi-chain token filtering to show relevant tokens based on the selected network for both EVM chains and Solana. Tests the chain permission system for dApps: granting/revoking chain access, handling dApp chain switch requests, and modifying permitted chains per dApp. Also covers notification settings flows. When changes affect dApp chain permissions or multi-chain selection, also select SmokeNetworkExpansion and SmokeMultiChainAPI. Integrates with SmokeNetworkExpansion for multi-chain provider support and SmokeMultiChainAPI for session-based permissions.', }, smokeNetworkExpansion: { tag: 'SmokeNetworkExpansion:', description: - 'Tests non-EVM blockchain support and multi-chain provider architecture. Focuses on Solana Wallet Standard compliance: dApp connect/disconnect flows, displaying Solana account addresses to dApps, account switching with dApp notification, session persistence after page refresh, SOL token transfers, and Solana message signing. Tests the multi-chain provider system enabling simultaneous connections from a single dApp to multiple blockchain networks (e.g., EVM and Solana together). Covers initial connection permission flows for multi-chain dApps and account selection across chain types. Integrates with SmokeNetworkAbstractions for chain permissions and SmokeConfirmations for Solana transaction confirmations.', + 'Tests non-EVM blockchain support and multi-chain provider architecture. Focuses on Solana Wallet Standard compliance: dApp connect/disconnect flows, displaying Solana account addresses to dApps, account switching with dApp notification, session persistence after page refresh, SOL token transfers, and Solana message signing. Tests the multi-chain provider system enabling simultaneous connections from a single dApp to multiple blockchain networks (e.g., EVM and Solana together). Covers initial connection permission flows for multi-chain dApps and account selection across chain types. When selecting SmokeNetworkExpansion for Solana transaction or signing flows, also select SmokeConfirmations (Solana confirmations). Integrates with SmokeNetworkAbstractions for chain permissions and SmokeMultiChainAPI for session-based multi-chain.', }, smokeTrade: { tag: 'SmokeTrade:', description: - 'Tests DeFi trading and financial features accessed via the Trade wallet actions menu. Covers token swaps: executing swaps (e.g., ETH to USDC) through the unified swap interface, quote fetching, and swap completion verification in activities. Tests cross-chain bridging between networks (e.g., Ethereum mainnet to Base). Validates gasless swap execution via Smart Transactions where users do not pay gas directly. Tests native ETH staking flows initiated from the wallet actions menu. Covers deep link navigation into swap and bridge screens from external sources. Validates analytics event tracking for swap started, swap completed, quotes received, and bridge events. The TradeWalletActions bottom sheet provides entry points to Swap, Bridge, Perps, Predictions, and Earn features. Related to SmokeConfirmations for transaction confirmations and SmokeWalletPlatform for activity display.', + 'Tests DeFi trading and financial features accessed via the Trade wallet actions menu. Covers token swaps: executing swaps (e.g., ETH to USDC) through the unified swap interface, quote fetching, and swap completion verification in activities. Tests cross-chain bridging between networks (e.g., Ethereum mainnet to Base). Validates gasless swap execution via Smart Transactions where users do not pay gas directly. Tests native ETH staking flows initiated from the wallet actions menu. Covers deep link navigation into swap and bridge screens from external sources. Validates analytics event tracking for swap started, swap completed, quotes received, and bridge events. The TradeWalletActions bottom sheet provides entry points to Swap, Bridge, Perps, Predictions, and Earn features. When selecting SmokeTrade for swap or bridge flows, also select SmokeConfirmations (transaction confirmations are part of the flow). Related to SmokeWalletPlatform for activity display.', }, smokeWalletPlatform: { tag: 'SmokeWalletPlatform:', description: - 'Tests core wallet platform features and services. Covers the Trending discovery tab: search functionality, browsing content feeds (Predictions, Tokens, Perps, Sites sections), and browser navigation integration. Tests transaction history: displaying incoming/outgoing ETH transactions, token transfer details, and privacy mode support to hide sensitive balances. Validates wallet lifecycle analytics tracking for new wallet creation and SRP import events. Tests multi-SRP wallet architecture: importing additional Secret Recovery Phrases, adding accounts to different SRPs, exporting SRP from Settings and account action menus, and managing separate account hierarchies per SRP. Covers account deletion flows and EVM provider event handling (accountsChanged, chainChanged) for dApp communication. Integrates with SmokeAccounts for account management, SmokeTrade for activity display, and SmokeIdentity for sync features.', + 'Tests core wallet platform features and services. Covers the Trending discovery tab: search functionality, browsing content feeds (Predictions, Tokens, Perps, Sites sections), and browser navigation integration. Trending is the connecting point for all subsections—changes to Perps, Predictions, or Tokens views (headers, lists, full views) that are embedded in Trending affect this tag. Tests transaction history: displaying incoming/outgoing ETH transactions, token transfer details, and privacy mode support to hide sensitive balances. Validates wallet lifecycle analytics tracking for new wallet creation and SRP import events. Tests multi-SRP wallet architecture: importing additional Secret Recovery Phrases, adding accounts to different SRPs, exporting SRP from Settings and account action menus, and managing separate account hierarchies per SRP. Covers account deletion flows and EVM provider event handling (accountsChanged, chainChanged) for dApp communication. Integrates with SmokeAccounts for account management, SmokeTrade for activity display, SmokePerps (Perps section inside Trending), and SmokeIdentity for sync features.', }, smokeCard: { tag: 'SmokeCard:', description: - 'Tests MetaMask Card integration for crypto-to-fiat spending. Covers the Card home screen display showing card status and balance, the Add Funds button with Deposit and Swap funding options, and Advanced Card Management which opens the external card dashboard in the browser. Tests the Card navbar button for quick navigation to Card home. Validates card-related analytics events: Card Button Viewed, Card Home Clicked, Card Add Funds Clicked, and Card Advanced Management Clicked. The Card feature is controlled by experimental feature flags. Integrates with SmokeTrade for funding via swaps.', + 'Tests MetaMask Card integration for crypto-to-fiat spending. Covers the Card home screen display showing card status and balance, the Add Funds button with Deposit and Swap funding options, and Advanced Card Management which opens the external card dashboard in the browser. Tests the Card navbar button for quick navigation to Card home. Validates card-related analytics events: Card Button Viewed, Card Home Clicked, Card Add Funds Clicked, and Card Advanced Management Clicked. The Card feature is controlled by experimental feature flags. When selecting SmokeCard, also select SmokeTrade and SmokeConfirmations (Add Funds uses swaps which require transaction confirmations). Integrates with SmokeTrade for funding via swaps.', }, smokePerps: { tag: 'SmokePerps:', description: - 'Tests perpetuals (perps) futures trading functionality on Arbitrum One. Covers the Add Funds flow to deposit USDC into the Perps trading account, balance verification after deposits, and balance updates reflecting correctly in the Perps interface. Tests with multi-account setups and existing user states (non-first-time users). Entry point is via the Perps button in the TradeWalletActions menu. Integrates with SmokeTrade for the trading category entry and SmokeWalletPlatform for balance display.', + 'Tests perpetuals (perps) futures trading functionality on Arbitrum One. Covers the Add Funds flow to deposit USDC into the Perps trading account, balance verification after deposits, and balance updates reflecting correctly in the Perps interface. Tests with multi-account setups and existing user states (non-first-time users). Entry point is via the Perps button in the TradeWalletActions menu. Perps is also a section inside the Trending tab (SmokeWalletPlatform); changes to Perps views (headers, lists, full views, e.g. PerpsHomeView, PerpsMarketListView, PerpsWithdrawView) affect Trending. When selecting SmokePerps, also select SmokeWalletPlatform (Trending section) and SmokeConfirmations (Add Funds deposits are on-chain transactions). Integrates with SmokeTrade for the trading category entry.', }, smokeRamps: { tag: 'SmokeRamps:', description: - 'Tests fiat on-ramp (buy crypto) and off-ramp (sell crypto) features. Covers the off-ramp token amount input screen: direct amount entry via keypad, percentage quick-select buttons (25%, 50%, 75%, Max), and amount correction via delete. Tests region-aware on-ramp flows with mocked regional settings (e.g., France) and payment method availability. Validates deep link navigation into buy flows from external sources. On-ramp tests include limits validation and handling of unsupported networks. Integrates with SmokeWalletPlatform for wallet actions entry point.', + 'Tests fiat on-ramp (buy crypto) and off-ramp (sell crypto) features. Covers the off-ramp token amount input screen: direct amount entry via keypad, percentage quick-select buttons (25%, 50%, 75%, Max), and amount correction via delete. Tests region-aware on-ramp flows with mocked regional settings (e.g., France) and payment method availability. Validates deep link navigation into buy flows from external sources. On-ramp tests include limits validation and handling of unsupported networks. When changes touch wallet home or actions entry to buy/sell, also select SmokeWalletPlatform. Integrates with SmokeWalletPlatform for wallet actions entry point.', }, smokeMultiChainAPI: { tag: 'SmokeMultiChainAPI:', description: - 'Tests the CAIP-25 multi-chain session API for dApp permission management across networks. Covers wallet_createSession: creating sessions for single chains (Ethereum only), multiple EVM chains (Ethereum + Polygon + Base), and all EVM networks. Tests wallet_getSession for retrieving current session scopes and verifying session structure. Validates wallet_sessionChanged event emission when network permissions are modified. Tests wallet_revokeSession for completely clearing dApp sessions. Verifies session data consistency with EIP155 scope format for chain identification. Supports both EVM-only and EVM + Solana multi-chain scope combinations. Integrates with SmokeNetworkAbstractions for permission system and SmokeNetworkExpansion for multi-chain support.', + 'Tests the CAIP-25 multi-chain session API for dApp permission management across networks. Covers wallet_createSession: creating sessions for single chains (Ethereum only), multiple EVM chains (Ethereum + Polygon + Base), and all EVM networks. Tests wallet_getSession for retrieving current session scopes and verifying session structure. Validates wallet_sessionChanged event emission when network permissions are modified. Tests wallet_revokeSession for completely clearing dApp sessions. Verifies session data consistency with EIP155 scope format for chain identification. Supports both EVM-only and EVM + Solana multi-chain scope combinations. When selecting SmokeMultiChainAPI, also select SmokeNetworkAbstractions (permission UI) and SmokeNetworkExpansion (multi-chain provider). Integrates with SmokeNetworkAbstractions for permission system and SmokeNetworkExpansion for multi-chain support.', }, smokePredictions: { tag: 'SmokePredictions:', description: - 'Tests Polymarket prediction market integration. Covers the full position lifecycle: opening new positions on available markets (sports, crypto, events), cashing out open positions early, and claiming winnings from resolved markets. Tests balance synchronization after prediction transactions, verifying USDC balance updates correctly. Validates the Positions tab for viewing all open positions and the Activities tab for transaction history. Tests error handling scenarios: API failures when Polymarket is unavailable and geographic restriction handling for unsupported regions. Entry point is via the Predict button in TradeWalletActions. Integrates with SmokeWalletPlatform for Trending tab market discovery and SmokeTrade for the trading category.', + 'Tests Polymarket prediction market integration. Covers the full position lifecycle: opening new positions on available markets (sports, crypto, events), cashing out open positions early, and claiming winnings from resolved markets. Tests balance synchronization after prediction transactions, verifying USDC balance updates correctly. Validates the Positions tab for viewing all open positions and the Activities tab for transaction history. Tests error handling scenarios: API failures when Polymarket is unavailable and geographic restriction handling for unsupported regions. Entry point is via the Predict button in TradeWalletActions. Predictions is also a section inside the Trending tab (SmokeWalletPlatform); changes to Predictions views (headers, lists, full views) affect Trending. When selecting SmokePredictions, also select SmokeWalletPlatform (Trending section) and SmokeConfirmations (opening/closing positions are on-chain transactions). Integrates with SmokeTrade for the trading category.', }, }; diff --git a/locales/languages/en.json b/locales/languages/en.json index 6cf20cbfee4..cb2aae67357 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -115,6 +115,10 @@ "message": "You're sending your assets to a burn address. If you continue, you'll lose your assets.", "title": "Sending assets to burn address" }, + "gas_sponsorship_reserve_balance": { + "message": "Gas sponsorship isn't available for this transaction. You'll need to keep at least %{minBalance} %{nativeTokenSymbol} in your account.", + "title": "Gas sponsorship unavailable" + }, "token_trust_signal": { "malicious": { "message": "This token has been identified as malicious. Interacting with this token may result in a loss of funds.", @@ -4848,6 +4852,7 @@ "lowest_sell_limit": "lowest sell limit", "medium_sell_limit": "medium sell limit", "highest_sell_limit": "highest sell limit", + "change": "Change", "continue_to_amount": "Continue to amount", "no_payment_methods_title": "No payment methods in {{regionName}}", "no_cash_destinations_title": "No cash destinations in {{regionName}}", diff --git a/package.json b/package.json index c972be85b2c..bb2a87d6c4d 100644 --- a/package.json +++ b/package.json @@ -179,12 +179,12 @@ "@ethereumjs/util@npm:^9.0.2": "patch:@ethereumjs/util@npm%3A9.1.0#~/.yarn/patches/@ethereumjs-util-npm-9.1.0-7e85509408.patch", "@metamask/key-tree@npm:^10.1.1": "patch:@metamask/key-tree@npm%3A10.1.1#~/.yarn/patches/@metamask-key-tree-npm-10.1.1-0bfab435ac.patch", "@metamask/key-tree@npm:^10.0.2": "patch:@metamask/key-tree@npm%3A10.1.1#~/.yarn/patches/@metamask-key-tree-npm-10.1.1-0bfab435ac.patch", - "@metamask/transaction-controller@npm:^62.9.0": "patch:@metamask/transaction-controller@npm%3A62.9.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.9.0-5c8f871530.patch", + "@metamask/transaction-controller@npm:^62.9.0": "patch:@metamask/transaction-controller@npm%3A62.10.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch", "qs": "6.14.1", "@playwright/test": "^1.57.0", - "@metamask/transaction-controller@npm:^61.0.0": "patch:@metamask/transaction-controller@npm%3A62.9.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.9.0-5c8f871530.patch", - "@metamask/transaction-controller@npm:^62.9.2": "patch:@metamask/transaction-controller@npm%3A62.9.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.9.0-5c8f871530.patch", - "@metamask/transaction-controller@npm:^62.11.0": "patch:@metamask/transaction-controller@npm%3A62.9.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.9.0-5c8f871530.patch" + "@metamask/transaction-controller@npm:^61.0.0": "patch:@metamask/transaction-controller@npm%3A62.10.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch", + "@metamask/transaction-controller@npm:^62.9.2": "patch:@metamask/transaction-controller@npm%3A62.10.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch", + "@metamask/transaction-controller@npm:^62.11.0": "patch:@metamask/transaction-controller@npm%3A62.10.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch" }, "dependencies": { "@config-plugins/detox": "^9.0.0", @@ -268,7 +268,7 @@ "@metamask/preinstalled-example-snap": "^0.7.2", "@metamask/profile-metrics-controller": "^2.0.0", "@metamask/profile-sync-controller": "^27.0.0", - "@metamask/ramps-controller": "^5.0.0", + "@metamask/ramps-controller": "^6.0.0", "@metamask/react-native-acm": "^1.0.1", "@metamask/react-native-actionsheet": "2.4.2", "@metamask/react-native-button": "^3.0.0", @@ -285,7 +285,7 @@ "@metamask/selected-network-controller": "^25.0.0", "@metamask/signature-controller": "^35.0.0", "@metamask/slip44": "^4.2.0", - "@metamask/smart-transactions-controller": "^22.3.0", + "@metamask/smart-transactions-controller": "^22.4.0", "@metamask/snaps-controllers": "^17.2.1", "@metamask/snaps-execution-environments": "^10.4.1", "@metamask/snaps-rpc-methods": "^14.2.0", @@ -297,7 +297,7 @@ "@metamask/storage-service": "^1.0.0", "@metamask/swappable-obj-proxy": "^2.1.0", "@metamask/swaps-controller": "^15.0.0", - "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A62.9.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.9.0-5c8f871530.patch", + "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A62.10.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch", "@metamask/transaction-pay-controller": "^12.0.2", "@metamask/tron-wallet-snap": "^1.19.2", "@metamask/utils": "^11.8.1", diff --git a/scripts/aggregate-performance-reports.mjs b/scripts/aggregate-performance-reports.mjs index ba421fbed77..0afc134ba63 100755 --- a/scripts/aggregate-performance-reports.mjs +++ b/scripts/aggregate-performance-reports.mjs @@ -151,19 +151,29 @@ function extractPlatformScenarioAndDevice(filePath) { function processTestReport(testReport) { const cleanedReport = { testName: testReport.testName, + testFilePath: testReport.testFilePath || null, + tags: testReport.tags || [], steps: testReport.steps || [], totalTime: testReport.total, videoURL: testReport.videoURL || null, sessionId: testReport.sessionId || null, device: testReport.device || null, + // Include team information + team: testReport.team || null, // Include profiling data if available profilingData: testReport.profilingData || null, - profilingSummary: testReport.profilingSummary || null + profilingSummary: testReport.profilingSummary || null, + // Include quality gates if available + qualityGates: testReport.qualityGates || null, }; if (testReport.testFailed) { cleanedReport.testFailed = true; cleanedReport.failureReason = testReport.failureReason; + // Include quality gates violations for failed tests + if (testReport.qualityGates && !testReport.qualityGates.passed) { + cleanedReport.qualityGatesViolations = testReport.qualityGates.violations || null; + } } return cleanedReport; @@ -287,6 +297,10 @@ function createSummary(groupedResults) { let totalMemoryUsage = 0; let profilingTestCount = 0; + // First pass: collect all test executions grouped by unique test key + // This allows us to determine if a test ultimately passed (at least one retry succeeded) + const testExecutions = {}; // Key: testName|platform|device -> { passed: bool, failed: bool, testInfo: {} } + Object.keys(groupedResults).forEach(platform => { Object.keys(groupedResults[platform]).forEach(device => { devices.push(`${platform}-${device}`); @@ -308,10 +322,107 @@ function createSummary(groupedResults) { totalMemoryUsage += test.profilingSummary.memory?.avg || 0; profilingTestCount++; } + + // Track test executions by unique key (testName + platform + device) + const uniqueKey = `${test.testName}|${platform}|${device}`; + + if (!testExecutions[uniqueKey]) { + testExecutions[uniqueKey] = { + hasPassed: false, + hasFailed: false, + testInfo: { + testName: test.testName, + testFilePath: test.testFilePath, + tags: test.tags || [], + platform, + device, + team: test.team, + sessionId: test.sessionId || null, + videoURL: test.videoURL || null, + failureReason: test.failureReason, + qualityGates: test.qualityGates || null, + qualityGatesViolations: test.qualityGatesViolations || null, + } + }; + } + + // Track if this test has ever passed or failed + if (test.testFailed) { + testExecutions[uniqueKey].hasFailed = true; + // Update failure reason and recording info with the latest execution + testExecutions[uniqueKey].testInfo.failureReason = test.failureReason; + if (test.sessionId) testExecutions[uniqueKey].testInfo.sessionId = test.sessionId; + if (test.videoURL) testExecutions[uniqueKey].testInfo.videoURL = test.videoURL; + // Update quality gates info if available + if (test.qualityGates) { + testExecutions[uniqueKey].testInfo.qualityGates = test.qualityGates; + testExecutions[uniqueKey].testInfo.qualityGatesViolations = test.qualityGatesViolations; + } + } else { + testExecutions[uniqueKey].hasPassed = true; + } }); }); }); + // Second pass: determine final test status + // A test is only considered failed if ALL executions failed (no successful retry) + const failedTestsByTeam = {}; + let totalFailedTests = 0; + const failedTestsByPlatform = { android: 0, ios: 0 }; + + Object.values(testExecutions).forEach(execution => { + // If test passed at least once, it's considered passed (successful retry) + if (execution.hasPassed) { + return; // Skip - test ultimately passed + } + + // Test failed all executions - count as 1 failure + if (execution.hasFailed) { + totalFailedTests++; + + const { testInfo } = execution; + const platformKey = testInfo.platform.toLowerCase(); + + // Track per-platform failures + if (platformKey === 'android' || platformKey === 'ios') { + failedTestsByPlatform[platformKey]++; + } + + // Track by team if team info available + if (testInfo.team) { + const teamId = testInfo.team.teamId || 'unknown'; + if (!failedTestsByTeam[teamId]) { + failedTestsByTeam[teamId] = { + team: testInfo.team, + tests: [] + }; + } + + // Use quality_gates_exceeded when quality gates failed (for Slack "Quality gates FAILED") + const failureReason = + testInfo.qualityGates && + testInfo.qualityGates.hasThresholds && + !testInfo.qualityGates.passed + ? 'quality_gates_exceeded' + : (testInfo.failureReason ?? null); + + failedTestsByTeam[teamId].tests.push({ + testName: testInfo.testName, + testFilePath: testInfo.testFilePath, + tags: testInfo.tags, + platform: testInfo.platform, + device: testInfo.device, + sessionId: testInfo.sessionId ?? null, + recordingLink: testInfo.videoURL ?? null, + failureReason, + qualityGates: testInfo.qualityGates, + qualityGatesViolations: testInfo.qualityGatesViolations, + }); + } + } + }); + // Count tests by platform const platforms = {}; const testsByPlatform = {}; @@ -352,14 +463,22 @@ function createSummary(groupedResults) { avgMemoryUsage: `${avgMemoryUsage} MB`, profilingTestCount }, + // Failed tests grouped by team for Slack notifications + // Only includes tests that failed ALL retries (if a retry passed, test is not counted as failed) + failedTestsStats: { + totalFailedTests, + teamsAffected: Object.keys(failedTestsByTeam).length, + failedTestsByTeam + }, metadata: { generatedAt: new Date().toISOString(), totalReports: summaryDevices.length, platforms, jobResults: { - android: "success", // This would need to be determined from actual test results - ios: "success" + android: failedTestsByPlatform.android > 0 ? "failure" : "success", + ios: failedTestsByPlatform.ios > 0 ? "failure" : "success" }, + failedTestsByPlatform, branch: process.env.BRANCH_NAME || process.env.GITHUB_REF_NAME || 'unknown', commit: process.env.GITHUB_SHA || 'unknown', workflowRun: process.env.GITHUB_RUN_ID || 'unknown' diff --git a/scripts/generate-slack-summary.sh b/scripts/generate-slack-summary.sh index 2903edad5c8..36922db3f59 100755 --- a/scripts/generate-slack-summary.sh +++ b/scripts/generate-slack-summary.sh @@ -15,6 +15,10 @@ if [ -f "$SUMMARY_FILE" ]; then iosCount=$(jq -r '.platformDevices.iOS | length' "$SUMMARY_FILE") totalDevices=$((androidCount + iosCount)) + # Get failed tests statistics + totalFailedTests=$(jq -r '.failedTestsStats.totalFailedTests // 0' "$SUMMARY_FILE") + teamsAffected=$(jq -r '.failedTestsStats.teamsAffected // 0' "$SUMMARY_FILE") + # Determine test results status by checking job statuses via GitHub API if [ -n "$GITHUB_TOKEN" ] && [ -n "$GITHUB_RUN_ID" ]; then @@ -104,6 +108,51 @@ if [ -f "$SUMMARY_FILE" ]; then SUMMARY+=" • Android: $androidImportedWalletStatus\n" SUMMARY+=" • iOS: $iosImportedWalletStatus\n\n" SUMMARY+="---------------\n\n" + + # Add failed tests section: one line per unique test name, grouped by team. + # Same test failing on Android and iOS shows once with "Failed on Android & iOS" and both recording links. + if [ "$totalFailedTests" -gt 0 ]; then + SUMMARY+="*Failed tests:*\n\n" + + prevMention="" + while IFS= read -r line; do + [ -z "$line" ] && continue + mention=$(echo "$line" | cut -f1) + name=$(echo "$line" | cut -f2) + reasonDisplay=$(echo "$line" | cut -f3) + platformsLabel=$(echo "$line" | cut -f4) + recordings=$(echo "$line" | cut -f5-) + + if [ "$mention" != "$prevMention" ]; then + SUMMARY+="${mention}:\n" + prevMention="$mention" + fi + + if [ -n "$platformsLabel" ]; then + SUMMARY+=" - ${name} - ${reasonDisplay} -> Failed on ${platformsLabel}: ${recordings}\n" + else + SUMMARY+=" - ${name} - ${reasonDisplay} -> ${recordings}\n" + fi + done <<< "$(jq -r ' + .failedTestsStats.failedTestsByTeam | to_entries[] | + .value.team.slackMention as $mention | + (.value.tests | group_by(.testName)[] | + (.[0].testName) as $name | + (.[0].failureReason) as $reason | + ([.[].platform] | unique) as $platforms | + ($platforms | join(" & ")) as $platformsLabel | + ([.[] | + (if .device != null and .device != "" then (if (.device | type) == "object" then ((.device.name // "") + (if .device.osVersion != null and .device.osVersion != "" then " (" + .device.osVersion + ")" else "" end)) else (.device | tostring) end) else .platform end) as $deviceLabel | + if .recordingLink != null and .recordingLink != "" then "<" + .recordingLink + "|Recording (" + $deviceLabel + ")>" else (if .sessionId != null and .sessionId != "" then "Recording (" + $deviceLabel + ") (session: " + .sessionId + ")" else "—" end) end + ] | join(" · ")) as $recordings | + ($reason | if . == "quality_gates_exceeded" then "Quality gates FAILED" elif . == "timedOut" then "Test timed out" elif . == "test_error" or . == "failed" then "Test error" else . end) as $reasonDisplay | + [$mention, $name, $reasonDisplay, $platformsLabel, $recordings] | @tsv + ) + ' "$SUMMARY_FILE" 2>/dev/null)" + + SUMMARY+="\n---------------\n\n" + fi + SUMMARY+="*Build Info:*\n" SUMMARY+="• Commit Hash: \`$GITHUB_SHA\`\n" SUMMARY+="• Branch: \`$GITHUB_REF_NAME\`\n" diff --git a/e2e/specs/confirmations/regression/new-networks-signatures.spec.ts b/tests/regression/confirmations/new-networks-signatures.spec.ts similarity index 71% rename from e2e/specs/confirmations/regression/new-networks-signatures.spec.ts rename to tests/regression/confirmations/new-networks-signatures.spec.ts index 0a8279b8ba8..7fcc9ff1879 100644 --- a/e2e/specs/confirmations/regression/new-networks-signatures.spec.ts +++ b/tests/regression/confirmations/new-networks-signatures.spec.ts @@ -1,17 +1,17 @@ -import Assertions from '../../../../tests/framework/Assertions'; -import Browser from '../../../pages/Browser/BrowserView'; -import FooterActions from '../../../pages/Browser/Confirmations/FooterActions'; -import FixtureBuilder from '../../../../tests/framework/fixtures/FixtureBuilder'; -import RequestTypes from '../../../pages/Browser/Confirmations/RequestTypes'; -import TestDApp from '../../../pages/Browser/TestDApp'; -import { loginToApp, navigateToBrowserView } from '../../../viewHelper'; -import { withFixtures } from '../../../../tests/framework/fixtures/FixtureHelper'; -import { RegressionConfirmations } from '../../../tags'; -import { buildPermissions } from '../../../../tests/framework/fixtures/FixtureUtils'; -import RowComponents from '../../../pages/Browser/Confirmations/RowComponents'; -import { DappVariants } from '../../../../tests/framework/Constants'; +import Assertions from '../../framework/Assertions'; +import Browser from '../../../e2e/pages/Browser/BrowserView'; +import FooterActions from '../../../e2e/pages/Browser/Confirmations/FooterActions'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; +import RequestTypes from '../../../e2e/pages/Browser/Confirmations/RequestTypes'; +import TestDApp from '../../../e2e/pages/Browser/TestDApp'; +import { loginToApp, navigateToBrowserView } from '../../../e2e/viewHelper'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper'; +import { RegressionConfirmations } from '../../../e2e/tags'; +import { buildPermissions } from '../../framework/fixtures/FixtureUtils'; +import RowComponents from '../../../e2e/pages/Browser/Confirmations/RowComponents'; +import { DappVariants } from '../../framework/Constants'; -import { NETWORK_TEST_CONFIGS } from '../../../../tests/resources/mock-configs'; +import { NETWORK_TEST_CONFIGS } from '../../resources/mock-configs'; const SIGNATURE_LIST = [ { diff --git a/e2e/specs/send/metricsValidationHelper.ts b/tests/smoke/confirmations/send/metricsValidationHelper.ts similarity index 88% rename from e2e/specs/send/metricsValidationHelper.ts rename to tests/smoke/confirmations/send/metricsValidationHelper.ts index a457b900ffb..5990d57b999 100644 --- a/e2e/specs/send/metricsValidationHelper.ts +++ b/tests/smoke/confirmations/send/metricsValidationHelper.ts @@ -1,7 +1,7 @@ import { Mockttp } from 'mockttp'; -import { AnvilManager } from '../../../tests/seeder/anvil-manager'; -import { LocalNode } from '../../../tests/framework'; -import { getEventsPayloads } from '../../../tests/helpers/analytics/helpers'; +import { AnvilManager } from '../../../seeder/anvil-manager'; +import { LocalNode } from '../../../framework'; +import { getEventsPayloads } from '../../../helpers/analytics/helpers'; export const validateTransactionHashInTransactionFinalizedEvent = async ( localNodes?: LocalNode[], diff --git a/e2e/specs/send/send-btc-token.spec.ts b/tests/smoke/confirmations/send/send-btc-token.spec.ts similarity index 52% rename from e2e/specs/send/send-btc-token.spec.ts rename to tests/smoke/confirmations/send/send-btc-token.spec.ts index 995b1d0faf4..0c1b8b388ab 100644 --- a/e2e/specs/send/send-btc-token.spec.ts +++ b/tests/smoke/confirmations/send/send-btc-token.spec.ts @@ -1,10 +1,10 @@ -import SendView from '../../pages/Send/RedesignedSendView'; -import TokenOverview from '../../pages/wallet/TokenOverview'; -import WalletView from '../../pages/wallet/WalletView'; -import { SmokeConfirmations } from '../../tags'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { loginToApp } from '../../viewHelper'; +import SendView from '../../../../e2e/pages/Send/RedesignedSendView'; +import TokenOverview from '../../../../e2e/pages/wallet/TokenOverview'; +import WalletView from '../../../../e2e/pages/wallet/WalletView'; +import { SmokeConfirmations } from '../../../../e2e/tags'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import { loginToApp } from '../../../../e2e/viewHelper.ts'; describe(SmokeConfirmations('Send Bitcoin'), () => { it('shows insufficient funds', async () => { diff --git a/e2e/specs/send/send-erc20-token.spec.ts b/tests/smoke/confirmations/send/send-erc20-token.spec.ts similarity index 90% rename from e2e/specs/send/send-erc20-token.spec.ts rename to tests/smoke/confirmations/send/send-erc20-token.spec.ts index dbf76232e16..bb0f04fb8c5 100644 --- a/e2e/specs/send/send-erc20-token.spec.ts +++ b/tests/smoke/confirmations/send/send-erc20-token.spec.ts @@ -1,13 +1,13 @@ -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import FooterActions from '../../pages/Browser/Confirmations/FooterActions'; -import SendView from '../../pages/Send/RedesignedSendView'; -import WalletView from '../../pages/wallet/WalletView'; -import { DappVariants } from '../../../tests/framework/Constants'; -import { SmokeConfirmations } from '../../tags'; -import { AnvilPort } from '../../../tests/framework/fixtures/FixtureUtils'; -import { loginToApp } from '../../viewHelper'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import { AnvilManager } from '../../../tests/seeder/anvil-manager'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import FooterActions from '../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import SendView from '../../../../e2e/pages/Send/RedesignedSendView'; +import WalletView from '../../../../e2e/pages/wallet/WalletView'; +import { DappVariants } from '../../../framework/Constants'; +import { SmokeConfirmations } from '../../../../e2e/tags'; +import { AnvilPort } from '../../../framework/fixtures/FixtureUtils'; +import { loginToApp } from '../../../../e2e/viewHelper'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import { AnvilManager } from '../../../seeder/anvil-manager'; const RECIPIENT = '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb'; diff --git a/e2e/specs/send/send-native-token.spec.ts b/tests/smoke/confirmations/send/send-native-token.spec.ts similarity index 76% rename from e2e/specs/send/send-native-token.spec.ts rename to tests/smoke/confirmations/send/send-native-token.spec.ts index a7f91b59017..7ef3a55e513 100644 --- a/e2e/specs/send/send-native-token.spec.ts +++ b/tests/smoke/confirmations/send/send-native-token.spec.ts @@ -1,21 +1,18 @@ -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import FooterActions from '../../pages/Browser/Confirmations/FooterActions'; -import SendView from '../../pages/Send/RedesignedSendView'; -import TabBarComponent from '../../pages/wallet/TabBarComponent'; -import WalletView from '../../pages/wallet/WalletView'; -import { Assertions } from '../../../tests/framework'; -import { - DappVariants, - LOCAL_NODE_RPC_URL, -} from '../../../tests/framework/Constants'; -import { SmokeConfirmations } from '../../tags'; -import { loginToApp } from '../../viewHelper'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import { LocalNode } from '../../../tests/framework/types'; -import { setupRemoteFeatureFlagsMock } from '../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { remoteFeatureFlagExtensionUxPna25 } from '../../../tests/api-mocking/mock-responses/feature-flags-mocks'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import FooterActions from '../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import SendView from '../../../../e2e/pages/Send/RedesignedSendView'; +import TabBarComponent from '../../../../e2e/pages/wallet/TabBarComponent'; +import WalletView from '../../../../e2e/pages/wallet/WalletView'; +import { Assertions } from '../../../framework'; +import { DappVariants, LOCAL_NODE_RPC_URL } from '../../../framework/Constants'; +import { SmokeConfirmations } from '../../../../e2e/tags'; +import { loginToApp } from '../../../../e2e/viewHelper'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import { LocalNode } from '../../../framework/types'; +import { setupRemoteFeatureFlagsMock } from '../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { remoteFeatureFlagExtensionUxPna25 } from '../../../api-mocking/mock-responses/feature-flags-mocks'; import { Mockttp } from 'mockttp'; -import { setupMockRequest } from '../../../tests/api-mocking/helpers/mockHelpers'; +import { setupMockRequest } from '../../../api-mocking/helpers/mockHelpers'; import { validateTransactionHashInTransactionFinalizedEvent } from './metricsValidationHelper'; const RECIPIENT = '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb'; diff --git a/e2e/specs/send/send-solana-token.spec.ts b/tests/smoke/confirmations/send/send-solana-token.spec.ts similarity index 57% rename from e2e/specs/send/send-solana-token.spec.ts rename to tests/smoke/confirmations/send/send-solana-token.spec.ts index 36c286b8c16..d783e660c9f 100644 --- a/e2e/specs/send/send-solana-token.spec.ts +++ b/tests/smoke/confirmations/send/send-solana-token.spec.ts @@ -1,11 +1,11 @@ -import SendView from '../../pages/Send/RedesignedSendView'; -import SolanaTestDApp from '../../pages/Browser/SolanaTestDApp'; -import TokenOverview from '../../pages/wallet/TokenOverview'; -import WalletView from '../../pages/wallet/WalletView'; -import { SmokeConfirmations } from '../../tags'; -import { loginToApp } from '../../viewHelper'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; +import SendView from '../../../../e2e/pages/Send/RedesignedSendView'; +import SolanaTestDApp from '../../../../e2e/pages/Browser/SolanaTestDApp'; +import TokenOverview from '../../../../e2e/pages/wallet/TokenOverview'; +import WalletView from '../../../../e2e/pages/wallet/WalletView'; +import { SmokeConfirmations } from '../../../../e2e/tags'; +import { loginToApp } from '../../../../e2e/viewHelper'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; const RECIPIENT = '4Nd1mZyJY5ZqzR3n8bQF7h5L2Q9gY1yTtM6nQhc7P1Dp'; diff --git a/e2e/specs/send/send-tron-token.spec.ts b/tests/smoke/confirmations/send/send-tron-token.spec.ts similarity index 53% rename from e2e/specs/send/send-tron-token.spec.ts rename to tests/smoke/confirmations/send/send-tron-token.spec.ts index 22057d72e2a..f8e5b1942d4 100644 --- a/e2e/specs/send/send-tron-token.spec.ts +++ b/tests/smoke/confirmations/send/send-tron-token.spec.ts @@ -1,10 +1,10 @@ -import SendView from '../../pages/Send/RedesignedSendView'; -import TokenOverview from '../../pages/wallet/TokenOverview'; -import WalletView from '../../pages/wallet/WalletView'; -import { SmokeConfirmations } from '../../tags'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { loginToApp } from '../../viewHelper'; +import SendView from '../../../../e2e/pages/Send/RedesignedSendView'; +import TokenOverview from '../../../../e2e/pages/wallet/TokenOverview'; +import WalletView from '../../../../e2e/pages/wallet/WalletView'; +import { SmokeConfirmations } from '../../../../e2e/tags'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import { loginToApp } from '../../../../e2e/viewHelper'; describe(SmokeConfirmations('Send TRX token'), () => { it.skip('shows insufficient funds', async () => { diff --git a/e2e/specs/confirmations/signatures/alert-system.spec.ts b/tests/smoke/confirmations/signatures/alert-system.spec.ts similarity index 86% rename from e2e/specs/confirmations/signatures/alert-system.spec.ts rename to tests/smoke/confirmations/signatures/alert-system.spec.ts index f1c45ac789e..2f5ca6f7749 100644 --- a/e2e/specs/confirmations/signatures/alert-system.spec.ts +++ b/tests/smoke/confirmations/signatures/alert-system.spec.ts @@ -1,29 +1,29 @@ -import Assertions from '../../../../tests/framework/Assertions'; -import Browser from '../../../pages/Browser/BrowserView'; -import FixtureBuilder from '../../../../tests/framework/fixtures/FixtureBuilder'; -import RequestTypes from '../../../pages/Browser/Confirmations/RequestTypes'; -import AlertSystem from '../../../pages/Browser/Confirmations/AlertSystem'; -import TestDApp from '../../../pages/Browser/TestDApp'; -import { loginToApp, navigateToBrowserView } from '../../../viewHelper'; -import { SmokeConfirmations } from '../../../tags'; -import { withFixtures } from '../../../../tests/framework/fixtures/FixtureHelper'; -import FooterActions from '../../../pages/Browser/Confirmations/FooterActions'; +import Assertions from '../../../framework/Assertions'; +import Browser from '../../../../e2e/pages/Browser/BrowserView'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import RequestTypes from '../../../../e2e/pages/Browser/Confirmations/RequestTypes'; +import AlertSystem from '../../../../e2e/pages/Browser/Confirmations/AlertSystem'; +import TestDApp from '../../../../e2e/pages/Browser/TestDApp'; +import { loginToApp, navigateToBrowserView } from '../../../../e2e/viewHelper'; +import { SmokeConfirmations } from '../../../../e2e/tags'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import FooterActions from '../../../../e2e/pages/Browser/Confirmations/FooterActions'; import { buildPermissions, getTestDappLocalUrl, -} from '../../../../tests/framework/fixtures/FixtureUtils'; -import { DappVariants } from '../../../../tests/framework/Constants'; +} from '../../../framework/fixtures/FixtureUtils'; +import { DappVariants } from '../../../framework/Constants'; import { Mockttp } from 'mockttp'; import { setupMockRequest, setupMockPostRequest, -} from '../../../../tests/api-mocking/helpers/mockHelpers'; +} from '../../../api-mocking/helpers/mockHelpers'; import { SECURITY_ALERTS_BENIGN_RESPONSE, securityAlertsUrl, -} from '../../../../tests/api-mocking/mock-responses/security-alerts-mock'; -import { setupRemoteFeatureFlagsMock } from '../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { confirmationFeatureFlags } from '../../../../tests/api-mocking/mock-responses/feature-flags-mocks'; +} from '../../../api-mocking/mock-responses/security-alerts-mock'; +import { setupRemoteFeatureFlagsMock } from '../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { confirmationFeatureFlags } from '../../../api-mocking/mock-responses/feature-flags-mocks'; const typedSignRequestBody = { method: 'eth_signTypedData', diff --git a/e2e/specs/confirmations/signatures/signatures-typed.spec.ts b/tests/smoke/confirmations/signatures/signatures-typed.spec.ts similarity index 74% rename from e2e/specs/confirmations/signatures/signatures-typed.spec.ts rename to tests/smoke/confirmations/signatures/signatures-typed.spec.ts index 319c069eaf8..273f969eb7a 100644 --- a/e2e/specs/confirmations/signatures/signatures-typed.spec.ts +++ b/tests/smoke/confirmations/signatures/signatures-typed.spec.ts @@ -1,23 +1,23 @@ -import Assertions from '../../../../tests/framework/Assertions'; -import Browser from '../../../pages/Browser/BrowserView'; -import FooterActions from '../../../pages/Browser/Confirmations/FooterActions'; -import FixtureBuilder from '../../../../tests/framework/fixtures/FixtureBuilder'; -import RequestTypes from '../../../pages/Browser/Confirmations/RequestTypes'; -import TestDApp from '../../../pages/Browser/TestDApp'; -import { loginToApp, navigateToBrowserView } from '../../../viewHelper'; -import { withFixtures } from '../../../../tests/framework/fixtures/FixtureHelper'; -import { SmokeConfirmations } from '../../../tags'; +import Assertions from '../../../framework/Assertions'; +import Browser from '../../../../e2e/pages/Browser/BrowserView'; +import FooterActions from '../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import RequestTypes from '../../../../e2e/pages/Browser/Confirmations/RequestTypes'; +import TestDApp from '../../../../e2e/pages/Browser/TestDApp'; +import { loginToApp, navigateToBrowserView } from '../../../../e2e/viewHelper'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import { SmokeConfirmations } from '../../../../e2e/tags'; import { buildPermissions, AnvilPort, -} from '../../../../tests/framework/fixtures/FixtureUtils'; -import RowComponents from '../../../pages/Browser/Confirmations/RowComponents'; -import { DappVariants } from '../../../../tests/framework/Constants'; +} from '../../../framework/fixtures/FixtureUtils'; +import RowComponents from '../../../../e2e/pages/Browser/Confirmations/RowComponents'; +import { DappVariants } from '../../../framework/Constants'; import { Mockttp } from 'mockttp'; -import { setupRemoteFeatureFlagsMock } from '../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { confirmationFeatureFlags } from '../../../../tests/api-mocking/mock-responses/feature-flags-mocks'; -import { LocalNode } from '../../../../tests/framework/types'; -import { AnvilManager } from '../../../../tests/seeder/anvil-manager'; +import { setupRemoteFeatureFlagsMock } from '../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { confirmationFeatureFlags } from '../../../api-mocking/mock-responses/feature-flags-mocks'; +import { LocalNode } from '../../../framework/types'; +import { AnvilManager } from '../../../seeder/anvil-manager'; const SIGNATURE_LIST = [ { diff --git a/e2e/specs/confirmations/signatures/signatures.spec.ts b/tests/smoke/confirmations/signatures/signatures.spec.ts similarity index 73% rename from e2e/specs/confirmations/signatures/signatures.spec.ts rename to tests/smoke/confirmations/signatures/signatures.spec.ts index 258f04c3889..7e91871046c 100644 --- a/e2e/specs/confirmations/signatures/signatures.spec.ts +++ b/tests/smoke/confirmations/signatures/signatures.spec.ts @@ -1,23 +1,23 @@ -import Assertions from '../../../../tests/framework/Assertions'; -import Browser from '../../../pages/Browser/BrowserView'; -import FooterActions from '../../../pages/Browser/Confirmations/FooterActions'; -import FixtureBuilder from '../../../../tests/framework/fixtures/FixtureBuilder'; -import RequestTypes from '../../../pages/Browser/Confirmations/RequestTypes'; -import TestDApp from '../../../pages/Browser/TestDApp'; -import { loginToApp, navigateToBrowserView } from '../../../viewHelper'; -import { withFixtures } from '../../../../tests/framework/fixtures/FixtureHelper'; -import { SmokeConfirmations } from '../../../tags'; +import Assertions from '../../../framework/Assertions'; +import Browser from '../../../../e2e/pages/Browser/BrowserView'; +import FooterActions from '../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import RequestTypes from '../../../../e2e/pages/Browser/Confirmations/RequestTypes'; +import TestDApp from '../../../../e2e/pages/Browser/TestDApp'; +import { loginToApp, navigateToBrowserView } from '../../../../e2e/viewHelper'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import { SmokeConfirmations } from '../../../../e2e/tags'; import { buildPermissions, AnvilPort, -} from '../../../../tests/framework/fixtures/FixtureUtils'; -import RowComponents from '../../../pages/Browser/Confirmations/RowComponents'; -import { DappVariants } from '../../../../tests/framework/Constants'; +} from '../../../framework/fixtures/FixtureUtils'; +import RowComponents from '../../../../e2e/pages/Browser/Confirmations/RowComponents'; +import { DappVariants } from '../../../framework/Constants'; import { Mockttp } from 'mockttp'; -import { setupRemoteFeatureFlagsMock } from '../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { confirmationFeatureFlags } from '../../../../tests/api-mocking/mock-responses/feature-flags-mocks'; -import { LocalNode } from '../../../../tests/framework/types'; -import { AnvilManager } from '../../../../tests/seeder/anvil-manager'; +import { setupRemoteFeatureFlagsMock } from '../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { confirmationFeatureFlags } from '../../../api-mocking/mock-responses/feature-flags-mocks'; +import { LocalNode } from '../../../framework/types'; +import { AnvilManager } from '../../../seeder/anvil-manager'; const SIGNATURE_LIST = [ { diff --git a/e2e/specs/confirmations/transactions/7702/batch-transaction.spec.ts b/tests/smoke/confirmations/transactions/7702/batch-transaction.spec.ts similarity index 79% rename from e2e/specs/confirmations/transactions/7702/batch-transaction.spec.ts rename to tests/smoke/confirmations/transactions/7702/batch-transaction.spec.ts index 89683d26a0b..03633079aff 100644 --- a/e2e/specs/confirmations/transactions/7702/batch-transaction.spec.ts +++ b/tests/smoke/confirmations/transactions/7702/batch-transaction.spec.ts @@ -1,35 +1,38 @@ -import AccountDetails from '../../../../pages/MultichainAccounts/AccountDetails'; -import AccountListBottomSheet from '../../../../pages/wallet/AccountListBottomSheet'; -import Assertions from '../../../../../tests/framework/Assertions'; -import BrowserView from '../../../../pages/Browser/BrowserView'; -import ConfirmationUITypes from '../../../../pages/Browser/Confirmations/ConfirmationUITypes'; -import FixtureBuilder from '../../../../../tests/framework/fixtures/FixtureBuilder'; -import FooterActions from '../../../../pages/Browser/Confirmations/FooterActions'; -import NetworkListModal from '../../../../pages/Network/NetworkListModal'; -import RowComponents from '../../../../pages/Browser/Confirmations/RowComponents'; -import SwitchAccountModal from '../../../../pages/wallet/SwitchAccountModal'; -import TabBarComponent from '../../../../pages/wallet/TabBarComponent'; -import TestDApp from '../../../../pages/Browser/TestDApp'; -import WalletView from '../../../../pages/wallet/WalletView'; -import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../../tests/api-mocking/mock-responses/simulations'; +import AccountDetails from '../../../../../e2e/pages/MultichainAccounts/AccountDetails'; +import AccountListBottomSheet from '../../../../../e2e/pages/wallet/AccountListBottomSheet'; +import Assertions from '../../../../framework/Assertions'; +import BrowserView from '../../../../../e2e/pages/Browser/BrowserView'; +import ConfirmationUITypes from '../../../../../e2e/pages/Browser/Confirmations/ConfirmationUITypes'; +import FixtureBuilder from '../../../../framework/fixtures/FixtureBuilder'; +import FooterActions from '../../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import NetworkListModal from '../../../../../e2e/pages/Network/NetworkListModal'; +import RowComponents from '../../../../../e2e/pages/Browser/Confirmations/RowComponents'; +import SwitchAccountModal from '../../../../../e2e/pages/wallet/SwitchAccountModal'; +import TabBarComponent from '../../../../../e2e/pages/wallet/TabBarComponent'; +import TestDApp from '../../../../../e2e/pages/Browser/TestDApp'; +import WalletView from '../../../../../e2e/pages/wallet/WalletView'; +import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../api-mocking/mock-responses/simulations'; import { AnvilPort, buildPermissions, -} from '../../../../../tests/framework/fixtures/FixtureUtils'; -import { loginToApp, navigateToBrowserView } from '../../../../viewHelper'; -import { SmokeConfirmations } from '../../../../tags'; -import { withFixtures } from '../../../../../tests/framework/fixtures/FixtureHelper'; -import { DappVariants } from '../../../../../tests/framework/Constants'; +} from '../../../../framework/fixtures/FixtureUtils'; +import { + loginToApp, + navigateToBrowserView, +} from '../../../../../e2e/viewHelper'; +import { SmokeConfirmations } from '../../../../../e2e/tags'; +import { withFixtures } from '../../../../framework/fixtures/FixtureHelper'; +import { DappVariants } from '../../../../framework/Constants'; import { AnvilNodeOptions, LocalNode, LocalNodeType, -} from '../../../../../tests/framework'; +} from '../../../../framework'; import { Mockttp } from 'mockttp'; -import { setupMockRequest } from '../../../../../tests/api-mocking/helpers/mockHelpers'; -import { confirmationFeatureFlags } from '../../../../../tests/api-mocking/mock-responses/feature-flags-mocks'; -import { setupRemoteFeatureFlagsMock } from '../../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { AnvilManager } from '../../../../../tests/seeder/anvil-manager'; +import { setupMockRequest } from '../../../../api-mocking/helpers/mockHelpers'; +import { confirmationFeatureFlags } from '../../../../api-mocking/mock-responses/feature-flags-mocks'; +import { setupRemoteFeatureFlagsMock } from '../../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { AnvilManager } from '../../../../seeder/anvil-manager'; const LOCAL_CHAIN_NAME = 'Local RPC'; @@ -39,7 +42,7 @@ const localNodeOptions = [ options: { hardfork: 'prague', loadState: - './e2e/specs/confirmations/transactions/7702/withDelegatorContracts.json', + './tests/smoke/confirmations/transactions/7702/withDelegatorContracts.json', }, }, ]; diff --git a/e2e/specs/confirmations/transactions/7702/withDelegatorContracts.json b/tests/smoke/confirmations/transactions/7702/withDelegatorContracts.json similarity index 100% rename from e2e/specs/confirmations/transactions/7702/withDelegatorContracts.json rename to tests/smoke/confirmations/transactions/7702/withDelegatorContracts.json diff --git a/e2e/specs/confirmations/transactions/contract-deployment.spec.ts b/tests/smoke/confirmations/transactions/contract-deployment.spec.ts similarity index 67% rename from e2e/specs/confirmations/transactions/contract-deployment.spec.ts rename to tests/smoke/confirmations/transactions/contract-deployment.spec.ts index 8a97e552b1e..dff362fcfcb 100644 --- a/e2e/specs/confirmations/transactions/contract-deployment.spec.ts +++ b/tests/smoke/confirmations/transactions/contract-deployment.spec.ts @@ -1,26 +1,26 @@ -import { SmokeConfirmations } from '../../../tags'; -import { loginToApp, navigateToBrowserView } from '../../../viewHelper'; -import Browser from '../../../pages/Browser/BrowserView'; -import FixtureBuilder from '../../../../tests/framework/fixtures/FixtureBuilder'; -import TabBarComponent from '../../../pages/wallet/TabBarComponent'; -import ConfirmationUITypes from '../../../pages/Browser/Confirmations/ConfirmationUITypes'; -import FooterActions from '../../../pages/Browser/Confirmations/FooterActions'; -import Assertions from '../../../../tests/framework/Assertions'; -import { withFixtures } from '../../../../tests/framework/fixtures/FixtureHelper'; +import { SmokeConfirmations } from '../../../../e2e/tags'; +import { loginToApp, navigateToBrowserView } from '../../../../e2e/viewHelper'; +import Browser from '../../../../e2e/pages/Browser/BrowserView'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import TabBarComponent from '../../../../e2e/pages/wallet/TabBarComponent'; +import ConfirmationUITypes from '../../../../e2e/pages/Browser/Confirmations/ConfirmationUITypes'; +import FooterActions from '../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import Assertions from '../../../framework/Assertions'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; import { buildPermissions, AnvilPort, -} from '../../../../tests/framework/fixtures/FixtureUtils'; -import RowComponents from '../../../pages/Browser/Confirmations/RowComponents'; -import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../tests/api-mocking/mock-responses/simulations'; -import TestDApp from '../../../pages/Browser/TestDApp'; -import { DappVariants } from '../../../../tests/framework/Constants'; +} from '../../../framework/fixtures/FixtureUtils'; +import RowComponents from '../../../../e2e/pages/Browser/Confirmations/RowComponents'; +import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../api-mocking/mock-responses/simulations'; +import TestDApp from '../../../../e2e/pages/Browser/TestDApp'; +import { DappVariants } from '../../../framework/Constants'; import { Mockttp } from 'mockttp'; -import { setupMockRequest } from '../../../../tests/api-mocking/helpers/mockHelpers'; -import { setupRemoteFeatureFlagsMock } from '../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { confirmationFeatureFlags } from '../../../../tests/api-mocking/mock-responses/feature-flags-mocks'; -import { LocalNode } from '../../../../tests/framework/types'; -import { AnvilManager } from '../../../../tests/seeder/anvil-manager'; +import { setupMockRequest } from '../../../api-mocking/helpers/mockHelpers'; +import { setupRemoteFeatureFlagsMock } from '../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { confirmationFeatureFlags } from '../../../api-mocking/mock-responses/feature-flags-mocks'; +import { LocalNode } from '../../../framework/types'; +import { AnvilManager } from '../../../seeder/anvil-manager'; describe(SmokeConfirmations('Contract Deployment'), () => { const testSpecificMock = async (mockServer: Mockttp) => { diff --git a/e2e/specs/confirmations/transactions/contract-interaction.spec.ts b/tests/smoke/confirmations/transactions/contract-interaction.spec.ts similarity index 70% rename from e2e/specs/confirmations/transactions/contract-interaction.spec.ts rename to tests/smoke/confirmations/transactions/contract-interaction.spec.ts index 8ba48ac77ee..94da8715c5c 100644 --- a/e2e/specs/confirmations/transactions/contract-interaction.spec.ts +++ b/tests/smoke/confirmations/transactions/contract-interaction.spec.ts @@ -1,27 +1,27 @@ import { SMART_CONTRACTS } from '../../../../app/util/test/smart-contracts'; -import { SmokeConfirmations } from '../../../tags'; -import { loginToApp, navigateToBrowserView } from '../../../viewHelper'; -import FixtureBuilder from '../../../../tests/framework/fixtures/FixtureBuilder'; -import TabBarComponent from '../../../pages/wallet/TabBarComponent'; -import ConfirmationUITypes from '../../../pages/Browser/Confirmations/ConfirmationUITypes'; -import FooterActions from '../../../pages/Browser/Confirmations/FooterActions'; -import Assertions from '../../../../tests/framework/Assertions'; -import { withFixtures } from '../../../../tests/framework/fixtures/FixtureHelper'; +import { SmokeConfirmations } from '../../../../e2e/tags'; +import { loginToApp, navigateToBrowserView } from '../../../../e2e/viewHelper'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import TabBarComponent from '../../../../e2e/pages/wallet/TabBarComponent'; +import ConfirmationUITypes from '../../../../e2e/pages/Browser/Confirmations/ConfirmationUITypes'; +import FooterActions from '../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import Assertions from '../../../framework/Assertions'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; import { buildPermissions, AnvilPort, -} from '../../../../tests/framework/fixtures/FixtureUtils'; -import RowComponents from '../../../pages/Browser/Confirmations/RowComponents'; -import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../tests/api-mocking/mock-responses/simulations'; -import TestDApp from '../../../pages/Browser/TestDApp'; -import { DappVariants } from '../../../../tests/framework/Constants'; +} from '../../../framework/fixtures/FixtureUtils'; +import RowComponents from '../../../../e2e/pages/Browser/Confirmations/RowComponents'; +import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../api-mocking/mock-responses/simulations'; +import TestDApp from '../../../../e2e/pages/Browser/TestDApp'; +import { DappVariants } from '../../../framework/Constants'; import { Mockttp } from 'mockttp'; -import { setupMockRequest } from '../../../../tests/api-mocking/helpers/mockHelpers'; -import { setupRemoteFeatureFlagsMock } from '../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { confirmationFeatureFlags } from '../../../../tests/api-mocking/mock-responses/feature-flags-mocks'; -import { LocalNode } from '../../../../tests/framework/types'; -import { AnvilManager } from '../../../../tests/seeder/anvil-manager'; -import Browser from '../../../pages/Browser/BrowserView'; +import { setupMockRequest } from '../../../api-mocking/helpers/mockHelpers'; +import { setupRemoteFeatureFlagsMock } from '../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { confirmationFeatureFlags } from '../../../api-mocking/mock-responses/feature-flags-mocks'; +import { LocalNode } from '../../../framework/types'; +import { AnvilManager } from '../../../seeder/anvil-manager'; +import Browser from '../../../../e2e/pages/Browser/BrowserView'; describe(SmokeConfirmations('Contract Interaction'), () => { const NFT_CONTRACT = SMART_CONTRACTS.NFTS; diff --git a/e2e/specs/confirmations/transactions/dapp-initiated-transfer.spec.ts b/tests/smoke/confirmations/transactions/dapp-initiated-transfer.spec.ts similarity index 88% rename from e2e/specs/confirmations/transactions/dapp-initiated-transfer.spec.ts rename to tests/smoke/confirmations/transactions/dapp-initiated-transfer.spec.ts index 2979c4f14ab..f3d107a8e38 100644 --- a/e2e/specs/confirmations/transactions/dapp-initiated-transfer.spec.ts +++ b/tests/smoke/confirmations/transactions/dapp-initiated-transfer.spec.ts @@ -1,43 +1,43 @@ -import { SmokeConfirmations } from '../../../tags'; -import { loginToApp, navigateToBrowserView } from '../../../viewHelper'; -import Browser from '../../../pages/Browser/BrowserView'; -import FixtureBuilder from '../../../../tests/framework/fixtures/FixtureBuilder'; -import TabBarComponent from '../../../pages/wallet/TabBarComponent'; -import ConfirmationUITypes from '../../../pages/Browser/Confirmations/ConfirmationUITypes'; -import FooterActions from '../../../pages/Browser/Confirmations/FooterActions'; -import Assertions from '../../../../tests/framework/Assertions'; -import { withFixtures } from '../../../../tests/framework/fixtures/FixtureHelper'; +import { SmokeConfirmations } from '../../../../e2e/tags'; +import { loginToApp, navigateToBrowserView } from '../../../../e2e/viewHelper'; +import Browser from '../../../../e2e/pages/Browser/BrowserView'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import TabBarComponent from '../../../../e2e/pages/wallet/TabBarComponent'; +import ConfirmationUITypes from '../../../../e2e/pages/Browser/Confirmations/ConfirmationUITypes'; +import FooterActions from '../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import Assertions from '../../../framework/Assertions'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; import { AnvilPort, buildPermissions, -} from '../../../../tests/framework/fixtures/FixtureUtils'; -import RowComponents from '../../../pages/Browser/Confirmations/RowComponents'; +} from '../../../framework/fixtures/FixtureUtils'; +import RowComponents from '../../../../e2e/pages/Browser/Confirmations/RowComponents'; import { SEND_ETH_SIMULATION_MOCK, SIMULATION_ENABLED_NETWORKS_MOCK, -} from '../../../../tests/api-mocking/mock-responses/simulations'; -import TestDApp from '../../../pages/Browser/TestDApp'; -import { DappVariants } from '../../../../tests/framework/Constants'; +} from '../../../api-mocking/mock-responses/simulations'; +import TestDApp from '../../../../e2e/pages/Browser/TestDApp'; +import { DappVariants } from '../../../framework/Constants'; import { EventPayload, getEventsPayloads, -} from '../../../../tests/helpers/analytics/helpers'; -import SoftAssert from '../../../../tests/framework/SoftAssert'; +} from '../../../helpers/analytics/helpers'; +import SoftAssert from '../../../framework/SoftAssert'; import { Mockttp } from 'mockttp'; import { setupMockRequest, setupMockPostRequest, -} from '../../../../tests/api-mocking/helpers/mockHelpers'; -import Gestures from '../../../../tests/framework/Gestures'; +} from '../../../api-mocking/helpers/mockHelpers'; +import Gestures from '../../../framework/Gestures'; import { SECURITY_ALERTS_BENIGN_RESPONSE, SECURITY_ALERTS_REQUEST_BODY, securityAlertsUrl, -} from '../../../../tests/api-mocking/mock-responses/security-alerts-mock'; -import { setupRemoteFeatureFlagsMock } from '../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { confirmationFeatureFlags } from '../../../../tests/api-mocking/mock-responses/feature-flags-mocks'; -import { LocalNode } from '../../../../tests/framework/types'; -import { AnvilManager } from '../../../../tests/seeder/anvil-manager'; +} from '../../../api-mocking/mock-responses/security-alerts-mock'; +import { setupRemoteFeatureFlagsMock } from '../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { confirmationFeatureFlags } from '../../../api-mocking/mock-responses/feature-flags-mocks'; +import { LocalNode } from '../../../framework/types'; +import { AnvilManager } from '../../../seeder/anvil-manager'; const expectedEvents = { TRANSACTION_ADDED: 'Transaction Added', diff --git a/e2e/specs/confirmations/transactions/gas-fee-tokens-eip-7702-sponsored.spec.ts b/tests/smoke/confirmations/transactions/gas-fee-tokens-eip-7702-sponsored.spec.ts similarity index 83% rename from e2e/specs/confirmations/transactions/gas-fee-tokens-eip-7702-sponsored.spec.ts rename to tests/smoke/confirmations/transactions/gas-fee-tokens-eip-7702-sponsored.spec.ts index f4c7d80a015..c75aed5e278 100644 --- a/e2e/specs/confirmations/transactions/gas-fee-tokens-eip-7702-sponsored.spec.ts +++ b/tests/smoke/confirmations/transactions/gas-fee-tokens-eip-7702-sponsored.spec.ts @@ -1,32 +1,32 @@ -import FixtureBuilder from '../../../../tests/framework/fixtures/FixtureBuilder'; -import FooterActions from '../../../pages/Browser/Confirmations/FooterActions'; -import SendView from '../../../pages/Send/RedesignedSendView'; -import TabBarComponent from '../../../pages/wallet/TabBarComponent'; -import WalletView from '../../../pages/wallet/WalletView'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import FooterActions from '../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import SendView from '../../../../e2e/pages/Send/RedesignedSendView'; +import TabBarComponent from '../../../../e2e/pages/wallet/TabBarComponent'; +import WalletView from '../../../../e2e/pages/wallet/WalletView'; import { Assertions, LocalNode, LocalNodeType, Utilities, -} from '../../../../tests/framework'; -import { SmokeConfirmations } from '../../../tags'; -import { AnvilPort } from '../../../../tests/framework/fixtures/FixtureUtils'; -import { loginToApp } from '../../../viewHelper'; -import { withFixtures } from '../../../../tests/framework/fixtures/FixtureHelper'; -import RowComponents from '../../../pages/Browser/Confirmations/RowComponents'; -import { AnvilManager, Hardfork } from '../../../../tests/seeder/anvil-manager'; +} from '../../../framework'; +import { SmokeConfirmations } from '../../../../e2e/tags'; +import { AnvilPort } from '../../../framework/fixtures/FixtureUtils'; +import { loginToApp } from '../../../../e2e/viewHelper'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import RowComponents from '../../../../e2e/pages/Browser/Confirmations/RowComponents'; +import { AnvilManager, Hardfork } from '../../../seeder/anvil-manager'; import { setupMockRequest, setupMockPostRequest, -} from '../../../../tests/api-mocking/helpers/mockHelpers'; -import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../tests/api-mocking/mock-responses/simulations'; -import { setupRemoteFeatureFlagsMock } from '../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { remoteFeatureEip7702 } from '../../../../tests/api-mocking/mock-responses/feature-flags-mocks'; +} from '../../../api-mocking/helpers/mockHelpers'; +import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../api-mocking/mock-responses/simulations'; +import { setupRemoteFeatureFlagsMock } from '../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { remoteFeatureEip7702 } from '../../../api-mocking/mock-responses/feature-flags-mocks'; import { Mockttp } from 'mockttp'; import { TRANSACTION_RELAY_STATUS_NETWORKS_MOCK, TRANSACTION_RELAY_SUBMIT_NETWORKS_MOCK, -} from '../../../../tests/api-mocking/mock-responses/transaction-relay-mocks'; +} from '../../../api-mocking/mock-responses/transaction-relay-mocks'; import { RelayStatus } from '../../../../app/util/transactions/transaction-relay'; const TRANSACTION_UUID_MOCK = '1234-5678'; @@ -148,7 +148,7 @@ const localNodeOptions = [ options: { hardfork: 'prague' as Hardfork, loadState: - './e2e/specs/confirmations/transactions/7702/withDelegatorContracts.json', + './tests/smoke/confirmations/transactions/7702/withDelegatorContracts.json', }, }, ]; diff --git a/e2e/specs/confirmations/transactions/gas-fee-tokens-eip-7702.spec.ts b/tests/smoke/confirmations/transactions/gas-fee-tokens-eip-7702.spec.ts similarity index 85% rename from e2e/specs/confirmations/transactions/gas-fee-tokens-eip-7702.spec.ts rename to tests/smoke/confirmations/transactions/gas-fee-tokens-eip-7702.spec.ts index 2492f0cb7da..72057d54661 100644 --- a/e2e/specs/confirmations/transactions/gas-fee-tokens-eip-7702.spec.ts +++ b/tests/smoke/confirmations/transactions/gas-fee-tokens-eip-7702.spec.ts @@ -1,35 +1,35 @@ -import FixtureBuilder from '../../../../tests/framework/fixtures/FixtureBuilder'; -import FooterActions from '../../../pages/Browser/Confirmations/FooterActions'; -import SendView from '../../../pages/Send/RedesignedSendView'; -import TabBarComponent from '../../../pages/wallet/TabBarComponent'; -import WalletView from '../../../pages/wallet/WalletView'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import FooterActions from '../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import SendView from '../../../../e2e/pages/Send/RedesignedSendView'; +import TabBarComponent from '../../../../e2e/pages/wallet/TabBarComponent'; +import WalletView from '../../../../e2e/pages/wallet/WalletView'; import { Assertions, LocalNode, LocalNodeType, Matchers, -} from '../../../../tests/framework'; -import { SmokeConfirmations } from '../../../tags'; -import { loginToApp } from '../../../viewHelper'; -import { withFixtures } from '../../../../tests/framework/fixtures/FixtureHelper'; -import RowComponents from '../../../pages/Browser/Confirmations/RowComponents'; -import { AnvilManager, Hardfork } from '../../../../tests/seeder/anvil-manager'; +} from '../../../framework'; +import { SmokeConfirmations } from '../../../../e2e/tags'; +import { loginToApp } from '../../../../e2e/viewHelper'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import RowComponents from '../../../../e2e/pages/Browser/Confirmations/RowComponents'; +import { AnvilManager, Hardfork } from '../../../seeder/anvil-manager'; import { setupMockPostRequest, setupMockRequest, -} from '../../../../tests/api-mocking/helpers/mockHelpers'; -import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../tests/api-mocking/mock-responses/simulations'; -import { setupRemoteFeatureFlagsMock } from '../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { remoteFeatureEip7702 } from '../../../../tests/api-mocking/mock-responses/feature-flags-mocks'; +} from '../../../api-mocking/helpers/mockHelpers'; +import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../api-mocking/mock-responses/simulations'; +import { setupRemoteFeatureFlagsMock } from '../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { remoteFeatureEip7702 } from '../../../api-mocking/mock-responses/feature-flags-mocks'; import { Mockttp } from 'mockttp'; import { TRANSACTION_RELAY_STATUS_NETWORKS_MOCK, TRANSACTION_RELAY_SUBMIT_NETWORKS_MOCK, -} from '../../../../tests/api-mocking/mock-responses/transaction-relay-mocks'; +} from '../../../api-mocking/mock-responses/transaction-relay-mocks'; import { RelayStatus } from '../../../../app/util/transactions/transaction-relay'; -import TransactionConfirmView from '../../../pages/Send/TransactionConfirmView'; -import GasFeeTokenModal from '../../../pages/Confirmation/GasFeeTokenModal'; -import { AnvilPort } from '../../../../tests/framework/fixtures/FixtureUtils'; +import TransactionConfirmView from '../../../../e2e/pages/Send/TransactionConfirmView'; +import GasFeeTokenModal from '../../../../e2e/pages/Confirmation/GasFeeTokenModal'; +import { AnvilPort } from '../../../framework/fixtures/FixtureUtils'; const TRANSACTION_UUID_MOCK = '1234-5678'; const SENDER_ADDRESS_MOCK = '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3'; @@ -202,7 +202,7 @@ describe( options: { hardfork: 'prague' as Hardfork, loadState: - './e2e/specs/confirmations/transactions/7702/withDelegatorContracts.json', + './tests/smoke/confirmations/transactions/7702/withDelegatorContracts.json', }, }, ]; diff --git a/e2e/specs/confirmations/transactions/per-dapp-selected-network.spec.ts b/tests/smoke/confirmations/transactions/per-dapp-selected-network.spec.ts similarity index 75% rename from e2e/specs/confirmations/transactions/per-dapp-selected-network.spec.ts rename to tests/smoke/confirmations/transactions/per-dapp-selected-network.spec.ts index 0ab1029e957..1e0fa4baa86 100644 --- a/e2e/specs/confirmations/transactions/per-dapp-selected-network.spec.ts +++ b/tests/smoke/confirmations/transactions/per-dapp-selected-network.spec.ts @@ -1,25 +1,25 @@ -import FixtureBuilder from '../../../../tests/framework/fixtures/FixtureBuilder.ts'; -import { withFixtures } from '../../../../tests/framework/fixtures/FixtureHelper.ts'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; import { buildPermissions, AnvilPort, -} from '../../../../tests/framework/fixtures/FixtureUtils.ts'; -import Browser from '../../../pages/Browser/BrowserView.ts'; -import ConfirmationFooterActions from '../../../pages/Browser/Confirmations/FooterActions.ts'; -import ConfirmationUITypes from '../../../pages/Browser/Confirmations/ConfirmationUITypes.ts'; -import TestDApp from '../../../pages/Browser/TestDApp.ts'; -import NetworkListModal from '../../../pages/Network/NetworkListModal.ts'; -import TabBarComponent from '../../../pages/wallet/TabBarComponent.ts'; -import WalletView from '../../../pages/wallet/WalletView.ts'; -import { SmokeConfirmations } from '../../../tags.js'; -import Assertions from '../../../../tests/framework/Assertions.ts'; -import { loginToApp, navigateToBrowserView } from '../../../viewHelper.ts'; -import { DappVariants } from '../../../../tests/framework/Constants.ts'; -import { setupRemoteFeatureFlagsMock } from '../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper.ts'; -import { confirmationFeatureFlags } from '../../../../tests/api-mocking/mock-responses/feature-flags-mocks.ts'; +} from '../../../framework/fixtures/FixtureUtils'; +import Browser from '../../../../e2e/pages/Browser/BrowserView'; +import ConfirmationFooterActions from '../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import ConfirmationUITypes from '../../../../e2e/pages/Browser/Confirmations/ConfirmationUITypes'; +import TestDApp from '../../../../e2e/pages/Browser/TestDApp'; +import NetworkListModal from '../../../../e2e/pages/Network/NetworkListModal'; +import TabBarComponent from '../../../../e2e/pages/wallet/TabBarComponent'; +import WalletView from '../../../../e2e/pages/wallet/WalletView'; +import { SmokeConfirmations } from '../../../../e2e/tags.js'; +import Assertions from '../../../framework/Assertions'; +import { loginToApp, navigateToBrowserView } from '../../../../e2e/viewHelper'; +import { DappVariants } from '../../../framework/Constants'; +import { setupRemoteFeatureFlagsMock } from '../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { confirmationFeatureFlags } from '../../../api-mocking/mock-responses/feature-flags-mocks'; import { Mockttp } from 'mockttp'; -import { LocalNode } from '../../../../tests/framework/types'; -import { AnvilManager } from '../../../../tests/seeder/anvil-manager'; +import { LocalNode } from '../../../framework/types'; +import { AnvilManager } from '../../../seeder/anvil-manager'; const LOCAL_CHAIN_ID = '0x539'; const LOCAL_CHAIN_NAME = 'Localhost'; diff --git a/e2e/specs/confirmations/transactions/token-approve/approve.spec.ts b/tests/smoke/confirmations/transactions/token-approve/approve.spec.ts similarity index 80% rename from e2e/specs/confirmations/transactions/token-approve/approve.spec.ts rename to tests/smoke/confirmations/transactions/token-approve/approve.spec.ts index ea734fde4d0..d8af3bd3756 100644 --- a/e2e/specs/confirmations/transactions/token-approve/approve.spec.ts +++ b/tests/smoke/confirmations/transactions/token-approve/approve.spec.ts @@ -1,28 +1,31 @@ import { SMART_CONTRACTS } from '../../../../../app/util/test/smart-contracts'; -import { SmokeConfirmations } from '../../../../tags'; -import { loginToApp, navigateToBrowserView } from '../../../../viewHelper'; -import FixtureBuilder from '../../../../../tests/framework/fixtures/FixtureBuilder'; -import TabBarComponent from '../../../../pages/wallet/TabBarComponent'; -import Browser from '../../../../pages/Browser/BrowserView'; -import ConfirmationUITypes from '../../../../pages/Browser/Confirmations/ConfirmationUITypes'; -import FooterActions from '../../../../pages/Browser/Confirmations/FooterActions'; -import Assertions from '../../../../../tests/framework/Assertions'; -import { withFixtures } from '../../../../../tests/framework/fixtures/FixtureHelper'; +import { SmokeConfirmations } from '../../../../../e2e/tags'; +import { + loginToApp, + navigateToBrowserView, +} from '../../../../../e2e/viewHelper'; +import FixtureBuilder from '../../../../framework/fixtures/FixtureBuilder'; +import TabBarComponent from '../../../../../e2e/pages/wallet/TabBarComponent'; +import Browser from '../../../../../e2e/pages/Browser/BrowserView'; +import ConfirmationUITypes from '../../../../../e2e/pages/Browser/Confirmations/ConfirmationUITypes'; +import FooterActions from '../../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import Assertions from '../../../../framework/Assertions'; +import { withFixtures } from '../../../../framework/fixtures/FixtureHelper'; import { buildPermissions, AnvilPort, -} from '../../../../../tests/framework/fixtures/FixtureUtils'; -import RowComponents from '../../../../pages/Browser/Confirmations/RowComponents'; -import TokenApproveConfirmation from '../../../../pages/Confirmation/TokenApproveConfirmation'; -import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../../tests/api-mocking/mock-responses/simulations'; -import TestDApp from '../../../../pages/Browser/TestDApp'; -import { DappVariants } from '../../../../../tests/framework/Constants'; -import { setupMockRequest } from '../../../../../tests/api-mocking/helpers/mockHelpers'; +} from '../../../../framework/fixtures/FixtureUtils'; +import RowComponents from '../../../../../e2e/pages/Browser/Confirmations/RowComponents'; +import TokenApproveConfirmation from '../../../../../e2e/pages/Confirmation/TokenApproveConfirmation'; +import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../api-mocking/mock-responses/simulations'; +import TestDApp from '../../../../../e2e/pages/Browser/TestDApp'; +import { DappVariants } from '../../../../framework/Constants'; +import { setupMockRequest } from '../../../../api-mocking/helpers/mockHelpers'; import { Mockttp } from 'mockttp'; -import { setupRemoteFeatureFlagsMock } from '../../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { confirmationFeatureFlags } from '../../../../../tests/api-mocking/mock-responses/feature-flags-mocks'; -import { LocalNode } from '../../../../../tests/framework/types'; -import { AnvilManager } from '../../../../../tests/seeder/anvil-manager'; +import { setupRemoteFeatureFlagsMock } from '../../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { confirmationFeatureFlags } from '../../../../api-mocking/mock-responses/feature-flags-mocks'; +import { LocalNode } from '../../../../framework/types'; +import { AnvilManager } from '../../../../seeder/anvil-manager'; describe(SmokeConfirmations('Token Approve - approve method'), () => { const ERC_20_CONTRACT = SMART_CONTRACTS.HST; diff --git a/e2e/specs/confirmations/transactions/token-approve/increase-allowance.spec.ts b/tests/smoke/confirmations/transactions/token-approve/increase-allowance.spec.ts similarity index 71% rename from e2e/specs/confirmations/transactions/token-approve/increase-allowance.spec.ts rename to tests/smoke/confirmations/transactions/token-approve/increase-allowance.spec.ts index 95a12a6942c..2c6608b1c0b 100644 --- a/e2e/specs/confirmations/transactions/token-approve/increase-allowance.spec.ts +++ b/tests/smoke/confirmations/transactions/token-approve/increase-allowance.spec.ts @@ -1,28 +1,31 @@ import { SMART_CONTRACTS } from '../../../../../app/util/test/smart-contracts'; -import { SmokeConfirmations } from '../../../../tags'; -import { loginToApp, navigateToBrowserView } from '../../../../viewHelper'; -import FixtureBuilder from '../../../../../tests/framework/fixtures/FixtureBuilder'; -import TabBarComponent from '../../../../pages/wallet/TabBarComponent'; -import Browser from '../../../../pages/Browser/BrowserView'; -import ConfirmationUITypes from '../../../../pages/Browser/Confirmations/ConfirmationUITypes'; -import FooterActions from '../../../../pages/Browser/Confirmations/FooterActions'; -import Assertions from '../../../../../tests/framework/Assertions'; -import { withFixtures } from '../../../../../tests/framework/fixtures/FixtureHelper'; +import { SmokeConfirmations } from '../../../../../e2e/tags'; +import { + loginToApp, + navigateToBrowserView, +} from '../../../../../e2e/viewHelper'; +import FixtureBuilder from '../../../../framework/fixtures/FixtureBuilder'; +import TabBarComponent from '../../../../../e2e/pages/wallet/TabBarComponent'; +import Browser from '../../../../../e2e/pages/Browser/BrowserView'; +import ConfirmationUITypes from '../../../../../e2e/pages/Browser/Confirmations/ConfirmationUITypes'; +import FooterActions from '../../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import Assertions from '../../../../framework/Assertions'; +import { withFixtures } from '../../../../framework/fixtures/FixtureHelper'; import { AnvilPort, buildPermissions, -} from '../../../../../tests/framework/fixtures/FixtureUtils'; -import RowComponents from '../../../../pages/Browser/Confirmations/RowComponents'; -import TokenApproveConfirmation from '../../../../pages/Confirmation/TokenApproveConfirmation'; -import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../../tests/api-mocking/mock-responses/simulations'; -import TestDApp from '../../../../pages/Browser/TestDApp'; -import { DappVariants } from '../../../../../tests/framework/Constants'; +} from '../../../../framework/fixtures/FixtureUtils'; +import RowComponents from '../../../../../e2e/pages/Browser/Confirmations/RowComponents'; +import TokenApproveConfirmation from '../../../../../e2e/pages/Confirmation/TokenApproveConfirmation'; +import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../api-mocking/mock-responses/simulations'; +import TestDApp from '../../../../../e2e/pages/Browser/TestDApp'; +import { DappVariants } from '../../../../framework/Constants'; import { Mockttp } from 'mockttp'; -import { setupMockRequest } from '../../../../../tests/api-mocking/helpers/mockHelpers'; -import { confirmationFeatureFlags } from '../../../../../tests/api-mocking/mock-responses/feature-flags-mocks'; -import { setupRemoteFeatureFlagsMock } from '../../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { LocalNode } from '../../../../../tests/framework/types'; -import { AnvilManager } from '../../../../../tests/seeder/anvil-manager'; +import { setupMockRequest } from '../../../../api-mocking/helpers/mockHelpers'; +import { confirmationFeatureFlags } from '../../../../api-mocking/mock-responses/feature-flags-mocks'; +import { setupRemoteFeatureFlagsMock } from '../../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { LocalNode } from '../../../../framework/types'; +import { AnvilManager } from '../../../../seeder/anvil-manager'; describe(SmokeConfirmations('Token Approve - increaseAllowance method'), () => { const ERC_20_CONTRACT = SMART_CONTRACTS.HST; diff --git a/e2e/specs/confirmations/transactions/token-approve/set-approval-for-all.spec.ts b/tests/smoke/confirmations/transactions/token-approve/set-approval-for-all.spec.ts similarity index 85% rename from e2e/specs/confirmations/transactions/token-approve/set-approval-for-all.spec.ts rename to tests/smoke/confirmations/transactions/token-approve/set-approval-for-all.spec.ts index c6ff9a64036..a83f4fa728a 100644 --- a/e2e/specs/confirmations/transactions/token-approve/set-approval-for-all.spec.ts +++ b/tests/smoke/confirmations/transactions/token-approve/set-approval-for-all.spec.ts @@ -1,28 +1,31 @@ import { SMART_CONTRACTS } from '../../../../../app/util/test/smart-contracts'; -import { SmokeConfirmations } from '../../../../tags'; -import { loginToApp, navigateToBrowserView } from '../../../../viewHelper'; -import FixtureBuilder from '../../../../../tests/framework/fixtures/FixtureBuilder'; -import TabBarComponent from '../../../../pages/wallet/TabBarComponent'; -import Browser from '../../../../pages/Browser/BrowserView'; -import ConfirmationUITypes from '../../../../pages/Browser/Confirmations/ConfirmationUITypes'; -import FooterActions from '../../../../pages/Browser/Confirmations/FooterActions'; -import Assertions from '../../../../../tests/framework/Assertions'; -import { withFixtures } from '../../../../../tests/framework/fixtures/FixtureHelper'; +import { SmokeConfirmations } from '../../../../../e2e/tags'; +import { + loginToApp, + navigateToBrowserView, +} from '../../../../../e2e/viewHelper'; +import FixtureBuilder from '../../../../framework/fixtures/FixtureBuilder'; +import TabBarComponent from '../../../../../e2e/pages/wallet/TabBarComponent'; +import Browser from '../../../../../e2e/pages/Browser/BrowserView'; +import ConfirmationUITypes from '../../../../../e2e/pages/Browser/Confirmations/ConfirmationUITypes'; +import FooterActions from '../../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import Assertions from '../../../../framework/Assertions'; +import { withFixtures } from '../../../../framework/fixtures/FixtureHelper'; import { AnvilPort, buildPermissions, -} from '../../../../../tests/framework/fixtures/FixtureUtils'; -import RowComponents from '../../../../pages/Browser/Confirmations/RowComponents'; -import TokenApproveConfirmation from '../../../../pages/Confirmation/TokenApproveConfirmation'; -import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../../tests/api-mocking/mock-responses/simulations'; -import TestDApp from '../../../../pages/Browser/TestDApp'; -import { DappVariants } from '../../../../../tests/framework/Constants'; -import { setupMockRequest } from '../../../../../tests/api-mocking/helpers/mockHelpers'; +} from '../../../../framework/fixtures/FixtureUtils'; +import RowComponents from '../../../../../e2e/pages/Browser/Confirmations/RowComponents'; +import TokenApproveConfirmation from '../../../../../e2e/pages/Confirmation/TokenApproveConfirmation'; +import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../api-mocking/mock-responses/simulations'; +import TestDApp from '../../../../../e2e/pages/Browser/TestDApp'; +import { DappVariants } from '../../../../framework/Constants'; +import { setupMockRequest } from '../../../../api-mocking/helpers/mockHelpers'; import { Mockttp } from 'mockttp'; -import { setupRemoteFeatureFlagsMock } from '../../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { confirmationFeatureFlags } from '../../../../../tests/api-mocking/mock-responses/feature-flags-mocks'; -import { LocalNode } from '../../../../../tests/framework/types'; -import { AnvilManager } from '../../../../../tests/seeder/anvil-manager'; +import { setupRemoteFeatureFlagsMock } from '../../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { confirmationFeatureFlags } from '../../../../api-mocking/mock-responses/feature-flags-mocks'; +import { LocalNode } from '../../../../framework/types'; +import { AnvilManager } from '../../../../seeder/anvil-manager'; describe(SmokeConfirmations('Token Approve - setApprovalForAll method'), () => { const ERC_721_CONTRACT = SMART_CONTRACTS.NFTS; diff --git a/e2e/specs/confirmations/transactions/transaction-pay.spec.ts b/tests/smoke/confirmations/transactions/transaction-pay.spec.ts similarity index 64% rename from e2e/specs/confirmations/transactions/transaction-pay.spec.ts rename to tests/smoke/confirmations/transactions/transaction-pay.spec.ts index 66206dd8010..2ac8e6c1e23 100644 --- a/e2e/specs/confirmations/transactions/transaction-pay.spec.ts +++ b/tests/smoke/confirmations/transactions/transaction-pay.spec.ts @@ -1,28 +1,28 @@ -import { withFixtures } from '../../../../tests/framework/fixtures/FixtureHelper'; -import FixtureBuilder from '../../../../tests/framework/fixtures/FixtureBuilder'; -import { SmokeConfirmations } from '../../../tags'; -import { loginToApp } from '../../../viewHelper'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import { SmokeConfirmations } from '../../../../e2e/tags'; +import { loginToApp } from '../../../../e2e/viewHelper'; import { remoteFeatureEip7702, remoteFeatureFlagPredictEnabled, -} from '../../../../tests/api-mocking/mock-responses/feature-flags-mocks'; +} from '../../../api-mocking/mock-responses/feature-flags-mocks'; import { Mockttp } from 'mockttp'; -import { setupRemoteFeatureFlagsMock } from '../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { POLYMARKET_COMPLETE_MOCKS } from '../../../../tests/api-mocking/mock-responses/polymarket/polymarket-mocks'; -import PredictAddFunds from '../../../../tests/page-objects/Predict/PredictAddFunds'; +import { setupRemoteFeatureFlagsMock } from '../../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { POLYMARKET_COMPLETE_MOCKS } from '../../../api-mocking/mock-responses/polymarket/polymarket-mocks'; +import PredictAddFunds from '../../../page-objects/Predict/PredictAddFunds'; import { mockRelayQuote, mockRelayStatus, -} from '../../../../tests/api-mocking/mock-responses/transaction-pay'; +} from '../../../api-mocking/mock-responses/transaction-pay'; import { CHAIN_IDS } from '@metamask/transaction-controller'; -import TransactionPayConfirmation from '../../../pages/Confirmation/TransactionPayConfirmation'; -import FooterActions from '../../../pages/Browser/Confirmations/FooterActions'; -import { Gestures } from '../../../../tests/framework'; -import TransactionDetailsModal from '../../../pages/Transactions/TransactionDetailsModal'; -import TabBarComponent from '../../../pages/wallet/TabBarComponent'; -import WalletActionsBottomSheet from '../../../pages/wallet/WalletActionsBottomSheet'; -import ActivitiesView from '../../../pages/Transactions/ActivitiesView'; -import PredictMarketList from '../../../../tests/page-objects/Predict/PredictMarketList'; +import TransactionPayConfirmation from '../../../../e2e/pages/Confirmation/TransactionPayConfirmation'; +import FooterActions from '../../../../e2e/pages/Browser/Confirmations/FooterActions'; +import { Gestures } from '../../../framework'; +import TransactionDetailsModal from '../../../../e2e/pages/Transactions/TransactionDetailsModal'; +import TabBarComponent from '../../../../e2e/pages/wallet/TabBarComponent'; +import WalletActionsBottomSheet from '../../../../e2e/pages/wallet/WalletActionsBottomSheet'; +import ActivitiesView from '../../../../e2e/pages/Transactions/ActivitiesView'; +import PredictMarketList from '../../../page-objects/Predict/PredictMarketList'; describe(SmokeConfirmations('Transaction Pay'), () => { it('deposits to predict balance', async () => { diff --git a/tests/tools/e2e-ai-analyzer/README.md b/tests/tools/e2e-ai-analyzer/README.md index e6b4a3f3415..0d4a8f45941 100644 --- a/tests/tools/e2e-ai-analyzer/README.md +++ b/tests/tools/e2e-ai-analyzer/README.md @@ -6,14 +6,26 @@ AI-powered analysis system for E2E tests. Uses an agentic approach to make intel It is designed to be used in different **modes**, being each mode responsible for a different aspect of the analysis. +### Running locally + +You need at least one API key set (see Configuration): `E2E_CLAUDE_API_KEY`, `E2E_OPENAI_API_KEY`, or `E2E_GEMINI_API_KEY`. + ```bash -# Run with default provider (uses priority order from config) -node -r esbuild-register tests/tools/e2e-ai-analyzer --pr 12345 +# Analyze a PR (fetches changed files via gh CLI; requires gh auth and network) +node -r esbuild-register tests/tools/e2e-ai-analyzer --mode select-tags --pr + +# Analyze local changes vs base branch (default: origin/main) +node -r esbuild-register tests/tools/e2e-ai-analyzer --mode select-tags + +# Analyze specific changed files (space-separated; files must have actual changes) +node -r esbuild-register tests/tools/e2e-ai-analyzer --mode select-tags --changed-files "path/to/file1 path/to/file2" # Run with a specific provider node -r esbuild-register tests/tools/e2e-ai-analyzer --pr 12345 --provider ``` +For PR analysis, the same command is used in CI (see `.github/scripts/e2e-smart-selection.mjs`). Locally, results are printed to the console; in CI, they are also written to `e2e-ai-analysis.json` and used as workflow outputs. + ### Modes - `select-tags`: Analyzes PR code changes and selects which E2E smoke test tags to run in CI. Output Example: diff --git a/tests/tools/e2e-ai-analyzer/modes/select-tags/prompt.ts b/tests/tools/e2e-ai-analyzer/modes/select-tags/prompt.ts index d4e5c9158a7..1ec9f1961b3 100644 --- a/tests/tools/e2e-ai-analyzer/modes/select-tags/prompt.ts +++ b/tests/tools/e2e-ai-analyzer/modes/select-tags/prompt.ts @@ -111,7 +111,7 @@ export function buildTaskPrompt( const filesSection = `CHANGED FILES (${ allFiles.length } total):\n${fileList.join('\n')}`; - const closing = `Investigate efficiently (consider using several tool calls in the same iteration), then call finalize_tag_selection when ready. Include performance_tests in your final selection with selected_tags (empty array if no performance tests needed) and reasoning.`; + const closing = `Investigate efficiently (consider using several tool calls in the same iteration), then call finalize_tag_selection when ready. Before finalizing: verify you have included all dependent tags as specified in each tag's description above. Include performance_tests in your final selection with selected_tags (empty array if no performance tests needed) and reasoning.`; const prompt = [ instruction, tagsSection, diff --git a/wdio/screen-objects/BridgeScreen.js b/wdio/screen-objects/BridgeScreen.js index c3fd3ed9533..0c20404d1c2 100644 --- a/wdio/screen-objects/BridgeScreen.js +++ b/wdio/screen-objects/BridgeScreen.js @@ -32,11 +32,6 @@ class BridgeScreen { } get destinationTokenArea(){ return AppwrightSelectors.getElementByID(this._device, PerpsWithdrawViewSelectorsIDs.DEST_TOKEN_AREA); - - } - get seeAllDropDown(){ - return AppwrightSelectors.getElementByText(this._device, "See all"); - } getNetworkButton(networkName) { diff --git a/wdio/screen-objects/PredictDetailsScreen.js b/wdio/screen-objects/PredictDetailsScreen.js index 1d4bf24cf76..6753efe0805 100644 --- a/wdio/screen-objects/PredictDetailsScreen.js +++ b/wdio/screen-objects/PredictDetailsScreen.js @@ -8,6 +8,8 @@ import { } from '../../app/components/UI/Predict/Predict.testIds'; import { expect as appwrightExpect } from 'appwright'; +const TAB_BAR_TAB_ID_PREFIX = 'predict-market-details-tab-bar-tab'; + class PredictDetailsScreen { get device() { @@ -37,25 +39,35 @@ class PredictDetailsScreen { get aboutTab() { if (!this._device) { return Selectors.getXpathElementByText(PredictMarketDetailsSelectorsText.ABOUT_TAB_TEXT); - } else { - return AppwrightSelectors.getElementByText(this._device, PredictMarketDetailsSelectorsText.ABOUT_TAB_TEXT); } + // Tab bar tab ID varies (tab-0, tab-1, tab-2); find by ID prefix + text + const text = PredictMarketDetailsSelectorsText.ABOUT_TAB_TEXT; + const xpath = AppwrightSelectors.isAndroid(this._device) + ? `//*[contains(@resource-id,'${TAB_BAR_TAB_ID_PREFIX}') and (@text='${text}' or @content-desc='${text}')]` + : `//*[contains(@name,'${TAB_BAR_TAB_ID_PREFIX}') and (@label='${text}' or contains(@label,'${text}'))]`; + return AppwrightSelectors.getElementByXpath(this._device, xpath); } get positionsTab() { if (!this._device) { return Selectors.getXpathElementByText(PredictMarketDetailsSelectorsText.POSITIONS_TAB_TEXT); - } else { - return AppwrightSelectors.getElementByText(this._device, PredictMarketDetailsSelectorsText.POSITIONS_TAB_TEXT); } + const text = PredictMarketDetailsSelectorsText.POSITIONS_TAB_TEXT; + const xpath = AppwrightSelectors.isAndroid(this._device) + ? `//*[contains(@resource-id,'${TAB_BAR_TAB_ID_PREFIX}') and (@text='${text}' or @content-desc='${text}')]` + : `//*[contains(@name,'${TAB_BAR_TAB_ID_PREFIX}') and (@label='${text}' or contains(@label,'${text}'))]`; + return AppwrightSelectors.getElementByXpath(this._device, xpath); } get outcomesTab() { if (!this._device) { return Selectors.getXpathElementByText(PredictMarketDetailsSelectorsText.OUTCOMES_TAB_TEXT); - } else { - return AppwrightSelectors.getElementByText(this._device, PredictMarketDetailsSelectorsText.OUTCOMES_TAB_TEXT); } + const text = PredictMarketDetailsSelectorsText.OUTCOMES_TAB_TEXT; + const xpath = AppwrightSelectors.isAndroid(this._device) + ? `//*[contains(@resource-id,'${TAB_BAR_TAB_ID_PREFIX}') and (@text='${text}' or @content-desc='${text}')]` + : `//*[contains(@name,'${TAB_BAR_TAB_ID_PREFIX}') and (@label='${text}' or contains(@label,'${text}'))]`; + return AppwrightSelectors.getElementByXpath(this._device, xpath); } get aboutTabContent() { @@ -111,6 +123,29 @@ class PredictDetailsScreen { } } + /** + * Returns true if the Outcomes tab is visible (market has multiple outcomes). + * Yes/No-only markets do not show the Outcomes tab. + */ + async hasOutcomesTab() { + if (!this._device) { + try { + const outcomesTab = await this.outcomesTab; + await outcomesTab.waitForDisplayed({ timeout: 2000 }); + return true; + } catch { + return false; + } + } + try { + const outcomesTab = await this.outcomesTab; + await appwrightExpect(outcomesTab).toBeVisible({ timeout: 2000 }); + return true; + } catch { + return false; + } + } + async tapOutcomesTab() { if (!this._device) { const outcomesTab = await this.outcomesTab; diff --git a/yarn.lock b/yarn.lock index 009a5036a9c..448dd5e3a03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8326,7 +8326,7 @@ __metadata: languageName: node linkType: hard -"@metamask/gas-fee-controller@npm:^26.0.0, @metamask/gas-fee-controller@npm:^26.0.2": +"@metamask/gas-fee-controller@npm:^26.0.2": version: 26.0.2 resolution: "@metamask/gas-fee-controller@npm:26.0.2" dependencies: @@ -8738,7 +8738,7 @@ __metadata: languageName: node linkType: hard -"@metamask/network-controller@npm:^27.0.0, @metamask/network-controller@npm:^27.2.0": +"@metamask/network-controller@npm:^27.0.0": version: 27.2.0 resolution: "@metamask/network-controller@npm:27.2.0" dependencies: @@ -9071,14 +9071,14 @@ __metadata: languageName: node linkType: hard -"@metamask/ramps-controller@npm:^5.0.0": - version: 5.0.0 - resolution: "@metamask/ramps-controller@npm:5.0.0" +"@metamask/ramps-controller@npm:^6.0.0": + version: 6.0.0 + resolution: "@metamask/ramps-controller@npm:6.0.0" dependencies: "@metamask/base-controller": "npm:^9.0.0" "@metamask/controller-utils": "npm:^11.18.0" "@metamask/messenger": "npm:^0.3.0" - checksum: 10/896a246f7b9b7c7f5029afcacbc56a659b3740520518d6cec98752bf292fe94a4cb3374e2e7b8f9d0ece51b30c33a62ca616422dcc0785257cf6ca5a134d76af + checksum: 10/a54560eb1cab00632ebcfe202dab7173827abaa14350874bfd8f09f66b450e48cae911f323fddd76c7af0b660032f517d02e8b9d94323b90b4cc2d4340f714a7 languageName: node linkType: hard @@ -9308,9 +9308,9 @@ __metadata: languageName: node linkType: hard -"@metamask/smart-transactions-controller@npm:^22.3.0": - version: 22.3.0 - resolution: "@metamask/smart-transactions-controller@npm:22.3.0" +"@metamask/smart-transactions-controller@npm:^22.4.0": + version: 22.4.0 + resolution: "@metamask/smart-transactions-controller@npm:22.4.0" dependencies: "@babel/runtime": "npm:^7.24.1" "@ethereumjs/tx": "npm:^5.2.1" @@ -9344,7 +9344,7 @@ __metadata: optional: true "@metamask/gas-fee-controller": optional: true - checksum: 10/a803add4124e964c7eb39aad5f4aac3e49a4da4eb0a52f1c99509b6edd66fdc72821268c697d511d4635d02ca72fdf3ca1eb7d599a483beb030920c4e9832bf0 + checksum: 10/e1cad73e890c5ce2d6f9422d4ac7bd248ac853d10ecf4d96d5092ff5ae911d18db3a851ca800e492613db44c8b4f8a325543a6fe06c10aab5ae1774c75259455 languageName: node linkType: hard @@ -9650,9 +9650,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:62.9.0": - version: 62.9.0 - resolution: "@metamask/transaction-controller@npm:62.9.0" +"@metamask/transaction-controller@npm:62.10.0": + version: 62.10.0 + resolution: "@metamask/transaction-controller@npm:62.10.0" dependencies: "@ethereumjs/common": "npm:^4.4.0" "@ethereumjs/tx": "npm:^5.4.0" @@ -9661,15 +9661,16 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@ethersproject/wallet": "npm:^5.7.0" - "@metamask/accounts-controller": "npm:^35.0.0" + "@metamask/accounts-controller": "npm:^35.0.2" "@metamask/approval-controller": "npm:^8.0.0" "@metamask/base-controller": "npm:^9.0.0" "@metamask/controller-utils": "npm:^11.18.0" + "@metamask/core-backend": "npm:^5.0.0" "@metamask/eth-query": "npm:^4.0.0" - "@metamask/gas-fee-controller": "npm:^26.0.0" + "@metamask/gas-fee-controller": "npm:^26.0.2" "@metamask/messenger": "npm:^0.3.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/network-controller": "npm:^27.2.0" + "@metamask/network-controller": "npm:^29.0.0" "@metamask/nonce-tracker": "npm:^6.0.0" "@metamask/remote-feature-flag-controller": "npm:^4.0.0" "@metamask/rpc-errors": "npm:^7.0.2" @@ -9684,13 +9685,13 @@ __metadata: peerDependencies: "@babel/runtime": ^7.0.0 "@metamask/eth-block-tracker": ">=9" - checksum: 10/4657f097d3c10f4e4f97c32e84c07384f66c4e437de2b70c874df138bca4a193e1856fc2eb25a0c7005c8171f37616f986d28464d132ae4d39b7f0585b72f2d9 + checksum: 10/440ec5b4e2df7abb486fb2b9ac2dee81817eb57d55fbb1c7667169376509685a3967c00bdc02304f05b0666b2ead709ce5f2e9291473d79c1e1ad9c685ef0e03 languageName: node linkType: hard -"@metamask/transaction-controller@patch:@metamask/transaction-controller@npm%3A62.9.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.9.0-5c8f871530.patch": - version: 62.9.0 - resolution: "@metamask/transaction-controller@patch:@metamask/transaction-controller@npm%3A62.9.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.9.0-5c8f871530.patch::version=62.9.0&hash=d44809" +"@metamask/transaction-controller@patch:@metamask/transaction-controller@npm%3A62.10.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch": + version: 62.10.0 + resolution: "@metamask/transaction-controller@patch:@metamask/transaction-controller@npm%3A62.10.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch::version=62.10.0&hash=dae606" dependencies: "@ethereumjs/common": "npm:^4.4.0" "@ethereumjs/tx": "npm:^5.4.0" @@ -9699,15 +9700,16 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@ethersproject/wallet": "npm:^5.7.0" - "@metamask/accounts-controller": "npm:^35.0.0" + "@metamask/accounts-controller": "npm:^35.0.2" "@metamask/approval-controller": "npm:^8.0.0" "@metamask/base-controller": "npm:^9.0.0" "@metamask/controller-utils": "npm:^11.18.0" + "@metamask/core-backend": "npm:^5.0.0" "@metamask/eth-query": "npm:^4.0.0" - "@metamask/gas-fee-controller": "npm:^26.0.0" + "@metamask/gas-fee-controller": "npm:^26.0.2" "@metamask/messenger": "npm:^0.3.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/network-controller": "npm:^27.2.0" + "@metamask/network-controller": "npm:^29.0.0" "@metamask/nonce-tracker": "npm:^6.0.0" "@metamask/remote-feature-flag-controller": "npm:^4.0.0" "@metamask/rpc-errors": "npm:^7.0.2" @@ -9722,7 +9724,7 @@ __metadata: peerDependencies: "@babel/runtime": ^7.0.0 "@metamask/eth-block-tracker": ">=9" - checksum: 10/1012a382fe3af42fb5560c5230d4e312fc611f5ca31559bc6e35b2197c4c0f2cd328822c36c3ea5c1b46e7eba43fb16bd1b6d1301f5bd258c22e09963591bc78 + checksum: 10/4ea385dd59bf8cff4b8a3602a81c9ff8bfdc2b23afafd407f5db504cbefe4fa4494f0d55590cb39969b0943060352931a4dd7c83df6532fdbb36d3651cf40447 languageName: node linkType: hard @@ -34737,7 +34739,7 @@ __metadata: "@metamask/profile-metrics-controller": "npm:^2.0.0" "@metamask/profile-sync-controller": "npm:^27.0.0" "@metamask/providers": "npm:^18.3.1" - "@metamask/ramps-controller": "npm:^5.0.0" + "@metamask/ramps-controller": "npm:^6.0.0" "@metamask/react-native-acm": "npm:^1.0.1" "@metamask/react-native-actionsheet": "npm:2.4.2" "@metamask/react-native-button": "npm:^3.0.0" @@ -34754,7 +34756,7 @@ __metadata: "@metamask/selected-network-controller": "npm:^25.0.0" "@metamask/signature-controller": "npm:^35.0.0" "@metamask/slip44": "npm:^4.2.0" - "@metamask/smart-transactions-controller": "npm:^22.3.0" + "@metamask/smart-transactions-controller": "npm:^22.4.0" "@metamask/snaps-controllers": "npm:^17.2.1" "@metamask/snaps-execution-environments": "npm:^10.4.1" "@metamask/snaps-rpc-methods": "npm:^14.2.0" @@ -34769,7 +34771,7 @@ __metadata: "@metamask/test-dapp": "npm:9.5.0" "@metamask/test-dapp-multichain": "npm:^0.17.1" "@metamask/test-dapp-solana": "npm:^0.3.0" - "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A62.9.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.9.0-5c8f871530.patch" + "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A62.10.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch" "@metamask/transaction-pay-controller": "npm:^12.0.2" "@metamask/tron-wallet-snap": "npm:^1.19.2" "@metamask/utils": "npm:^11.8.1"