diff --git a/ui/components/app/asset-list/asset-list.js b/ui/components/app/asset-list/asset-list.js index 8e493037da44..31514a8c60cc 100644 --- a/ui/components/app/asset-list/asset-list.js +++ b/ui/components/app/asset-list/asset-list.js @@ -19,6 +19,10 @@ import { getMultichainCurrencyImage, getMultichainIsMainnet, getMultichainSelectedAccountCachedBalance, + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + getMultichainIsBitcoin, + ///: END:ONLY_INCLUDE_IF + getMultichainSelectedAccountCachedBalanceIsZero, } from '../../../selectors/multichain'; import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -98,24 +102,33 @@ const AssetList = ({ onClickAsset, showTokensLinks }) => { getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, ); - const { tokensWithBalances, totalFiatBalance, loading } = - useAccountTotalFiatBalance(selectedAccount, shouldHideZeroBalanceTokens); + const { tokensWithBalances, loading } = useAccountTotalFiatBalance( + selectedAccount, + shouldHideZeroBalanceTokens, + ); tokensWithBalances.forEach((token) => { // token.string is the balance displayed in the TokenList UI token.string = roundToDecimalPlacesRemovingExtraZeroes(token.string, 5); }); - const balanceIsZero = Number(totalFiatBalance) === 0; + + const balanceIsZero = useSelector( + getMultichainSelectedAccountCachedBalanceIsZero, + ); + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const isBuyableChain = useSelector(getIsNativeTokenBuyable); const shouldShowBuy = isBuyableChain && balanceIsZero; ///: END:ONLY_INCLUDE_IF const isEvm = useSelector(getMultichainIsEvm); - // NOTE: Since we can parametrize it now, we keep the original behavior // for EVM assets const shouldShowTokensLinks = showTokensLinks ?? isEvm; + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + const isBtc = useSelector(getMultichainIsBitcoin); + ///: END:ONLY_INCLUDE_IF + let isStakeable = isMainnet && isEvm; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) isStakeable = false; @@ -133,7 +146,13 @@ const AssetList = ({ onClickAsset, showTokensLinks }) => { { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) shouldShowBuy ? ( - + ) : null ///: END:ONLY_INCLUDE_IF } diff --git a/ui/components/app/wallet-overview/btc-overview.test.tsx b/ui/components/app/wallet-overview/btc-overview.test.tsx index 233096f0918c..15893fa4d204 100644 --- a/ui/components/app/wallet-overview/btc-overview.test.tsx +++ b/ui/components/app/wallet-overview/btc-overview.test.tsx @@ -9,6 +9,7 @@ import mockState from '../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../test/jest/rendering'; import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; import { RampsMetaMaskEntry } from '../../../hooks/ramps/useRamps/useRamps'; +import { defaultBuyableChains } from '../../../ducks/ramps/constants'; import BtcOverview from './btc-overview'; const PORTOFOLIO_URL = 'https://portfolio.test'; @@ -40,43 +41,63 @@ const mockNonEvmAccount = { type: BtcAccountType.P2wpkh, }; -function getStore(state?: Record) { - return configureMockStore([thunk])({ - metamask: { - ...mockState.metamask, - internalAccounts: { - accounts: { - [mockNonEvmAccount.id]: mockNonEvmAccount, - }, - selectedAccount: mockNonEvmAccount.id, - }, - // (Multichain) BalancesController - balances: { - [mockNonEvmAccount.id]: { - [MultichainNativeAssets.BITCOIN]: { - amount: mockNonEvmBalance, - unit: 'BTC', - }, - }, - }, - // (Multichain) RatesController - fiatCurrency: 'usd', - rates: { - [Cryptocurrency.Btc]: { - conversionRate: '1.000', - conversionDate: 0, - }, +const mockBtcChain = { + active: true, + chainId: MultichainNetworks.BITCOIN, + chainName: 'Bitcoin', + shortName: 'Bitcoin', + nativeTokenSupported: true, + isEvm: false, +}; +// default chains do not include BTC +const mockBuyableChainsWithoutBtc = defaultBuyableChains.filter( + (chain) => chain.chainId !== MultichainNetworks.BITCOIN, +); +const mockBuyableChainsWithBtc = [...mockBuyableChainsWithoutBtc, mockBtcChain]; + +const mockMetamaskStore = { + ...mockState.metamask, + internalAccounts: { + accounts: { + [mockNonEvmAccount.id]: mockNonEvmAccount, + }, + selectedAccount: mockNonEvmAccount.id, + }, + // (Multichain) BalancesController + balances: { + [mockNonEvmAccount.id]: { + [MultichainNativeAssets.BITCOIN]: { + amount: mockNonEvmBalance, + unit: 'BTC', }, - cryptocurrencies: [Cryptocurrency.Btc], - // Required, during onboarding, the extension will assume we're in an "EVM context", meaning - // most multichain selectors will not use non-EVM logic despite having a non-EVM - // selected account - completedOnboarding: true, - // Used when clicking on some buttons - metaMetricsId: mockMetaMetricsId, - // Override state if provided - ...state, }, + }, + // (Multichain) RatesController + fiatCurrency: 'usd', + rates: { + [Cryptocurrency.Btc]: { + conversionRate: '1.000', + conversionDate: 0, + }, + }, + cryptocurrencies: [Cryptocurrency.Btc], + // Required, during onboarding, the extension will assume we're in an "EVM context", meaning + // most multichain selectors will not use non-EVM logic despite having a non-EVM + // selected account + completedOnboarding: true, + // Used when clicking on some buttons + metaMetricsId: mockMetaMetricsId, + // Override state if provided +}; +const mockRampsStore = { + buyableChains: mockBuyableChainsWithoutBtc, +}; + +function getStore(state?: Record) { + return configureMockStore([thunk])({ + metamask: mockMetamaskStore, + ramps: mockRampsStore, + ...state, }); } @@ -103,8 +124,11 @@ describe('BtcOverview', () => { const { container } = renderWithProvider( , getStore({ - // The balances won't be available - balances: {}, + metamask: { + ...mockMetamaskStore, + // The balances won't be available + balances: {}, + }, }), ); @@ -134,8 +158,44 @@ describe('BtcOverview', () => { expect(buyButton).toBeInTheDocument(); }); - it('opens the Portfolio "Buy & Sell" URI when clicking on "Buy & Sell" button', async () => { + it('"Buy & Sell" button is disabled if BTC is not buyable', () => { const { queryByTestId } = renderWithProvider(, getStore()); + const buyButton = queryByTestId(BTC_OVERVIEW_BUY); + + expect(buyButton).toBeInTheDocument(); + expect(buyButton).toBeDisabled(); + }); + + it('"Buy & Sell" button is enabled if BTC is buyable', () => { + const storeWithBtcBuyable = getStore({ + ramps: { + buyableChains: mockBuyableChainsWithBtc, + }, + }); + + const { queryByTestId } = renderWithProvider( + , + storeWithBtcBuyable, + ); + + const buyButton = queryByTestId(BTC_OVERVIEW_BUY); + + expect(buyButton).toBeInTheDocument(); + expect(buyButton).not.toBeDisabled(); + }); + + it('opens the Portfolio "Buy & Sell" URI when clicking on "Buy & Sell" button', async () => { + const storeWithBtcBuyable = getStore({ + ramps: { + buyableChains: mockBuyableChainsWithBtc, + }, + }); + + const { queryByTestId } = renderWithProvider( + , + storeWithBtcBuyable, + ); + const openTabSpy = jest.spyOn(global.platform, 'openTab'); const buyButton = queryByTestId(BTC_OVERVIEW_BUY); diff --git a/ui/components/app/wallet-overview/btc-overview.tsx b/ui/components/app/wallet-overview/btc-overview.tsx index 3703252f205a..f5728bf9cadd 100644 --- a/ui/components/app/wallet-overview/btc-overview.tsx +++ b/ui/components/app/wallet-overview/btc-overview.tsx @@ -1,10 +1,12 @@ import React from 'react'; - import { useSelector } from 'react-redux'; import { getMultichainProviderConfig, getMultichainSelectedAccountCachedBalance, } from '../../../selectors/multichain'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { getIsBitcoinBuyable } from '../../../ducks/ramps'; +///: END:ONLY_INCLUDE_IF import { CoinOverview } from './coin-overview'; type BtcOverviewProps = { @@ -14,6 +16,9 @@ type BtcOverviewProps = { const BtcOverview = ({ className }: BtcOverviewProps) => { const { chainId } = useSelector(getMultichainProviderConfig); const balance = useSelector(getMultichainSelectedAccountCachedBalance); + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + const isBtcBuyable = useSelector(getIsBitcoinBuyable); + ///: END:ONLY_INCLUDE_IF return ( { isSwapsChain={false} ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain={false} - isBuyableChain - // TODO: Remove this logic once `isNativeTokenBuyable` has been - // merged (see: https://github.com/MetaMask/metamask-extension/pull/24041) - isBuyableChainWithoutSigning + isBuyableChain={isBtcBuyable} ///: END:ONLY_INCLUDE_IF /> ); diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx index b1774897edc4..5d710b83adfb 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -75,9 +75,6 @@ const CoinButtons = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain, isBuyableChain, - // TODO: Remove this logic once `isNativeTokenBuyable` has been - // merged (see: https://github.com/MetaMask/metamask-extension/pull/24041) - isBuyableChainWithoutSigning = false, defaultSwapsToken, ///: END:ONLY_INCLUDE_IF classPrefix = 'coin', @@ -88,7 +85,6 @@ const CoinButtons = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain: boolean; isBuyableChain: boolean; - isBuyableChainWithoutSigning?: boolean; defaultSwapsToken?: SwapsEthToken; ///: END:ONLY_INCLUDE_IF classPrefix?: string; @@ -112,10 +108,6 @@ const CoinButtons = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) { condition: !isBuyableChain, message: '' }, ///: END:ONLY_INCLUDE_IF - { - condition: !(isSigningEnabled || isBuyableChainWithoutSigning), - message: 'methodNotSupported', - }, ], sendButton: [ { condition: !isSigningEnabled, message: 'methodNotSupported' }, @@ -339,10 +331,7 @@ const CoinButtons = ({ Icon={ } - disabled={ - !isBuyableChain || - !(isSigningEnabled || isBuyableChainWithoutSigning) - } + disabled={!isBuyableChain} data-testid={`${classPrefix}-overview-buy`} label={t('buyAndSell')} onClick={handleBuyAndSellOnClick} diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx index 6364b0231e82..53d98f17039d 100644 --- a/ui/components/app/wallet-overview/coin-overview.tsx +++ b/ui/components/app/wallet-overview/coin-overview.tsx @@ -39,7 +39,6 @@ export type CoinOverviewProps = { defaultSwapsToken?: SwapsEthToken; isBridgeChain: boolean; isBuyableChain: boolean; - isBuyableChainWithoutSigning: boolean; ///: END:ONLY_INCLUDE_IF isSwapsChain: boolean; isSigningEnabled: boolean; @@ -55,7 +54,6 @@ export const CoinOverview = ({ defaultSwapsToken, isBridgeChain, isBuyableChain, - isBuyableChainWithoutSigning, ///: END:ONLY_INCLUDE_IF isSwapsChain, isSigningEnabled, @@ -152,7 +150,6 @@ export const CoinOverview = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain, isBuyableChain, - isBuyableChainWithoutSigning, defaultSwapsToken, ///: END:ONLY_INCLUDE_IF classPrefix, diff --git a/ui/components/app/wallet-overview/eth-overview.test.js b/ui/components/app/wallet-overview/eth-overview.test.js index 0d079a32f104..2f913bcdb6fb 100644 --- a/ui/components/app/wallet-overview/eth-overview.test.js +++ b/ui/components/app/wallet-overview/eth-overview.test.js @@ -470,7 +470,6 @@ describe('EthOverview', () => { describe('Disabled buttons when an account cannot sign transactions', () => { const buttonTestCases = [ - { testId: ETH_OVERVIEW_BUY, buttonText: 'Buy & Sell' }, { testId: ETH_OVERVIEW_SEND, buttonText: 'Send' }, { testId: ETH_OVERVIEW_SWAP, buttonText: 'Swap' }, { testId: ETH_OVERVIEW_BRIDGE, buttonText: 'Bridge' }, diff --git a/ui/ducks/ramps/ramps.test.ts b/ui/ducks/ramps/ramps.test.ts index c4ca4089815a..c943a73fcf5c 100644 --- a/ui/ducks/ramps/ramps.test.ts +++ b/ui/ducks/ramps/ramps.test.ts @@ -2,9 +2,12 @@ import { configureStore, Store } from '@reduxjs/toolkit'; import RampAPI from '../../helpers/ramps/rampApi/rampAPI'; import { getCurrentChainId, getUseExternalServices } from '../../selectors'; import { CHAIN_IDS } from '../../../shared/constants/network'; +import { getMultichainIsBitcoin } from '../../selectors/multichain'; +import { MultichainNetworks } from '../../../shared/constants/multichain/networks'; import rampsReducer, { fetchBuyableChains, getBuyableChains, + getIsBitcoinBuyable, getIsNativeTokenBuyable, } from './ramps'; import { defaultBuyableChains } from './constants'; @@ -13,11 +16,17 @@ jest.mock('../../helpers/ramps/rampApi/rampAPI'); const mockedRampAPI = RampAPI as jest.Mocked; jest.mock('../../selectors', () => ({ + ...jest.requireActual('../../selectors'), getCurrentChainId: jest.fn(), getUseExternalServices: jest.fn(), getNames: jest.fn(), })); +jest.mock('../../selectors/multichain', () => ({ + ...jest.requireActual('../../selectors/multichain'), + getMultichainIsBitcoin: jest.fn(), +})); + describe('rampsSlice', () => { let store: Store; @@ -151,6 +160,7 @@ describe('rampsSlice', () => { describe('getIsNativeTokenBuyable', () => { const getCurrentChainIdMock = jest.mocked(getCurrentChainId); + const getMultichainIsBitcoinMock = jest.mocked(getMultichainIsBitcoin); afterEach(() => { jest.restoreAllMocks(); @@ -158,30 +168,92 @@ describe('rampsSlice', () => { it('should return true when current chain is buyable', () => { getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); + getMultichainIsBitcoinMock.mockReturnValue(false); const state = store.getState(); expect(getIsNativeTokenBuyable(state)).toEqual(true); }); it('should return false when current chain is not buyable', () => { getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.GOERLI); + getMultichainIsBitcoinMock.mockReturnValue(false); + const mockBuyableChains = [{ chainId: CHAIN_IDS.MAINNET, active: true }]; + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: mockBuyableChains, + }); const state = store.getState(); - expect(getIsNativeTokenBuyable(state)).toEqual(false); + expect(getIsNativeTokenBuyable(state)).toBe(false); }); - it('should return false when current chain is not a valid hex string', () => { - getCurrentChainIdMock.mockReturnValue('0x'); + it('should return true when Bitcoin is buyable and current chain is Bitcoin', () => { + getCurrentChainIdMock.mockReturnValue(MultichainNetworks.BITCOIN); + getMultichainIsBitcoinMock.mockReturnValue(true); + const mockBuyableChains = [ + { chainId: MultichainNetworks.BITCOIN, active: true }, + ]; + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: mockBuyableChains, + }); const state = store.getState(); - expect(getIsNativeTokenBuyable(state)).toEqual(false); + expect(getIsNativeTokenBuyable(state)).toBe(true); + }); + + it('should return false when Bitcoin is not buyable and current chain is Bitcoin', () => { + getCurrentChainIdMock.mockReturnValue(MultichainNetworks.BITCOIN); + getMultichainIsBitcoinMock.mockReturnValue(true); + const mockBuyableChains = [ + { chainId: MultichainNetworks.BITCOIN, active: false }, + ]; + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: mockBuyableChains, + }); + const state = store.getState(); + expect(getIsNativeTokenBuyable(state)).toBe(false); }); it('should return false when buyable chains is a corrupted array', () => { - const mockState = { + getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); + getMultichainIsBitcoinMock.mockReturnValue(false); + const mockCorruptedState = { + ...store.getState(), ramps: { buyableChains: [null, null, null], }, }; - getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); - expect(getIsNativeTokenBuyable(mockState)).toEqual(false); + expect(getIsNativeTokenBuyable(mockCorruptedState)).toBe(false); + }); + }); + + describe('getIsBitcoinBuyable', () => { + it('should return false when Bitcoin is not in buyableChains', () => { + const state = store.getState(); + expect(getIsBitcoinBuyable(state)).toBe(false); + }); + + it('should return true when Bitcoin is in buyableChains and active', () => { + const mockBuyableChains = [ + { chainId: MultichainNetworks.BITCOIN, active: true }, + ]; + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: mockBuyableChains, + }); + const state = store.getState(); + expect(getIsBitcoinBuyable(state)).toBe(true); + }); + + it('should return false when Bitcoin is in buyableChains but not active', () => { + const mockBuyableChains = [ + { chainId: MultichainNetworks.BITCOIN, active: false }, + ]; + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: mockBuyableChains, + }); + const state = store.getState(); + expect(getIsBitcoinBuyable(state)).toBe(false); }); }); }); diff --git a/ui/ducks/ramps/ramps.ts b/ui/ducks/ramps/ramps.ts index afff609cd4d8..4dd3731ad2ed 100644 --- a/ui/ducks/ramps/ramps.ts +++ b/ui/ducks/ramps/ramps.ts @@ -3,6 +3,8 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { getCurrentChainId, getUseExternalServices } from '../../selectors'; import RampAPI from '../../helpers/ramps/rampApi/rampAPI'; import { hexToDecimal } from '../../../shared/modules/conversion.utils'; +import { getMultichainIsBitcoin } from '../../selectors/multichain'; +import { MultichainNetworks } from '../../../shared/constants/multichain/networks'; import { defaultBuyableChains } from './constants'; import { AggregatorNetwork } from './types'; @@ -59,16 +61,34 @@ const { reducer } = rampsSlice; export const getBuyableChains = (state: any) => state.ramps?.buyableChains ?? defaultBuyableChains; +export const getIsBitcoinBuyable = createSelector( + [getBuyableChains], + (buyableChains) => + buyableChains + .filter(Boolean) + .some( + (network: AggregatorNetwork) => + network.chainId === MultichainNetworks.BITCOIN && network.active, + ), +); + export const getIsNativeTokenBuyable = createSelector( - [getCurrentChainId, getBuyableChains], - (currentChainId, buyableChains) => { + [ + getCurrentChainId, + getBuyableChains, + getIsBitcoinBuyable, + getMultichainIsBitcoin, + ], + (currentChainId, buyableChains, isBtcBuyable, isBtc) => { try { return buyableChains .filter(Boolean) - .some( - (network: AggregatorNetwork) => - String(network.chainId) === hexToDecimal(currentChainId), - ); + .some((network: AggregatorNetwork) => { + if (isBtc) { + return isBtcBuyable; + } + return String(network.chainId) === hexToDecimal(currentChainId); + }); } catch (e) { return false; } diff --git a/ui/ducks/ramps/types.ts b/ui/ducks/ramps/types.ts index 6a1571715dfe..ca6b783ec465 100644 --- a/ui/ducks/ramps/types.ts +++ b/ui/ducks/ramps/types.ts @@ -1,6 +1,8 @@ +import { CaipChainId } from '@metamask/utils'; + export type AggregatorNetwork = { active: boolean; - chainId: number; + chainId: number | CaipChainId; chainName: string; nativeTokenSupported: boolean; shortName: string; diff --git a/ui/helpers/ramps/rampApi/rampAPI.ts b/ui/helpers/ramps/rampApi/rampAPI.ts index a1da6da8ef0c..06355d01be6e 100644 --- a/ui/helpers/ramps/rampApi/rampAPI.ts +++ b/ui/helpers/ramps/rampApi/rampAPI.ts @@ -16,7 +16,6 @@ const RampAPI = { const url = new URL('/regions/networks', rampApiBaseUrl); url.searchParams.set('context', 'extension'); const response = await fetchWithTimeout(url.toString()); - const { networks } = await response.json(); return networks; }, diff --git a/ui/selectors/multichain.test.ts b/ui/selectors/multichain.test.ts index fadbfe08a08a..6ffed9c18c04 100644 --- a/ui/selectors/multichain.test.ts +++ b/ui/selectors/multichain.test.ts @@ -28,6 +28,8 @@ import { getMultichainProviderConfig, getMultichainSelectedAccountCachedBalance, getMultichainShouldShowFiat, + getMultichainIsBitcoin, + getMultichainSelectedAccountCachedBalanceIsZero, } from './multichain'; import { getCurrentCurrency, @@ -368,4 +370,54 @@ describe('Multichain Selectors', () => { }, ); }); + + describe('getMultichainIsBitcoin', () => { + it('returns false if account is EVM', () => { + const state = getEvmState(); + expect(getMultichainIsBitcoin(state)).toBe(false); + }); + + it('returns true if account is BTC', () => { + const state = getNonEvmState(MOCK_ACCOUNT_BIP122_P2WPKH); + expect(getMultichainIsBitcoin(state)).toBe(true); + }); + }); + + describe('getMultichainSelectedAccountCachedBalanceIsZero', () => { + it('returns true if the selected EVM account has a zero balance', () => { + const state = getEvmState(); + state.metamask.accountsByChainId['0x1'][ + MOCK_ACCOUNT_EOA.address + ].balance = '0x00'; + expect(getMultichainSelectedAccountCachedBalanceIsZero(state)).toBe(true); + }); + + it('returns false if the selected EVM account has a non-zero balance', () => { + const state = getEvmState(); + state.metamask.accountsByChainId['0x1'][ + MOCK_ACCOUNT_EOA.address + ].balance = '3'; + expect(getMultichainSelectedAccountCachedBalanceIsZero(state)).toBe( + false, + ); + }); + + it('returns true if the selected non-EVM account has a zero balance', () => { + const state = getNonEvmState(MOCK_ACCOUNT_BIP122_P2WPKH); + state.metamask.balances[MOCK_ACCOUNT_BIP122_P2WPKH.id][ + MultichainNativeAssets.BITCOIN + ].amount = '0.00000000'; + expect(getMultichainSelectedAccountCachedBalanceIsZero(state)).toBe(true); + }); + + it('returns false if the selected non-EVM account has a non-zero balance', () => { + const state = getNonEvmState(MOCK_ACCOUNT_BIP122_P2WPKH); + state.metamask.balances[MOCK_ACCOUNT_BIP122_P2WPKH.id][ + MultichainNativeAssets.BITCOIN + ].amount = '1.00000000'; + expect(getMultichainSelectedAccountCachedBalanceIsZero(state)).toBe( + false, + ); + }); + }); }); diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index fddf22002d9d..0ae736edc3dc 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -4,6 +4,8 @@ import { ProviderConfig } from '@metamask/network-controller'; import type { RatesControllerState } from '@metamask/assets-controllers'; import { CaipChainId, KnownCaipNamespace } from '@metamask/utils'; import { ChainId } from '@metamask/controller-utils'; +import { createSelector } from '@reduxjs/toolkit'; +import { Numeric } from '../../shared/modules/Numeric'; import { MultichainProviderConfig, MULTICHAIN_PROVIDER_CONFIGS, @@ -177,6 +179,16 @@ export function getMultichainIsEvm( ); } +export function getMultichainIsBitcoin( + state: MultichainState, + account?: InternalAccount, +) { + const isEvm = getMultichainIsEvm(state, account); + const { symbol } = getMultichainDefaultToken(state, account); + + return !isEvm && symbol === 'BTC'; +} + /** * Retrieves the provider configuration for a multichain network. * @@ -315,6 +327,15 @@ export function getMultichainSelectedAccountCachedBalance( : getBtcCachedBalance(state); } +export const getMultichainSelectedAccountCachedBalanceIsZero = createSelector( + [getMultichainIsEvm, getMultichainSelectedAccountCachedBalance], + (isEvm, balance) => { + const base = isEvm ? 16 : 10; + const numericBalance = new Numeric(balance, base); + return numericBalance.isZero(); + }, +); + export function getMultichainConversionRate( state: MultichainState, account?: InternalAccount,