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,