From be2b439924773a9dc5995c21ac33b446572a86e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3nio=20Regadas?= Date: Tue, 26 Nov 2024 16:14:59 +0000 Subject: [PATCH] chore: adds Solana support for the account overview (#28411) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** We added support for the Solana account overview. Now when we select a Solana address the user will be able to see its details in the home view. Also since the overview is the same for SOL and BTC, in order to not repeat components, we've renamed as "non-evm" the existing BTC ones, and reused them. ![Screenshot 2024-11-13 at 13 53 42](https://github.com/user-attachments/assets/649c1b1c-2da2-4f12-a18d-3549d8739c0e) ## **Related issues** Fixes: ## **Manual testing steps** As of right now, manually testing is a bit complex, it needs to run the snap manually and the extension, since we 1st need to publish a new release to npm with more up to date work. The snap version we have in npm is outdated and won't support this flow. That said, if you want to go ahead and run locally the steps are the following: 1. Clone the [ Solana Snap monorepo](https://github.com/MetaMask/snap-solana-wallet) and run it locally with `yarn` and then `yarn start` 2. In the extension, at this branch, apply the following changes and run the extension as flask: ``` At builds.yml add the solana feature to the flask build: features: - build-flask - keyring-snaps + - solana At shared/lib/accounts/solana-wallet-snap.ts point the snap ID to the snap localhost: -export const SOLANA_WALLET_SNAP_ID: SnapId = SolanaWalletSnap.snapId as SnapId; +//export const SOLANA_WALLET_SNAP_ID: SnapId = SolanaWalletSnap.snapId as SnapId; +export const SOLANA_WALLET_SNAP_ID: SnapId = "local:http://localhost:8080/"; ``` 3. Manually install the snap via the snap dapp at http://localhost:3000 4. Enable the Solana account via Settings > Experimental > Enable Solana account 5. Create a Solana account from the account-list menu and see the account overview of it ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot Co-authored-by: Charly Chevalier --- app/_locales/en/messages.json | 3 + .../lib/accounts/BalancesController.test.ts | 4 + .../lib/accounts/BalancesController.ts | 79 ++++++++++++++++--- app/scripts/metamask-controller.test.js | 2 +- package.json | 3 +- shared/constants/multichain/assets.ts | 18 +++++ shared/constants/multichain/networks.ts | 6 ++ shared/constants/network.ts | 8 +- .../errors-after-init-opt-in-ui-state.json | 16 ++-- test/jest/mocks.ts | 5 +- ui/components/app/wallet-overview/index.js | 2 +- ...ories.tsx => non-evm-overview.stories.tsx} | 6 +- ...iew.test.tsx => non-evm-overview.test.tsx} | 50 ++++++++---- ...{btc-overview.tsx => non-evm-overview.tsx} | 16 +++- .../account-list-menu/account-list-menu.tsx | 2 + .../account-overview-btc.stories.tsx | 12 --- .../account-overview-non-evm.stories.tsx | 19 +++++ ....tsx => account-overview-non-evm.test.tsx} | 12 +-- ...w-btc.tsx => account-overview-non-evm.tsx} | 10 ++- .../account-overview/account-overview.tsx | 11 ++- .../token-list-item/token-list-item.tsx | 9 ++- ui/hooks/useCurrencyDisplay.js | 11 +-- ui/selectors/multichain.ts | 32 +++++--- 23 files changed, 243 insertions(+), 93 deletions(-) rename ui/components/app/wallet-overview/{btc-overview.stories.tsx => non-evm-overview.stories.tsx} (70%) rename ui/components/app/wallet-overview/{btc-overview.test.tsx => non-evm-overview.test.tsx} (92%) rename ui/components/app/wallet-overview/{btc-overview.tsx => non-evm-overview.tsx} (73%) delete mode 100644 ui/components/multichain/account-overview/account-overview-btc.stories.tsx create mode 100644 ui/components/multichain/account-overview/account-overview-non-evm.stories.tsx rename ui/components/multichain/account-overview/{account-overview-btc.test.tsx => account-overview-non-evm.test.tsx} (88%) rename ui/components/multichain/account-overview/{account-overview-btc.tsx => account-overview-non-evm.tsx} (66%) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 54c0a782a592..e9e9cc807ccd 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -3205,6 +3205,9 @@ "networkNamePolygon": { "message": "Polygon" }, + "networkNameSolana": { + "message": "Solana" + }, "networkNameTestnet": { "message": "Testnet" }, diff --git a/app/scripts/lib/accounts/BalancesController.test.ts b/app/scripts/lib/accounts/BalancesController.test.ts index e8ddd89f021e..982df0289fea 100644 --- a/app/scripts/lib/accounts/BalancesController.test.ts +++ b/app/scripts/lib/accounts/BalancesController.test.ts @@ -6,6 +6,7 @@ import { InternalAccount, } from '@metamask/keyring-api'; import { createMockInternalAccount } from '../../../../test/jest/mocks'; +import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; import { BalancesController, AllowedActions, @@ -25,6 +26,9 @@ const mockBtcAccount = createMockInternalAccount({ name: 'mock-btc-snap', enabled: true, }, + options: { + scope: MultichainNetworks.BITCOIN_TESTNET, + }, }); const mockBalanceResult = { diff --git a/app/scripts/lib/accounts/BalancesController.ts b/app/scripts/lib/accounts/BalancesController.ts index e657fe47e64f..588053d6ea2a 100644 --- a/app/scripts/lib/accounts/BalancesController.ts +++ b/app/scripts/lib/accounts/BalancesController.ts @@ -13,6 +13,7 @@ import { type CaipAssetType, type InternalAccount, isEvmAccountType, + SolAccountType, } from '@metamask/keyring-api'; import type { HandleSnapRequest } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; @@ -23,6 +24,8 @@ import type { AccountsControllerAccountRemovedEvent, AccountsControllerListMultichainAccountsAction, } from '@metamask/accounts-controller'; +import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; +import { MULTICHAIN_NETWORK_TO_ASSET_TYPES } from '../../../../shared/constants/multichain/assets'; import { isBtcMainnetAddress } from '../../../../shared/lib/multichain'; import { BalancesTracker } from './BalancesTracker'; @@ -122,13 +125,17 @@ const balancesControllerMetadata = { }, }; -const BTC_TESTNET_ASSETS = ['bip122:000000000933ea01ad0ee984209779ba/slip44:0']; -const BTC_MAINNET_ASSETS = ['bip122:000000000019d6689c085ae165831e93/slip44:0']; const BTC_AVG_BLOCK_TIME = 10 * 60 * 1000; // 10 minutes in milliseconds +const SOLANA_AVG_BLOCK_TIME = 400; // 400 milliseconds // NOTE: We set an interval of half the average block time to mitigate when our interval // is de-synchronized with the actual block time. -export const BALANCES_UPDATE_TIME = BTC_AVG_BLOCK_TIME / 2; +export const BTC_BALANCES_UPDATE_TIME = BTC_AVG_BLOCK_TIME / 2; + +const BALANCE_CHECK_INTERVALS = { + [BtcAccountType.P2wpkh]: BTC_BALANCES_UPDATE_TIME, + [SolAccountType.DataAccount]: SOLANA_AVG_BLOCK_TIME, +}; /** * The BalancesController is responsible for fetching and caching account @@ -165,7 +172,7 @@ export class BalancesController extends BaseController< // Register all non-EVM accounts into the tracker for (const account of this.#listAccounts()) { if (this.#isNonEvmAccount(account)) { - this.#tracker.track(account.id, BALANCES_UPDATE_TIME); + this.#tracker.track(account.id, this.#getBlockTimeFor(account)); } } @@ -193,6 +200,23 @@ export class BalancesController extends BaseController< this.#tracker.stop(); } + /** + * Gets the block time for a given account. + * + * @param account - The account to get the block time for. + * @returns The block time for the account. + */ + #getBlockTimeFor(account: InternalAccount): number { + if (account.type in BALANCE_CHECK_INTERVALS) { + return BALANCE_CHECK_INTERVALS[ + account.type as keyof typeof BALANCE_CHECK_INTERVALS + ]; + } + throw new Error( + `Unsupported account type for balance tracking: ${account.type}`, + ); + } + /** * Lists the multichain accounts coming from the `AccountsController`. * @@ -207,15 +231,16 @@ export class BalancesController extends BaseController< /** * Lists the accounts that we should get balances for. * - * Currently, we only get balances for P2WPKH accounts, but this will change - * in the future when we start support other non-EVM account types. - * * @returns A list of accounts that we should get balances for. */ #listAccounts(): InternalAccount[] { const accounts = this.#listMultichainAccounts(); - return accounts.filter((account) => account.type === BtcAccountType.P2wpkh); + return accounts.filter( + (account) => + account.type === SolAccountType.DataAccount || + account.type === BtcAccountType.P2wpkh, + ); } /** @@ -249,12 +274,13 @@ export class BalancesController extends BaseController< const partialState: BalancesControllerState = { balances: {} }; if (account.metadata.snap) { + const scope = this.#getScopeFrom(account); + const assetTypes = MULTICHAIN_NETWORK_TO_ASSET_TYPES[scope]; + partialState.balances[account.id] = await this.#getBalances( account.id, account.metadata.snap.id, - isBtcMainnetAddress(account.address) - ? BTC_MAINNET_ASSETS - : BTC_TESTNET_ASSETS, + assetTypes, ); } @@ -312,7 +338,7 @@ export class BalancesController extends BaseController< return; } - this.#tracker.track(account.id, BTC_AVG_BLOCK_TIME); + this.#tracker.track(account.id, this.#getBlockTimeFor(account)); // NOTE: Unfortunately, we cannot update the balance right away here, because // messenger's events are running synchronously and fetching the balance is // asynchronous. @@ -376,4 +402,33 @@ export class BalancesController extends BaseController< })) as Promise, }); } + + /** + * Gets the network scope for a given account. + * + * @param account - The account to get the scope for. + * @returns The network scope for the account. + * @throws If the account type is unknown or unsupported. + */ + #getScopeFrom(account: InternalAccount): MultichainNetworks { + // TODO: Use the new `account.scopes` once available in the `keyring-api`. + + // For Bitcoin accounts, we get the scope based on the address format. + if (account.type === BtcAccountType.P2wpkh) { + if (isBtcMainnetAddress(account.address)) { + return MultichainNetworks.BITCOIN; + } + return MultichainNetworks.BITCOIN_TESTNET; + } + + // For Solana accounts, we know we have a `scope` on the account's `options` bag. + if (account.type === SolAccountType.DataAccount) { + if (!account.options.scope) { + throw new Error('Solana account scope is undefined'); + } + return account.options.scope as MultichainNetworks; + } + + throw new Error(`Unsupported non-EVM account type: ${account.type}`); + } } diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 880df69aa00f..0e08a0ac27c0 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -41,7 +41,7 @@ import { createMockInternalAccount } from '../../test/jest/mocks'; import { mockNetworkState } from '../../test/stub/networks'; import { BalancesController as MultichainBalancesController, - BALANCES_UPDATE_TIME as MULTICHAIN_BALANCES_UPDATE_TIME, + BTC_BALANCES_UPDATE_TIME as MULTICHAIN_BALANCES_UPDATE_TIME, } from './lib/accounts/BalancesController'; import { BalancesTracker as MultichainBalancesTracker } from './lib/accounts/BalancesTracker'; import { deferredPromise } from './lib/util'; diff --git a/package.json b/package.json index 9ed956d639cd..b7adb1fcdcee 100644 --- a/package.json +++ b/package.json @@ -752,7 +752,8 @@ "resolve-url-loader>es6-iterator>d>es5-ext": false, "resolve-url-loader>es6-iterator>d>es5-ext>esniff>es5-ext": false, "level>classic-level": false, - "jest-preview": false + "jest-preview": false, + "@metamask/solana-wallet-snap>@solana/web3.js>bigint-buffer": false } }, "packageManager": "yarn@4.5.1" diff --git a/shared/constants/multichain/assets.ts b/shared/constants/multichain/assets.ts index 23462d57d05a..58e0869ff045 100644 --- a/shared/constants/multichain/assets.ts +++ b/shared/constants/multichain/assets.ts @@ -1,3 +1,4 @@ +import { CaipAssetType } from '@metamask/keyring-api'; import { MultichainNetworks } from './networks'; export const MULTICHAIN_NATIVE_CURRENCY_TO_CAIP19 = { @@ -13,3 +14,20 @@ export enum MultichainNativeAssets { SOLANA_DEVNET = `${MultichainNetworks.SOLANA_DEVNET}/slip44:501`, SOLANA_TESTNET = `${MultichainNetworks.SOLANA_TESTNET}/slip44:501`, } + +/** + * Maps network identifiers to their corresponding native asset types. + * Each network is mapped to an array containing its native asset for consistency. + */ +export const MULTICHAIN_NETWORK_TO_ASSET_TYPES: Record< + MultichainNetworks, + CaipAssetType[] +> = { + [MultichainNetworks.SOLANA]: [MultichainNativeAssets.SOLANA], + [MultichainNetworks.SOLANA_TESTNET]: [MultichainNativeAssets.SOLANA_TESTNET], + [MultichainNetworks.SOLANA_DEVNET]: [MultichainNativeAssets.SOLANA_DEVNET], + [MultichainNetworks.BITCOIN]: [MultichainNativeAssets.BITCOIN], + [MultichainNetworks.BITCOIN_TESTNET]: [ + MultichainNativeAssets.BITCOIN_TESTNET, + ], +}; diff --git a/shared/constants/multichain/networks.ts b/shared/constants/multichain/networks.ts index f5a45138d88a..659228ba1199 100644 --- a/shared/constants/multichain/networks.ts +++ b/shared/constants/multichain/networks.ts @@ -1,4 +1,5 @@ import { CaipChainId } from '@metamask/utils'; +import { BtcAccountType, SolAccountType } from '@metamask/keyring-api'; import { isBtcMainnetAddress, isBtcTestnetAddress, @@ -33,6 +34,11 @@ export enum MultichainNetworks { SOLANA_TESTNET = 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z', } +export const MULTICHAIN_ACCOUNT_TYPE_TO_MAINNET = { + [BtcAccountType.P2wpkh]: MultichainNetworks.BITCOIN, + [SolAccountType.DataAccount]: MultichainNetworks.SOLANA, +} as const; + export const BITCOIN_TOKEN_IMAGE_URL = './images/bitcoin-logo.svg'; export const SOLANA_TOKEN_IMAGE_URL = './images/solana-logo.svg'; diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 3fca971c338e..41f1d9fd0d95 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -301,7 +301,6 @@ export const CURRENCY_SYMBOLS = { AVALANCHE: 'AVAX', BNB: 'BNB', BUSD: 'BUSD', - BTC: 'BTC', // Do we wanna mix EVM and non-EVM here? CELO: 'CELO', DAI: 'DAI', GNOSIS: 'XDAI', @@ -322,8 +321,15 @@ export const CURRENCY_SYMBOLS = { ONE: 'ONE', } as const; +// Non-EVM currency symbols +export const NON_EVM_CURRENCY_SYMBOLS = { + BTC: 'BTC', + SOL: 'SOL', +} as const; + const CHAINLIST_CURRENCY_SYMBOLS_MAP = { ...CURRENCY_SYMBOLS, + ...NON_EVM_CURRENCY_SYMBOLS, BASE: 'ETH', LINEA_MAINNET: 'ETH', OPBNB: 'BNB', diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 8b2efef3e517..01f94b55d5c7 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -35,13 +35,11 @@ "petnamesEnabled": true, "showMultiRpcModal": "boolean", "isRedesignedConfirmationsDeveloperEnabled": "boolean", - "redesignedConfirmationsEnabled": true, - "redesignedTransactionsEnabled": "boolean", "tokenSortConfig": "object", - "tokenNetworkFilter": { - "0x539": "boolean" - }, - "shouldShowAggregatedBalancePopover": "boolean" + "shouldShowAggregatedBalancePopover": "boolean", + "tokenNetworkFilter": { "0x539": "boolean" }, + "redesignedConfirmationsEnabled": true, + "redesignedTransactionsEnabled": "boolean" }, "firstTimeFlowType": "import", "completedOnboarding": true, @@ -176,10 +174,7 @@ "gasEstimateType": "none", "nonRPCGasFeeApisDisabled": "boolean", "tokenList": "object", - "tokensChainsCache": { - "0x539": "object" - }, - "tokenBalances": "object", + "tokensChainsCache": { "0x539": "object" }, "preventPollingOnNetworkRestart": false, "tokens": "object", "ignoredTokens": "object", @@ -187,6 +182,7 @@ "allTokens": {}, "allIgnoredTokens": {}, "allDetectedTokens": {}, + "tokenBalances": "object", "smartTransactionsState": { "fees": {}, "feesByChainId": "object", diff --git a/test/jest/mocks.ts b/test/jest/mocks.ts index b0750b022e78..8822b96315b6 100644 --- a/test/jest/mocks.ts +++ b/test/jest/mocks.ts @@ -9,6 +9,7 @@ import { import { KeyringTypes } from '@metamask/keyring-controller'; import { v4 as uuidv4 } from 'uuid'; import { keyringTypeToName } from '@metamask/accounts-controller'; +import { Json } from '@metamask/utils'; import { DraftTransaction, draftTransactionInitialState, @@ -186,6 +187,7 @@ export function createMockInternalAccount({ keyringType = KeyringTypes.hd, lastSelected = 0, snapOptions = undefined, + options = undefined, }: { name?: string; address?: string; @@ -197,6 +199,7 @@ export function createMockInternalAccount({ name: string; id: string; }; + options?: Record; } = {}) { let methods; @@ -236,7 +239,7 @@ export function createMockInternalAccount({ snap: snapOptions, lastSelected, }, - options: {}, + options: options ?? {}, methods, type, }; diff --git a/ui/components/app/wallet-overview/index.js b/ui/components/app/wallet-overview/index.js index 54536007bc41..82003b364199 100644 --- a/ui/components/app/wallet-overview/index.js +++ b/ui/components/app/wallet-overview/index.js @@ -1,2 +1,2 @@ export { default as EthOverview } from './eth-overview'; -export { default as BtcOverview } from './btc-overview'; +export { default as NonEvmOverview } from './non-evm-overview'; diff --git a/ui/components/app/wallet-overview/btc-overview.stories.tsx b/ui/components/app/wallet-overview/non-evm-overview.stories.tsx similarity index 70% rename from ui/components/app/wallet-overview/btc-overview.stories.tsx rename to ui/components/app/wallet-overview/non-evm-overview.stories.tsx index 43dff2554bef..2e8ae16045ce 100644 --- a/ui/components/app/wallet-overview/btc-overview.stories.tsx +++ b/ui/components/app/wallet-overview/non-evm-overview.stories.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import BtcOverview from './btc-overview'; +import NonEvmOverview from './non-evm-overview'; export default { title: 'Components/App/WalletOverview/BtcOverview', - component: BtcOverview, + component: NonEvmOverview, parameters: { docs: { description: { @@ -14,6 +14,6 @@ export default { }, }; -const Template = (args) => ; +const Template = (args) => ; export const Default = Template.bind({}); diff --git a/ui/components/app/wallet-overview/btc-overview.test.tsx b/ui/components/app/wallet-overview/non-evm-overview.test.tsx similarity index 92% rename from ui/components/app/wallet-overview/btc-overview.test.tsx rename to ui/components/app/wallet-overview/non-evm-overview.test.tsx index 3c5697cb5853..aa49eb77e79d 100644 --- a/ui/components/app/wallet-overview/btc-overview.test.tsx +++ b/ui/components/app/wallet-overview/non-evm-overview.test.tsx @@ -17,7 +17,7 @@ import { MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import useMultiPolling from '../../../hooks/useMultiPolling'; -import BtcOverview from './btc-overview'; +import NonEvmOverview from './non-evm-overview'; // We need to mock `dispatch` since we use it for `setDefaultHomeActiveTabName`. const mockDispatch = jest.fn().mockReturnValue(() => jest.fn()); @@ -134,7 +134,7 @@ function makePortfolioUrl(path: string, getParams: Record) { return `${PORTOFOLIO_URL}/${path}?${params.toString()}`; } -describe('BtcOverview', () => { +describe('NonEvmOverview', () => { beforeEach(() => { setBackgroundConnection({ setBridgeFeatureFlags: jest.fn() } as never); // Clear previous mock implementations @@ -156,8 +156,11 @@ describe('BtcOverview', () => { }); }); - it('shows the primary balance as BTC when showNativeTokenAsMainBalance if true', async () => { - const { queryByTestId } = renderWithProvider(, getStore()); + it('shows the primary balance using the native token when showNativeTokenAsMainBalance if true', async () => { + const { queryByTestId } = renderWithProvider( + , + getStore(), + ); const primaryBalance = queryByTestId(BTC_OVERVIEW_PRIMARY_CURRENCY); expect(primaryBalance).toBeInTheDocument(); @@ -166,7 +169,7 @@ describe('BtcOverview', () => { it('shows the primary balance as fiat when showNativeTokenAsMainBalance if false', async () => { const { queryByTestId } = renderWithProvider( - , + , getStore({ metamask: { ...mockMetamaskStore, @@ -186,7 +189,7 @@ describe('BtcOverview', () => { it('shows a spinner if balance is not available', async () => { const { container } = renderWithProvider( - , + , getStore({ metamask: { ...mockMetamaskStore, @@ -203,7 +206,10 @@ describe('BtcOverview', () => { }); it('buttons Swap/Bridge are disabled', () => { - const { queryByTestId } = renderWithProvider(, getStore()); + const { queryByTestId } = renderWithProvider( + , + getStore(), + ); for (const buttonTestId of [BTC_OVERVIEW_SWAP, BTC_OVERVIEW_BRIDGE]) { const button = queryByTestId(buttonTestId); @@ -213,13 +219,19 @@ describe('BtcOverview', () => { }); it('shows the "Buy & Sell" button', () => { - const { queryByTestId } = renderWithProvider(, getStore()); + const { queryByTestId } = renderWithProvider( + , + getStore(), + ); const buyButton = queryByTestId(BTC_OVERVIEW_BUY); expect(buyButton).toBeInTheDocument(); }); it('"Buy & Sell" button is disabled if BTC is not buyable', () => { - const { queryByTestId } = renderWithProvider(, getStore()); + const { queryByTestId } = renderWithProvider( + , + getStore(), + ); const buyButton = queryByTestId(BTC_OVERVIEW_BUY); expect(buyButton).toBeInTheDocument(); @@ -234,7 +246,7 @@ describe('BtcOverview', () => { }); const { queryByTestId } = renderWithProvider( - , + , storeWithBtcBuyable, ); @@ -252,7 +264,7 @@ describe('BtcOverview', () => { }); const { queryByTestId } = renderWithProvider( - , + , storeWithBtcBuyable, ); @@ -283,7 +295,7 @@ describe('BtcOverview', () => { const mockTrackEvent = jest.fn(); const { queryByTestId } = renderWithProvider( - + , storeWithBtcBuyable, ); @@ -307,7 +319,10 @@ describe('BtcOverview', () => { }); it('always show the Receive button', () => { - const { queryByTestId } = renderWithProvider(, getStore()); + const { queryByTestId } = renderWithProvider( + , + getStore(), + ); const receiveButton = queryByTestId(BTC_OVERVIEW_RECEIVE); expect(receiveButton).toBeInTheDocument(); }); @@ -332,7 +347,7 @@ describe('BtcOverview', () => { }); const { queryByTestId } = renderWithProvider( - , + , storeWithBtcBuyable, ); @@ -343,7 +358,10 @@ describe('BtcOverview', () => { }); it('always show the Send button', () => { - const { queryByTestId } = renderWithProvider(, getStore()); + const { queryByTestId } = renderWithProvider( + , + getStore(), + ); const sendButton = queryByTestId(BTC_OVERVIEW_SEND); expect(sendButton).toBeInTheDocument(); expect(sendButton).not.toBeDisabled(); @@ -353,7 +371,7 @@ describe('BtcOverview', () => { const mockTrackEvent = jest.fn(); const { queryByTestId } = renderWithProvider( - + , getStore(), ); diff --git a/ui/components/app/wallet-overview/btc-overview.tsx b/ui/components/app/wallet-overview/non-evm-overview.tsx similarity index 73% rename from ui/components/app/wallet-overview/btc-overview.tsx rename to ui/components/app/wallet-overview/non-evm-overview.tsx index fb315d3ab3b0..8905a3a938f1 100644 --- a/ui/components/app/wallet-overview/btc-overview.tsx +++ b/ui/components/app/wallet-overview/non-evm-overview.tsx @@ -1,5 +1,8 @@ import React from 'react'; import { useSelector } from 'react-redux'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { BtcAccountType } from '@metamask/keyring-api'; +///: END:ONLY_INCLUDE_IF import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getMultichainIsMainnet, @@ -14,11 +17,11 @@ import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; import { getSelectedInternalAccount } from '../../../selectors'; import { CoinOverview } from './coin-overview'; -type BtcOverviewProps = { +type NonEvmOverviewProps = { className?: string; }; -const BtcOverview = ({ className }: BtcOverviewProps) => { +const NonEvmOverview = ({ className }: NonEvmOverviewProps) => { const { chainId } = useSelector(getMultichainProviderConfig); const balance = useSelector(getMultichainSelectedAccountCachedBalance); const account = useSelector(getSelectedInternalAccount); @@ -28,6 +31,11 @@ const BtcOverview = ({ className }: BtcOverviewProps) => { account, ); const isBtcBuyable = useSelector(getIsBitcoinBuyable); + + // TODO: Update this to add support to check if Solana is buyable when the Send flow starts + const accountType = account.type; + const isBtc = accountType === BtcAccountType.P2wpkh; + const isBuyableChain = isBtc ? isBtcBuyable && isBtcMainnetAccount : false; ///: END:ONLY_INCLUDE_IF return ( @@ -42,10 +50,10 @@ const BtcOverview = ({ className }: BtcOverviewProps) => { isSwapsChain={false} ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain={false} - isBuyableChain={isBtcBuyable && isBtcMainnetAccount} + isBuyableChain={isBuyableChain} ///: END:ONLY_INCLUDE_IF /> ); }; -export default BtcOverview; +export default NonEvmOverview; diff --git a/ui/components/multichain/account-list-menu/account-list-menu.tsx b/ui/components/multichain/account-list-menu/account-list-menu.tsx index 29d79e8537b1..030b57ebe242 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.tsx +++ b/ui/components/multichain/account-list-menu/account-list-menu.tsx @@ -17,6 +17,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { BtcAccountType, EthAccountType, + SolAccountType, ///: BEGIN:ONLY_INCLUDE_IF(build-flask) InternalAccount, KeyringAccountType, @@ -232,6 +233,7 @@ export const AccountListMenu = ({ EthAccountType.Eoa, EthAccountType.Erc4337, BtcAccountType.P2wpkh, + SolAccountType.DataAccount, ], }: AccountListMenuProps) => { const t = useI18nContext(); diff --git a/ui/components/multichain/account-overview/account-overview-btc.stories.tsx b/ui/components/multichain/account-overview/account-overview-btc.stories.tsx deleted file mode 100644 index 2afc54e22b23..000000000000 --- a/ui/components/multichain/account-overview/account-overview-btc.stories.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { AccountOverviewBtc } from './account-overview-btc' -import { AccountOverviewCommonProps } from './common'; - -export default { - title: 'Components/Multichain/AccountOverviewBtc', - component: AccountOverviewBtc, -}; - -export const DefaultStory = ( - args: JSX.IntrinsicAttributes & AccountOverviewCommonProps -) => ; diff --git a/ui/components/multichain/account-overview/account-overview-non-evm.stories.tsx b/ui/components/multichain/account-overview/account-overview-non-evm.stories.tsx new file mode 100644 index 000000000000..de3ac5484baf --- /dev/null +++ b/ui/components/multichain/account-overview/account-overview-non-evm.stories.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { AccountOverviewNonEvm } from './account-overview-non-evm'; +import { AccountOverviewCommonProps } from './common'; +import { BtcAccountType, SolAccountType } from '@metamask/keyring-api'; + +export default { + title: 'Components/Multichain/AccountOverviewNonEvm', + component: AccountOverviewNonEvm, + args: { + accountType: BtcAccountType.P2wpkh, + }, +}; + +export const DefaultStory = ( + args: JSX.IntrinsicAttributes & + AccountOverviewCommonProps & { + accountType: BtcAccountType.P2wpkh | SolAccountType.DataAccount; + }, +) => ; diff --git a/ui/components/multichain/account-overview/account-overview-btc.test.tsx b/ui/components/multichain/account-overview/account-overview-non-evm.test.tsx similarity index 88% rename from ui/components/multichain/account-overview/account-overview-btc.test.tsx rename to ui/components/multichain/account-overview/account-overview-non-evm.test.tsx index b171840a540e..17989cbf31a6 100644 --- a/ui/components/multichain/account-overview/account-overview-btc.test.tsx +++ b/ui/components/multichain/account-overview/account-overview-non-evm.test.tsx @@ -5,9 +5,9 @@ import { renderWithProvider } from '../../../../test/jest/rendering'; import { setBackgroundConnection } from '../../../store/background-connection'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { - AccountOverviewBtc, - AccountOverviewBtcProps, -} from './account-overview-btc'; + AccountOverviewNonEvm, + AccountOverviewNonEvmProps, +} from './account-overview-non-evm'; jest.mock('../../../store/actions', () => ({ tokenBalancesStartPolling: jest.fn().mockResolvedValue('pollingToken'), @@ -26,14 +26,14 @@ jest.mock('react-redux', () => { }; }); -const defaultProps: AccountOverviewBtcProps = { +const defaultProps: AccountOverviewNonEvmProps = { defaultHomeActiveTabName: null, onTabClick: jest.fn(), setBasicFunctionalityModalOpen: jest.fn(), onSupportLinkClick: jest.fn(), }; -const render = (props: AccountOverviewBtcProps = defaultProps) => { +const render = (props: AccountOverviewNonEvmProps = defaultProps) => { const store = configureStore({ metamask: { ...mockState.metamask, @@ -47,7 +47,7 @@ const render = (props: AccountOverviewBtcProps = defaultProps) => { }, }); - return renderWithProvider(, store); + return renderWithProvider(, store); }; describe('AccountOverviewBtc', () => { diff --git a/ui/components/multichain/account-overview/account-overview-btc.tsx b/ui/components/multichain/account-overview/account-overview-non-evm.tsx similarity index 66% rename from ui/components/multichain/account-overview/account-overview-btc.tsx rename to ui/components/multichain/account-overview/account-overview-non-evm.tsx index dd58b2eef414..dd7db4484306 100644 --- a/ui/components/multichain/account-overview/account-overview-btc.tsx +++ b/ui/components/multichain/account-overview/account-overview-non-evm.tsx @@ -1,11 +1,13 @@ import React from 'react'; -import { BtcOverview } from '../../app/wallet-overview'; +import { NonEvmOverview } from '../../app/wallet-overview'; import { AccountOverviewLayout } from './account-overview-layout'; import { AccountOverviewCommonProps } from './common'; -export type AccountOverviewBtcProps = AccountOverviewCommonProps; +export type AccountOverviewNonEvmProps = AccountOverviewCommonProps; -export const AccountOverviewBtc = (props: AccountOverviewBtcProps) => { +export const AccountOverviewNonEvm = ({ + ...props +}: AccountOverviewNonEvmProps) => { return ( { > { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask,build-mmi) - + ///: END:ONLY_INCLUDE_IF } diff --git a/ui/components/multichain/account-overview/account-overview.tsx b/ui/components/multichain/account-overview/account-overview.tsx index 3d6121e41471..f3f3e427a688 100644 --- a/ui/components/multichain/account-overview/account-overview.tsx +++ b/ui/components/multichain/account-overview/account-overview.tsx @@ -1,13 +1,17 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import { BtcAccountType, EthAccountType } from '@metamask/keyring-api'; +import { + BtcAccountType, + EthAccountType, + SolAccountType, +} from '@metamask/keyring-api'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { BannerAlert, BannerAlertSeverity } from '../../component-library'; import { getSelectedInternalAccount } from '../../../selectors'; import { AccountOverviewEth } from './account-overview-eth'; -import { AccountOverviewBtc } from './account-overview-btc'; import { AccountOverviewUnknown } from './account-overview-unknown'; import { AccountOverviewCommonProps } from './common'; +import { AccountOverviewNonEvm } from './account-overview-non-evm'; export type AccountOverviewProps = AccountOverviewCommonProps & { useExternalServices: boolean; @@ -25,7 +29,8 @@ export function AccountOverview(props: AccountOverviewProps) { case EthAccountType.Erc4337: return ; case BtcAccountType.P2wpkh: - return ; + case SolAccountType.DataAccount: + return ; default: return ; } diff --git a/ui/components/multichain/token-list-item/token-list-item.tsx b/ui/components/multichain/token-list-item/token-list-item.tsx index ef49ec3126cb..5ee4c19c8c52 100644 --- a/ui/components/multichain/token-list-item/token-list-item.tsx +++ b/ui/components/multichain/token-list-item/token-list-item.tsx @@ -56,7 +56,10 @@ import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { CURRENCY_SYMBOLS } from '../../../../shared/constants/network'; +import { + CURRENCY_SYMBOLS, + NON_EVM_CURRENCY_SYMBOLS, +} from '../../../../shared/constants/network'; import { hexToDecimal } from '../../../../shared/modules/conversion.utils'; import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; @@ -141,8 +144,10 @@ export const TokenListItem = ({ switch (title) { case CURRENCY_SYMBOLS.ETH: return t('networkNameEthereum'); - case CURRENCY_SYMBOLS.BTC: + case NON_EVM_CURRENCY_SYMBOLS.BTC: return t('networkNameBitcoin'); + case NON_EVM_CURRENCY_SYMBOLS.SOL: + return t('networkNameSolana'); default: return title; } diff --git a/ui/hooks/useCurrencyDisplay.js b/ui/hooks/useCurrencyDisplay.js index 12b2cfc06ec3..03561153ed53 100644 --- a/ui/hooks/useCurrencyDisplay.js +++ b/ui/hooks/useCurrencyDisplay.js @@ -62,7 +62,8 @@ function formatEthCurrencyDisplay({ return null; } -function formatBtcCurrencyDisplay({ +function formatNonEvmAssetCurrencyDisplay({ + tokenSymbol, isNativeCurrency, isUserPreferredCurrency, currency, @@ -77,7 +78,7 @@ function formatBtcCurrencyDisplay({ // We use `Numeric` here, so we handle those amount the same way than for EVMs (it's worth // noting that if `inputValue` is not properly defined, the amount will be set to '0', see // `Numeric` constructor for that) - return new Numeric(inputValue, 10).toString(); // BTC usually uses 10 digits + return new Numeric(inputValue, 10).toString(); } else if (isUserPreferredCurrency && conversionRate) { const amount = getTokenFiatAmount( @@ -85,7 +86,7 @@ function formatBtcCurrencyDisplay({ Number(conversionRate), // native to fiat conversion rate currentCurrency, inputValue, - 'BTC', + tokenSymbol, false, false, ) ?? '0'; // if the conversion fails, return 0 @@ -162,8 +163,8 @@ export function useCurrencyDisplay( } if (!isEvm) { - // TODO: We would need to update this for other non-EVM coins - return formatBtcCurrencyDisplay({ + return formatNonEvmAssetCurrencyDisplay({ + tokenSymbol: nativeCurrency, isNativeCurrency, isUserPreferredCurrency, currency, diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 1914dbce2dd8..903b5d0a4a71 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -9,6 +9,7 @@ import { MultichainProviderConfig, MULTICHAIN_PROVIDER_CONFIGS, MultichainNetworks, + MULTICHAIN_ACCOUNT_TYPE_TO_MAINNET, } from '../../shared/constants/multichain/networks'; import { getCompletedOnboarding, @@ -18,7 +19,7 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { BalancesControllerState } from '../../app/scripts/lib/accounts/BalancesController'; -import { MultichainNativeAssets } from '../../shared/constants/multichain/assets'; +import { MULTICHAIN_NETWORK_TO_ASSET_TYPES } from '../../shared/constants/multichain/assets'; import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP, TEST_NETWORK_IDS, @@ -333,11 +334,15 @@ export function getMultichainIsMainnet( ) { const selectedAccount = account ?? getSelectedInternalAccount(state); const providerConfig = getMultichainProviderConfig(state, selectedAccount); - return getMultichainIsEvm(state, account) - ? getIsMainnet(state) - : // TODO: For now we only check for bitcoin, but we will need to - // update this for other non-EVM networks later! - providerConfig.chainId === MultichainNetworks.BITCOIN; + + if (getMultichainIsEvm(state, account)) { + return getIsMainnet(state); + } + + const mainnet = ( + MULTICHAIN_ACCOUNT_TYPE_TO_MAINNET as Record + )[selectedAccount.type]; + return providerConfig.chainId === mainnet ?? false; } export function getMultichainIsTestnet( @@ -370,12 +375,17 @@ export const getMultichainCoinRates = (state: MultichainState) => { return state.metamask.rates; }; -function getBtcCachedBalance(state: MultichainState) { +function getNonEvmCachedBalance(state: MultichainState) { const balances = getMultichainBalances(state); const account = getSelectedInternalAccount(state); - const asset = getMultichainIsMainnet(state) - ? MultichainNativeAssets.BITCOIN - : MultichainNativeAssets.BITCOIN_TESTNET; + const network = getMultichainCurrentNetwork(state); + + // We assume that there's at least one asset type in and that is the native + // token for that network. + const asset = + MULTICHAIN_NETWORK_TO_ASSET_TYPES[ + network.chainId as MultichainNetworks + ]?.[0]; return balances?.[account.id]?.[asset]?.amount; } @@ -394,7 +404,7 @@ export function getMultichainSelectedAccountCachedBalance( ) { return getMultichainIsEvm(state) ? getSelectedAccountCachedBalance(state) - : getBtcCachedBalance(state); + : getNonEvmCachedBalance(state); } export const getMultichainSelectedAccountCachedBalanceIsZero = createSelector(