diff --git a/builds.yml b/builds.yml index 35fa7341aa44..35a74cc10a23 100644 --- a/builds.yml +++ b/builds.yml @@ -264,6 +264,8 @@ env: # Enables the notifications feature within the build: - NOTIFICATIONS: '' + - METAMASK_RAMP_API_CONTENT_BASE_URL: https://on-ramp-content.api.cx.metamask.io + ### # Meta variables ### diff --git a/privacy-snapshot.json b/privacy-snapshot.json index b45cf79a6e8f..fe6579bfab73 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -30,6 +30,8 @@ "phishing-detection.api.cx.metamask.io", "portfolio.metamask.io", "price.api.cx.metamask.io", + "on-ramp-content.api.cx.metamask.io", + "on-ramp-content.uat-api.cx.metamask.io", "proxy.api.cx.metamask.io", "raw.githubusercontent.com", "registry.npmjs.org", diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 452f0584ffaa..7754b2a0f16f 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -66,22 +66,6 @@ export type RPCDefinition = { rpcPrefs: RPCPreferences; }; -/** - * For each chain that we support fiat onramps for, we provide a set of - * configuration options that help for initializing the connectiong to the - * onramp providers. - */ -type BuyableChainSettings = { - /** - * The native currency for the given chain - */ - nativeCurrency: CurrencySymbol | TestNetworkCurrencySymbol; - /** - * The network name or identifier - */ - network: string; -}; - /** * Throughout the extension we set the current provider by referencing its * "type", which can be any of the values in the below object. These values @@ -908,108 +892,6 @@ export const UNSUPPORTED_RPC_METHODS = new Set([ export const IPFS_DEFAULT_GATEWAY_URL = 'dweb.link'; -// The first item in transakCurrencies must be the -// default crypto currency for the network -const BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME = 'ethereum'; - -export const BUYABLE_CHAINS_MAP: { - [K in Exclude< - ChainId, - | typeof CHAIN_IDS.LOCALHOST - | typeof CHAIN_IDS.OPTIMISM_TESTNET - | typeof CHAIN_IDS.OPTIMISM_GOERLI - | typeof CHAIN_IDS.BASE_TESTNET - | typeof CHAIN_IDS.OPBNB_TESTNET - | typeof CHAIN_IDS.OPBNB - | typeof CHAIN_IDS.BSC_TESTNET - | typeof CHAIN_IDS.POLYGON_TESTNET - | typeof CHAIN_IDS.AVALANCHE_TESTNET - | typeof CHAIN_IDS.FANTOM_TESTNET - | typeof CHAIN_IDS.MOONBEAM_TESTNET - | typeof CHAIN_IDS.LINEA_GOERLI - | typeof CHAIN_IDS.LINEA_SEPOLIA - | typeof CHAIN_IDS.GOERLI - | typeof CHAIN_IDS.SEPOLIA - | typeof CHAIN_IDS.GNOSIS - | typeof CHAIN_IDS.AURORA - | typeof CHAIN_IDS.ARBITRUM_GOERLI - | typeof CHAIN_IDS.BLAST - | typeof CHAIN_IDS.FILECOIN - | typeof CHAIN_IDS.POLYGON_ZKEVM - | typeof CHAIN_IDS.SCROLL - | typeof CHAIN_IDS.SCROLL_SEPOLIA - | typeof CHAIN_IDS.WETHIO - | typeof CHAIN_IDS.CHZ - | typeof CHAIN_IDS.NUMBERS - | typeof CHAIN_IDS.SEI - >]: BuyableChainSettings; -} = { - [CHAIN_IDS.MAINNET]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME, - }, - [CHAIN_IDS.BSC]: { - nativeCurrency: CURRENCY_SYMBOLS.BNB, - network: 'bsc', - }, - [CHAIN_IDS.POLYGON]: { - nativeCurrency: CURRENCY_SYMBOLS.MATIC, - network: 'polygon', - }, - [CHAIN_IDS.AVALANCHE]: { - nativeCurrency: CURRENCY_SYMBOLS.AVALANCHE, - network: 'avaxcchain', - }, - [CHAIN_IDS.FANTOM]: { - nativeCurrency: CURRENCY_SYMBOLS.FANTOM, - network: 'fantom', - }, - [CHAIN_IDS.CELO]: { - nativeCurrency: CURRENCY_SYMBOLS.CELO, - network: 'celo', - }, - [CHAIN_IDS.OPTIMISM]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: 'optimism', - }, - [CHAIN_IDS.ARBITRUM]: { - nativeCurrency: CURRENCY_SYMBOLS.ARBITRUM, - network: 'arbitrum', - }, - [CHAIN_IDS.CRONOS]: { - nativeCurrency: CURRENCY_SYMBOLS.CRONOS, - network: 'cronos', - }, - [CHAIN_IDS.MOONBEAM]: { - nativeCurrency: CURRENCY_SYMBOLS.GLIMMER, - network: 'moonbeam', - }, - [CHAIN_IDS.MOONRIVER]: { - nativeCurrency: CURRENCY_SYMBOLS.MOONRIVER, - network: 'moonriver', - }, - [CHAIN_IDS.HARMONY]: { - nativeCurrency: CURRENCY_SYMBOLS.ONE, - network: 'harmony', - }, - [CHAIN_IDS.PALM]: { - nativeCurrency: CURRENCY_SYMBOLS.PALM, - network: 'palm', - }, - [CHAIN_IDS.LINEA_MAINNET]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: 'linea', - }, - [CHAIN_IDS.ZKSYNC_ERA]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: 'zksync', - }, - [CHAIN_IDS.BASE]: { - nativeCurrency: CURRENCY_SYMBOLS.ETH, - network: 'base', - }, -}; - export const FEATURED_RPCS: RPCDefinition[] = [ { chainId: CHAIN_IDS.ARBITRUM, diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index c8604c96c055..5beee30824ce 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -1242,6 +1242,143 @@ ], "swapsState": {} }, + "ramps": { + "buyableChains": [ + { + "active": true, + "chainId": 1, + "chainName": "Ethereum Mainnet", + "shortName": "Ethereum", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 10, + "chainName": "Optimism Mainnet", + "shortName": "Optimism", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 25, + "chainName": "Cronos Mainnet", + "shortName": "Cronos", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 56, + "chainName": "BNB Chain Mainnet", + "shortName": "BNB Chain", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 100, + "chainName": "Gnosis Mainnet", + "shortName": "Gnosis", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 137, + "chainName": "Polygon Mainnet", + "shortName": "Polygon", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 250, + "chainName": "Fantom Mainnet", + "shortName": "Fantom", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 324, + "chainName": "zkSync Era Mainnet", + "shortName": "zkSync Era", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1101, + "chainName": "Polygon zkEVM", + "shortName": "Polygon zkEVM", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1284, + "chainName": "Moonbeam Mainnet", + "shortName": "Moonbeam", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1285, + "chainName": "Moonriver Mainnet", + "shortName": "Moonriver", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 8453, + "chainName": "Base Mainnet", + "shortName": "Base", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42161, + "chainName": "Arbitrum Mainnet", + "shortName": "Arbitrum", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42220, + "chainName": "Celo Mainnet", + "shortName": "Celo", + "nativeTokenSupported": false + }, + { + "active": true, + "chainId": 43114, + "chainName": "Avalanche C-Chain Mainnet", + "shortName": "Avalanche C-Chain", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 59144, + "chainName": "Linea", + "shortName": "Linea", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1313161554, + "chainName": "Aurora Mainnet", + "shortName": "Aurora", + "nativeTokenSupported": false + }, + { + "active": true, + "chainId": 1666600000, + "chainName": "Harmony Mainnet (Shard 0)", + "shortName": "Harmony (Shard 0)", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 11297108109, + "chainName": "Palm Mainnet", + "shortName": "Palm", + "nativeTokenSupported": false + } + ] + }, "send": { "amountMode": "INPUT", "currentTransactionUUID": "1-tx", diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 920533023a58..9e595ad8f0e0 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -1943,6 +1943,143 @@ } } }, + "ramps": { + "buyableChains": [ + { + "active": true, + "chainId": 1, + "chainName": "Ethereum Mainnet", + "shortName": "Ethereum", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 10, + "chainName": "Optimism Mainnet", + "shortName": "Optimism", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 25, + "chainName": "Cronos Mainnet", + "shortName": "Cronos", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 56, + "chainName": "BNB Chain Mainnet", + "shortName": "BNB Chain", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 100, + "chainName": "Gnosis Mainnet", + "shortName": "Gnosis", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 137, + "chainName": "Polygon Mainnet", + "shortName": "Polygon", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 250, + "chainName": "Fantom Mainnet", + "shortName": "Fantom", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 324, + "chainName": "zkSync Era Mainnet", + "shortName": "zkSync Era", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1101, + "chainName": "Polygon zkEVM", + "shortName": "Polygon zkEVM", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1284, + "chainName": "Moonbeam Mainnet", + "shortName": "Moonbeam", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1285, + "chainName": "Moonriver Mainnet", + "shortName": "Moonriver", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 8453, + "chainName": "Base Mainnet", + "shortName": "Base", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42161, + "chainName": "Arbitrum Mainnet", + "shortName": "Arbitrum", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42220, + "chainName": "Celo Mainnet", + "shortName": "Celo", + "nativeTokenSupported": false + }, + { + "active": true, + "chainId": 43114, + "chainName": "Avalanche C-Chain Mainnet", + "shortName": "Avalanche C-Chain", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 59144, + "chainName": "Linea", + "shortName": "Linea", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1313161554, + "chainName": "Aurora Mainnet", + "shortName": "Aurora", + "nativeTokenSupported": false + }, + { + "active": true, + "chainId": 1666600000, + "chainName": "Harmony Mainnet (Shard 0)", + "shortName": "Harmony (Shard 0)", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 11297108109, + "chainName": "Palm Mainnet", + "shortName": "Palm", + "nativeTokenSupported": false + } + ] + }, "send": { "amountMode": "INPUT", "currentTransactionUUID": null, 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 1dd7775ec70f..4b7972acd311 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 @@ -257,6 +257,7 @@ "encryptionKey": "string", "encryptionSalt": "string" }, + "ramps": "object", "send": "object", "swaps": "object", "unconnectedAccount": { "state": "CLOSED" } diff --git a/ui/components/app/add-network/add-network.test.js b/ui/components/app/add-network/add-network.test.js index 3a15f4d33c3e..f3084b1db065 100644 --- a/ui/components/app/add-network/add-network.test.js +++ b/ui/components/app/add-network/add-network.test.js @@ -6,6 +6,7 @@ import mockState from '../../../../test/data/mock-state.json'; import AddNetwork from './add-network'; jest.mock('../../../selectors', () => ({ + ...jest.requireActual('../../../selectors'), getNetworkConfigurations: () => ({ networkConfigurationId: { chainId: '0x539', diff --git a/ui/components/app/asset-list/asset-list.js b/ui/components/app/asset-list/asset-list.js index 82c45a27134e..959789b8eebc 100644 --- a/ui/components/app/asset-list/asset-list.js +++ b/ui/components/app/asset-list/asset-list.js @@ -9,9 +9,6 @@ import { getDetectedTokensInCurrentNetwork, getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, getShouldHideZeroBalanceTokens, - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - getIsBuyableChain, - ///: END:ONLY_INCLUDE_IF getSelectedAccount, getPreferences, } from '../../../selectors'; @@ -48,6 +45,7 @@ import { RAMPS_CARD_VARIANT_TYPES, RampsCard, } from '../../multichain/ramps-card/ramps-card'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF const AssetList = ({ onClickAsset }) => { @@ -109,7 +107,7 @@ const AssetList = ({ onClickAsset }) => { }); const balanceIsZero = Number(totalFiatBalance) === 0; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const shouldShowBuy = isBuyableChain && balanceIsZero; ///: END:ONLY_INCLUDE_IF diff --git a/ui/components/app/nfts-tab/nfts-tab.js b/ui/components/app/nfts-tab/nfts-tab.js index bc54ca4c9766..43a0bd1a9d64 100644 --- a/ui/components/app/nfts-tab/nfts-tab.js +++ b/ui/components/app/nfts-tab/nfts-tab.js @@ -17,7 +17,6 @@ import { useNftsCollections } from '../../../hooks/useNftsCollections'; import { getCurrentNetwork, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - getIsBuyableChain, getShouldHideZeroBalanceTokens, getSelectedAccount, ///: END:ONLY_INCLUDE_IF @@ -49,6 +48,7 @@ import { RampsCard, } from '../../multichain/ramps-card/ramps-card'; import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF import Spinner from '../../ui/spinner'; @@ -73,7 +73,7 @@ export default function NftsTab() { shouldHideZeroBalanceTokens, ); const balanceIsZero = Number(totalFiatBalance) === 0; - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const showRampsCard = isBuyableChain && balanceIsZero; ///: END:ONLY_INCLUDE_IF diff --git a/ui/components/app/selected-account/selected-account-component.test.js b/ui/components/app/selected-account/selected-account-component.test.js index a48e24a661c6..ae8a3ff000db 100644 --- a/ui/components/app/selected-account/selected-account-component.test.js +++ b/ui/components/app/selected-account/selected-account-component.test.js @@ -52,6 +52,7 @@ jest.mock('../../../selectors', () => { return { getAccountType: mockGetAccountType, getSelectedInternalAccount: mockGetSelectedAccount, + getCurrentChainId: jest.fn(() => '0x1'), }; }); diff --git a/ui/components/app/transaction-list/transaction-list.component.js b/ui/components/app/transaction-list/transaction-list.component.js index b47130c5d739..20c41ef33e4d 100644 --- a/ui/components/app/transaction-list/transaction-list.component.js +++ b/ui/components/app/transaction-list/transaction-list.component.js @@ -10,7 +10,6 @@ import { getCurrentChainId, getSelectedAccount, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - getIsBuyableChain, getShouldHideZeroBalanceTokens, ///: END:ONLY_INCLUDE_IF } from '../../../selectors'; @@ -33,6 +32,7 @@ import { RAMPS_CARD_VARIANT_TYPES, RampsCard, } from '../../multichain/ramps-card/ramps-card'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF const PAGE_INCREMENT = 10; @@ -141,8 +141,7 @@ export default function TransactionList({ shouldHideZeroBalanceTokens, ); const balanceIsZero = Number(totalFiatBalance) === 0; - const isBuyableChain = useSelector(getIsBuyableChain); - + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const showRampsCard = isBuyableChain && balanceIsZero; ///: 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 5bba897121e4..8eb24128b60e 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -55,7 +55,8 @@ import { Box, Icon, IconName } from '../../component-library'; import IconButton from '../../ui/icon-button'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; -import useRamps from '../../../hooks/experiences/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; + ///: END:ONLY_INCLUDE_IF const CoinButtons = ({ diff --git a/ui/components/app/wallet-overview/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js index 5eeec7a59cbf..684c6cffbf6b 100644 --- a/ui/components/app/wallet-overview/eth-overview.js +++ b/ui/components/app/wallet-overview/eth-overview.js @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; - import { EthMethod } from '@metamask/keyring-api'; import { isEqual } from 'lodash'; import { @@ -13,15 +12,17 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getSwapsDefaultToken, getIsBridgeChain, - getIsBuyableChain, ///: END:ONLY_INCLUDE_IF } from '../../../selectors'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; +///: END:ONLY_INCLUDE_IF import { CoinOverview } from './coin-overview'; const EthOverview = ({ className }) => { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const isBridgeChain = useSelector(getIsBridgeChain); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); // FIXME: This causes re-renders, so use isEqual to avoid this const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); ///: END:ONLY_INCLUDE_IF diff --git a/ui/components/app/wallet-overview/eth-overview.test.js b/ui/components/app/wallet-overview/eth-overview.test.js index 0d13bff7e1ef..0d079a32f104 100644 --- a/ui/components/app/wallet-overview/eth-overview.test.js +++ b/ui/components/app/wallet-overview/eth-overview.test.js @@ -12,27 +12,11 @@ import { import { renderWithProvider } from '../../../../test/jest/rendering'; import { KeyringType } from '../../../../shared/constants/keyring'; import { useIsOriginalNativeTokenSymbol } from '../../../hooks/useIsOriginalNativeTokenSymbol'; +import { defaultBuyableChains } from '../../../ducks/ramps/constants'; import { ETH_EOA_METHODS } from '../../../../shared/constants/eth-methods'; import { getIntlLocale } from '../../../ducks/locale/locale'; import EthOverview from './eth-overview'; -// Mock BUYABLE_CHAINS_MAP -jest.mock('../../../../shared/constants/network', () => ({ - ...jest.requireActual('../../../../shared/constants/network'), - BUYABLE_CHAINS_MAP: { - // MAINNET - '0x1': { - nativeCurrency: 'ETH', - network: 'ethereum', - }, - // POLYGON - '0x89': { - nativeCurrency: 'MATIC', - network: 'polygon', - }, - }, -})); - jest.mock('../../../hooks/useIsOriginalNativeTokenSymbol', () => { return { useIsOriginalNativeTokenSymbol: jest.fn(), @@ -138,6 +122,9 @@ describe('EthOverview', () => { }, ], }, + ramps: { + buyableChains: defaultBuyableChains, + }, }; const store = configureMockStore([thunk])(mockStore); @@ -181,6 +168,7 @@ describe('EthOverview', () => { it('should show the cached primary balance', async () => { const mockedStoreWithCachedBalance = { + ...mockStore, metamask: { ...mockStore.metamask, accounts: { @@ -267,6 +255,7 @@ describe('EthOverview', () => { it('should open the MMI PD Swaps URI when clicking on Swap button with a Custody account', async () => { const mockedStoreWithCustodyKeyring = { + ...mockStore, metamask: { ...mockStore.metamask, mmiConfiguration: { @@ -375,6 +364,7 @@ describe('EthOverview', () => { it('should have the Buy native token button disabled if chain id is not part of supported buyable chains', () => { const mockedStoreWithUnbuyableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { @@ -399,6 +389,7 @@ describe('EthOverview', () => { it('should have the Buy native token enabled if chain id is part of supported buyable chains', () => { const mockedStoreWithUnbuyableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { @@ -432,6 +423,7 @@ describe('EthOverview', () => { it('should open the Buy native token URI when clicking on Buy button for a buyable chain ID', async () => { const mockedStoreWithBuyableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { diff --git a/ui/components/multichain/ramps-card/ramps-card.js b/ui/components/multichain/ramps-card/ramps-card.js index e1c61f2a35c1..2fa793b7e958 100644 --- a/ui/components/multichain/ramps-card/ramps-card.js +++ b/ui/components/multichain/ramps-card/ramps-card.js @@ -21,7 +21,7 @@ import { import { MetaMetricsContext } from '../../../contexts/metametrics'; import useRamps, { RampsMetaMaskEntry, -} from '../../../hooks/experiences/useRamps'; +} from '../../../hooks/ramps/useRamps/useRamps'; import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; import { getCurrentLocale } from '../../../ducks/locale/locale'; diff --git a/ui/ducks/index.js b/ui/ducks/index.js index 145dd708cb90..f72918460655 100644 --- a/ui/ducks/index.js +++ b/ui/ducks/index.js @@ -11,6 +11,7 @@ import gasReducer from './gas/gas.duck'; import { invalidCustomNetwork, unconnectedAccount } from './alerts'; import swapsReducer from './swaps/swaps'; import historyReducer from './history/history'; +import rampsReducer from './ramps/ramps'; import confirmAlertsReducer from './confirm-alerts/confirm-alerts'; export default combineReducers({ @@ -26,6 +27,7 @@ export default combineReducers({ confirmAlerts: confirmAlertsReducer, confirmTransaction: confirmTransactionReducer, swaps: swapsReducer, + ramps: rampsReducer, gas: gasReducer, localeMessages: localeMessagesReducer, }); diff --git a/ui/ducks/ramps/constants.ts b/ui/ducks/ramps/constants.ts new file mode 100644 index 000000000000..7a451658807e --- /dev/null +++ b/ui/ducks/ramps/constants.ts @@ -0,0 +1,137 @@ +import { AggregatorNetwork } from './types'; + +export const defaultBuyableChains: AggregatorNetwork[] = [ + { + active: true, + chainId: 1, + chainName: 'Ethereum Mainnet', + shortName: 'Ethereum', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 10, + chainName: 'Optimism Mainnet', + shortName: 'Optimism', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 25, + chainName: 'Cronos Mainnet', + shortName: 'Cronos', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 56, + chainName: 'BNB Chain Mainnet', + shortName: 'BNB Chain', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 100, + chainName: 'Gnosis Mainnet', + shortName: 'Gnosis', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 137, + chainName: 'Polygon Mainnet', + shortName: 'Polygon', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 250, + chainName: 'Fantom Mainnet', + shortName: 'Fantom', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 324, + chainName: 'zkSync Era Mainnet', + shortName: 'zkSync Era', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 1101, + chainName: 'Polygon zkEVM', + shortName: 'Polygon zkEVM', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 1284, + chainName: 'Moonbeam Mainnet', + shortName: 'Moonbeam', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 1285, + chainName: 'Moonriver Mainnet', + shortName: 'Moonriver', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 8453, + chainName: 'Base Mainnet', + shortName: 'Base', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 42161, + chainName: 'Arbitrum Mainnet', + shortName: 'Arbitrum', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 42220, + chainName: 'Celo Mainnet', + shortName: 'Celo', + nativeTokenSupported: false, + }, + { + active: true, + chainId: 43114, + chainName: 'Avalanche C-Chain Mainnet', + shortName: 'Avalanche C-Chain', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 59144, + chainName: 'Linea', + shortName: 'Linea', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 1313161554, + chainName: 'Aurora Mainnet', + shortName: 'Aurora', + nativeTokenSupported: false, + }, + { + active: true, + chainId: 1666600000, + chainName: 'Harmony Mainnet (Shard 0)', + shortName: 'Harmony (Shard 0)', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 11297108109, + chainName: 'Palm Mainnet', + shortName: 'Palm', + nativeTokenSupported: false, + }, +]; diff --git a/ui/ducks/ramps/index.ts b/ui/ducks/ramps/index.ts new file mode 100644 index 000000000000..b6f8f2473810 --- /dev/null +++ b/ui/ducks/ramps/index.ts @@ -0,0 +1 @@ +export * from './ramps'; diff --git a/ui/ducks/ramps/ramps.test.ts b/ui/ducks/ramps/ramps.test.ts new file mode 100644 index 000000000000..c4ca4089815a --- /dev/null +++ b/ui/ducks/ramps/ramps.test.ts @@ -0,0 +1,187 @@ +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 rampsReducer, { + fetchBuyableChains, + getBuyableChains, + getIsNativeTokenBuyable, +} from './ramps'; +import { defaultBuyableChains } from './constants'; + +jest.mock('../../helpers/ramps/rampApi/rampAPI'); +const mockedRampAPI = RampAPI as jest.Mocked; + +jest.mock('../../selectors', () => ({ + getCurrentChainId: jest.fn(), + getUseExternalServices: jest.fn(), + getNames: jest.fn(), +})); + +describe('rampsSlice', () => { + let store: Store; + + beforeEach(() => { + store = configureStore({ + reducer: { + ramps: rampsReducer, + }, + }); + mockedRampAPI.getNetworks.mockReset(); + }); + + it('should set the initial state to defaultBuyableChains', () => { + const { ramps: rampsState } = store.getState(); + expect(rampsState).toEqual({ + buyableChains: defaultBuyableChains, + }); + }); + + describe('setBuyableChains', () => { + it('should update the buyableChains state when setBuyableChains is dispatched', () => { + const mockBuyableChains = [{ chainId: '0x1' }]; + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: mockBuyableChains, + }); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(mockBuyableChains); + }); + it('should disregard invalid array and set buyableChains to default', () => { + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: 'Invalid array', + }); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + + it('should disregard empty array and set buyableChains to default', () => { + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: [], + }); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + + it('should disregard array with invalid elements and set buyableChains to default', () => { + store.dispatch({ + type: 'ramps/setBuyableChains', + payload: ['some invalid', 'element'], + }); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + }); + + describe('getBuyableChains', () => { + it('returns buyableChains', () => { + const state = store.getState(); + expect(getBuyableChains(state)).toBe(state.ramps.buyableChains); + }); + }); + + describe('fetchBuyableChains', () => { + beforeEach(() => { + // simulate the Basic Functionality Toggle being on + const getUseExternalServicesMock = jest.mocked(getUseExternalServices); + getUseExternalServicesMock.mockReturnValue(true); + }); + + it('should call RampAPI.getNetworks when the Basic Functionality Toggle is on', async () => { + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + expect(RampAPI.getNetworks).toHaveBeenCalledTimes(1); + }); + + it('should not call RampAPI.getNetworks when the Basic Functionality Toggle is off', async () => { + const getUseExternalServicesMock = jest.mocked(getUseExternalServices); + getUseExternalServicesMock.mockReturnValue(false); + + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + + expect(RampAPI.getNetworks).not.toHaveBeenCalled(); + }); + + it('should update the state with the data that is returned', async () => { + const mockBuyableChains = [ + { + active: true, + chainId: 1, + chainName: 'Ethereum Mainnet', + nativeTokenSupported: true, + shortName: 'Ethereum', + }, + ]; + jest.spyOn(RampAPI, 'getNetworks').mockResolvedValue(mockBuyableChains); + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(mockBuyableChains); + }); + it('should set state to defaultBuyableChains when returned networks are undefined', async () => { + // @ts-expect-error forcing undefined to test the behavior + jest.spyOn(RampAPI, 'getNetworks').mockResolvedValue(undefined); + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + + it('should set state to defaultBuyableChains when returned networks are empty', async () => { + jest.spyOn(RampAPI, 'getNetworks').mockResolvedValue([]); + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + + it('should set state to defaultBuyableChains when API request fails', async () => { + jest + .spyOn(RampAPI, 'getNetworks') + .mockRejectedValue(new Error('API error')); + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + const { ramps: rampsState } = store.getState(); + expect(rampsState.buyableChains).toEqual(defaultBuyableChains); + }); + }); + + describe('getIsNativeTokenBuyable', () => { + const getCurrentChainIdMock = jest.mocked(getCurrentChainId); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should return true when current chain is buyable', () => { + getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); + const state = store.getState(); + expect(getIsNativeTokenBuyable(state)).toEqual(true); + }); + + it('should return false when current chain is not buyable', () => { + getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.GOERLI); + const state = store.getState(); + expect(getIsNativeTokenBuyable(state)).toEqual(false); + }); + + it('should return false when current chain is not a valid hex string', () => { + getCurrentChainIdMock.mockReturnValue('0x'); + const state = store.getState(); + expect(getIsNativeTokenBuyable(state)).toEqual(false); + }); + + it('should return false when buyable chains is a corrupted array', () => { + const mockState = { + ramps: { + buyableChains: [null, null, null], + }, + }; + getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); + expect(getIsNativeTokenBuyable(mockState)).toEqual(false); + }); + }); +}); diff --git a/ui/ducks/ramps/ramps.ts b/ui/ducks/ramps/ramps.ts new file mode 100644 index 000000000000..afff609cd4d8 --- /dev/null +++ b/ui/ducks/ramps/ramps.ts @@ -0,0 +1,78 @@ +import { createSelector } from 'reselect'; +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 { defaultBuyableChains } from './constants'; +import { AggregatorNetwork } from './types'; + +export const fetchBuyableChains = createAsyncThunk( + 'ramps/fetchBuyableChains', + async (_, { getState }) => { + const state = getState(); + const allowExternalRequests = getUseExternalServices(state); + if (!allowExternalRequests) { + return defaultBuyableChains; + } + return await RampAPI.getNetworks(); + }, +); + +const rampsSlice = createSlice({ + name: 'ramps', + initialState: { + buyableChains: defaultBuyableChains, + }, + reducers: { + setBuyableChains: (state, action) => { + if ( + Array.isArray(action.payload) && + action.payload.length > 0 && + action.payload.every((network) => network?.chainId) + ) { + state.buyableChains = action.payload; + } else { + state.buyableChains = defaultBuyableChains; + } + }, + }, + extraReducers: (builder) => { + builder + .addCase(fetchBuyableChains.fulfilled, (state, action) => { + const networks = action.payload; + if (networks && networks.length > 0) { + state.buyableChains = networks; + } else { + state.buyableChains = defaultBuyableChains; + } + }) + .addCase(fetchBuyableChains.rejected, (state) => { + state.buyableChains = defaultBuyableChains; + }); + }, +}); + +const { reducer } = rampsSlice; + +// Can be typed to RootState if/when the interface is defined +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const getBuyableChains = (state: any) => + state.ramps?.buyableChains ?? defaultBuyableChains; + +export const getIsNativeTokenBuyable = createSelector( + [getCurrentChainId, getBuyableChains], + (currentChainId, buyableChains) => { + try { + return buyableChains + .filter(Boolean) + .some( + (network: AggregatorNetwork) => + String(network.chainId) === hexToDecimal(currentChainId), + ); + } catch (e) { + return false; + } + }, +); + +export default reducer; diff --git a/ui/ducks/ramps/types.ts b/ui/ducks/ramps/types.ts new file mode 100644 index 000000000000..6a1571715dfe --- /dev/null +++ b/ui/ducks/ramps/types.ts @@ -0,0 +1,7 @@ +export type AggregatorNetwork = { + active: boolean; + chainId: number; + chainName: string; + nativeTokenSupported: boolean; + shortName: string; +}; diff --git a/ui/helpers/ramps/rampApi/rampAPI.test.ts b/ui/helpers/ramps/rampApi/rampAPI.test.ts new file mode 100644 index 000000000000..bf6e2297481d --- /dev/null +++ b/ui/helpers/ramps/rampApi/rampAPI.test.ts @@ -0,0 +1,23 @@ +import nock from 'nock'; +import { defaultBuyableChains } from '../../../ducks/ramps/constants'; +import rampAPI from './rampAPI'; + +const mockedResponse = { + networks: defaultBuyableChains, +}; + +describe('rampAPI', () => { + afterEach(() => { + nock.cleanAll(); + }); + + it('should fetch networks', async () => { + nock('https://on-ramp-content.uat-api.cx.metamask.io') + .get('/regions/networks') + .query(true) + .reply(200, mockedResponse); + + const result = await rampAPI.getNetworks(); + expect(result).toStrictEqual(mockedResponse.networks); + }); +}); diff --git a/ui/helpers/ramps/rampApi/rampAPI.ts b/ui/helpers/ramps/rampApi/rampAPI.ts new file mode 100644 index 000000000000..a1da6da8ef0c --- /dev/null +++ b/ui/helpers/ramps/rampApi/rampAPI.ts @@ -0,0 +1,25 @@ +import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout'; +import { AggregatorNetwork } from '../../../ducks/ramps/types'; + +const fetchWithTimeout = getFetchWithTimeout(); + +const isProdEnv = process.env.NODE_ENV === 'production'; +const PROD_RAMP_API_BASE_URL = 'https://on-ramp-content.api.cx.metamask.io'; +const UAT_RAMP_API_BASE_URL = 'https://on-ramp-content.uat-api.cx.metamask.io'; + +const rampApiBaseUrl = + process.env.METAMASK_RAMP_API_CONTENT_BASE_URL || + (isProdEnv ? PROD_RAMP_API_BASE_URL : UAT_RAMP_API_BASE_URL); + +const RampAPI = { + async getNetworks(): Promise { + 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; + }, +}; + +export default RampAPI; diff --git a/ui/hooks/experiences/useRamps.test.js b/ui/hooks/ramps/useRamps/useRamps.test.tsx similarity index 85% rename from ui/hooks/experiences/useRamps.test.js rename to ui/hooks/ramps/useRamps/useRamps.test.tsx index dcb1dc2eb43c..9c53eb9da247 100644 --- a/ui/hooks/experiences/useRamps.test.js +++ b/ui/hooks/ramps/useRamps/useRamps.test.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import React, { FC } from 'react'; import { Provider } from 'react-redux'; import { renderHook } from '@testing-library/react-hooks'; -import configureStore from '../../store/store'; +import configureStore from '../../../store/store'; import useRamps, { RampsMetaMaskEntry } from './useRamps'; const mockedMetametricsId = '0xtestMetaMetricsId'; @@ -15,17 +15,18 @@ let mockStoreState = { }, }; -const wrapper = ({ children }) => ( +const wrapper: FC = ({ children }) => ( {children} ); describe('useRamps', () => { - beforeEach(() => { - global.platform = { openTab: jest.fn() }; - }); - - afterEach(() => { - jest.clearAllMocks(); + // mock the openTab function to test if it is called with the correct URL when opening the Pdapp + beforeAll(() => { + Object.defineProperty(global, 'platform', { + value: { + openTab: jest.fn(), + }, + }); }); it('should default the metamask entry param when opening the buy crypto URL', () => { @@ -81,9 +82,11 @@ describe('useRamps', () => { }); }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore it.each(['0x1', '0x38', '0xa'])( 'should open the buy crypto URL with the currently connected chain ID', - (mockChainId) => { + (mockChainId: string) => { mockStoreState = { ...mockStoreState, metamask: { diff --git a/ui/hooks/experiences/useRamps.ts b/ui/hooks/ramps/useRamps/useRamps.ts similarity index 90% rename from ui/hooks/experiences/useRamps.ts rename to ui/hooks/ramps/useRamps/useRamps.ts index 76bcdfe47879..7219d5fe4193 100644 --- a/ui/hooks/experiences/useRamps.ts +++ b/ui/hooks/ramps/useRamps/useRamps.ts @@ -1,8 +1,8 @@ import { useCallback } from 'react'; import { useSelector } from 'react-redux'; import type { Hex } from '@metamask/utils'; -import { ChainId } from '../../../shared/constants/network'; -import { getCurrentChainId, getMetaMetricsId } from '../../selectors'; +import { ChainId } from '../../../../shared/constants/network'; +import { getCurrentChainId, getMetaMetricsId } from '../../../selectors'; type IUseRamps = { openBuyCryptoInPdapp: VoidFunction; diff --git a/ui/hooks/useTheme.test.ts b/ui/hooks/useTheme.test.ts index 8b0f2edd66ac..5b44d2c918eb 100644 --- a/ui/hooks/useTheme.test.ts +++ b/ui/hooks/useTheme.test.ts @@ -3,6 +3,7 @@ import { renderHookWithProvider } from '../../test/lib/render-helpers'; import { useTheme } from './useTheme'; jest.mock('../selectors', () => ({ + ...jest.requireActual('../selectors'), getTheme: jest.fn(), })); diff --git a/ui/pages/asset/components/asset-page.tsx b/ui/pages/asset/components/asset-page.tsx index d83f6cfe24f8..38ea46e475f3 100644 --- a/ui/pages/asset/components/asset-page.tsx +++ b/ui/pages/asset/components/asset-page.tsx @@ -7,7 +7,6 @@ import { isEqual } from 'lodash'; import { getCurrentCurrency, getIsBridgeChain, - getIsBuyableChain, getIsSwapsChain, getSelectedInternalAccount, getSwapsDefaultToken, @@ -42,6 +41,7 @@ import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { getConversionRate } from '../../../ducks/metamask/metamask'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import CoinButtons from '../../../components/app/wallet-overview/coin-buttons'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; import AssetChart from './chart/asset-chart'; import TokenButtons from './token-buttons'; @@ -102,7 +102,7 @@ const AssetPage = ({ const conversionRate = useSelector(getConversionRate); const allMarketData = useSelector(getTokensMarketData); const isBridgeChain = useSelector(getIsBridgeChain); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); const account = useSelector(getSelectedInternalAccount, isEqual); const isSwapsChain = useSelector(getIsSwapsChain); diff --git a/ui/pages/asset/components/token-buttons.tsx b/ui/pages/asset/components/token-buttons.tsx index f0e39f046e24..6cd78fab7693 100644 --- a/ui/pages/asset/components/token-buttons.tsx +++ b/ui/pages/asset/components/token-buttons.tsx @@ -13,7 +13,7 @@ import { startNewDraftTransaction } from '../../../ducks/send'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { isHardwareKeyring } from '../../../helpers/utils/hardware'; import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; -import useRamps from '../../../hooks/experiences/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -28,11 +28,9 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getIsBridgeChain, getCurrentKeyring, - getIsBuyableChain, getMetaMetricsId, ///: END:ONLY_INCLUDE_IF } from '../../../selectors'; - import { INVALID_ASSET_TYPE } from '../../../helpers/constants/error-keys'; import { showModal } from '../../../store/actions'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -42,7 +40,6 @@ import { MetaMetricsSwapsEventSource, } from '../../../../shared/constants/metametrics'; import { AssetType } from '../../../../shared/constants/transaction'; - import { Display, IconColor, @@ -50,6 +47,9 @@ import { } from '../../../helpers/constants/design-system'; import IconButton from '../../../components/ui/icon-button/icon-button'; import { Box, Icon, IconName } from '../../../components/component-library'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; +///: END:ONLY_INCLUDE_IF import { Asset } from './asset-page'; const TokenButtons = ({ @@ -71,7 +71,7 @@ const TokenButtons = ({ const isSwapsChain = useSelector(getIsSwapsChain); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const isBridgeChain = useSelector(getIsBridgeChain); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const metaMetricsId = useSelector(getMetaMetricsId); const { openBuyCryptoInPdapp } = useRamps(); ///: END:ONLY_INCLUDE_IF diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js index d177bc6b8df2..d4a92172aed6 100644 --- a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js +++ b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js @@ -43,12 +43,11 @@ import { getAccountName, getAddressBookEntry, getInternalAccounts, - getIsBuyableChain, getMetadataContractName, getNetworkIdentifier, getSwapsDefaultToken, } from '../../../../selectors'; -import useRamps from '../../../../hooks/experiences/useRamps'; +import useRamps from '../../../../hooks/ramps/useRamps/useRamps'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { @@ -58,6 +57,7 @@ import { ///: END:ONLY_INCLUDE_IF import { BlockaidResultType } from '../../../../../shared/constants/security-provider'; +import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; import { ConfirmPageContainerHeader, ConfirmPageContainerContent, @@ -118,7 +118,7 @@ const ConfirmPageContainer = (props) => { const [collectionBalance, setCollectionBalance] = useState('0'); const [isShowingTxInsightWarnings, setIsShowingTxInsightWarnings] = useState(false); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const contact = useSelector((state) => getAddressBookEntry(state, toAddress)); const networkIdentifier = useSelector(getNetworkIdentifier); const defaultToken = useSelector(getSwapsDefaultToken); @@ -131,10 +131,6 @@ const ConfirmPageContainer = (props) => { getMetadataContractName(state, toAddress), ); - // TODO: Move useRamps hook to the confirm-transaction-base parent component. - // TODO: openBuyCryptoInPdapp should be passed to this component as a custom prop. - // We try to keep this component for layout purpose only, we need to move this hook to the confirm-transaction-base parent - // component once it is converted to a functional component const { openBuyCryptoInPdapp } = useRamps(); const isSetApproveForAll = diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js index be3ba7793eaf..db07ad4117e7 100644 --- a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js +++ b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js @@ -1,7 +1,6 @@ import { connect } from 'react-redux'; import { getAddressBookEntry, - getIsBuyableChain, getNetworkIdentifier, getSwapsDefaultToken, getMetadataContractName, @@ -12,7 +11,6 @@ import ConfirmPageContainer from './confirm-page-container.component'; function mapStateToProps(state, ownProps) { const to = ownProps.toAddress; - const isBuyableChain = getIsBuyableChain(state); const contact = getAddressBookEntry(state, to); const networkIdentifier = getNetworkIdentifier(state); const defaultToken = getSwapsDefaultToken(state); @@ -23,7 +21,6 @@ function mapStateToProps(state, ownProps) { const toMetadataName = getMetadataContractName(state, to); return { - isBuyableChain, contact, toName, toMetadataName, diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js index 7c8fb8f350f2..201633b13a89 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js @@ -45,7 +45,6 @@ import { getPreferences, doesAddressRequireLedgerHidConnection, getTokenList, - getIsBuyableChain, getEnsResolutionByAddress, getUnapprovedTransaction, getFullTxData, @@ -107,6 +106,7 @@ import { showCustodyConfirmLink } from '../../../store/institutional/institution ///: END:ONLY_INCLUDE_IF import { calcGasTotal } from '../../../../shared/lib/transactions-controller-utils'; import { subtractHexes } from '../../../../shared/modules/conversion.utils'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; import ConfirmTransactionBase from './confirm-transaction-base.component'; let customNonceValue = ''; @@ -162,7 +162,7 @@ const mapStateToProps = (state, ownProps) => { const isGasEstimatesLoading = getIsGasEstimatesLoading(state); const gasLoadingAnimationIsShowing = getGasLoadingAnimationIsShowing(state); - const isBuyableChain = getIsBuyableChain(state); + const isBuyableChain = getIsNativeTokenBuyable(state); const { confirmTransaction, metamask } = state; const conversionRate = getConversionRate(state); const { addressBook, nextNonce } = metamask; diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js index 918c24ed6681..bfa23af0af75 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js @@ -27,6 +27,7 @@ import { BlockaidReason, BlockaidResultType, } from '../../../../shared/constants/security-provider'; +import { defaultBuyableChains } from '../../../ducks/ramps/constants'; import { ETH_EOA_METHODS } from '../../../../shared/constants/eth-methods'; import ConfirmTransactionBase from './confirm-transaction-base.container'; @@ -203,6 +204,9 @@ const baseStore = { appState: { sendInputCurrencySwitched: false, }, + ramps: { + buyableChains: defaultBuyableChains, + }, }; const mockedStoreWithConfirmTxParams = ( @@ -497,6 +501,7 @@ describe('Confirm Transaction Base', () => { it('handleMMISubmit calls sendTransaction correctly and then showCustodianDeepLink', async () => { const state = { + ...baseStore, appState: { ...baseStore.appState, gasLoadingAnimationIsShowing: false, diff --git a/ui/pages/confirmations/hooks/useConfirmationAlertActions.ts b/ui/pages/confirmations/hooks/useConfirmationAlertActions.ts index fae80bc53316..b02ef8f809a6 100644 --- a/ui/pages/confirmations/hooks/useConfirmationAlertActions.ts +++ b/ui/pages/confirmations/hooks/useConfirmationAlertActions.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import { AlertActionKey } from '../../../components/app/confirm/info/row/constants'; -import useRamps from '../../../hooks/experiences/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import { useTransactionModalContext } from '../../../contexts/transaction-modal'; const useConfirmationAlertActions = () => { diff --git a/ui/pages/confirmations/hooks/useTransactionFunction.test.js b/ui/pages/confirmations/hooks/useTransactionFunction.test.js index cc4e69863892..22984ba71a37 100644 --- a/ui/pages/confirmations/hooks/useTransactionFunction.test.js +++ b/ui/pages/confirmations/hooks/useTransactionFunction.test.js @@ -22,6 +22,7 @@ useGasEstimates.mockImplementation(() => FEE_MARKET_ESTIMATE_RETURN_VALUE); jest.mock('../../../selectors', () => ({ checkNetworkAndAccountSupports1559: () => true, + getCurrentChainId: jest.fn().mockReturnValue('0x1'), })); const wrapper = ({ children }) => ( diff --git a/ui/pages/confirmations/send/gas-display/gas-display.js b/ui/pages/confirmations/send/gas-display/gas-display.js index e07c5473bdb9..5fbad8445cd6 100644 --- a/ui/pages/confirmations/send/gas-display/gas-display.js +++ b/ui/pages/confirmations/send/gas-display/gas-display.js @@ -23,7 +23,6 @@ import TransactionDetail from '../../components/transaction-detail'; import ActionableMessage from '../../../../components/ui/actionable-message'; import { getPreferences, - getIsBuyableChain, transactionFeeSelector, getIsTestnet, getUseCurrencyRateCheck, @@ -46,7 +45,8 @@ import { MetaMetricsEventName, } from '../../../../../shared/constants/metametrics'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; -import useRamps from '../../../../hooks/experiences/useRamps'; +import useRamps from '../../../../hooks/ramps/useRamps/useRamps'; +import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; export default function GasDisplay({ gasError }) { const t = useContext(I18nContext); @@ -58,7 +58,7 @@ export default function GasDisplay({ gasError }) { const providerConfig = useSelector(getProviderConfig); const isTestnet = useSelector(getIsTestnet); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const draftTransaction = useSelector(getCurrentDraftTransaction); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); const { showFiatInTestnets, useNativeCurrencyAsPrimaryCurrency } = diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 3a3dfc7ed753..cb177340501d 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -25,8 +25,8 @@ import Popover from '../../components/ui/popover'; import ConnectedSites from '../connected-sites'; import ConnectedAccounts from '../connected-accounts'; import { isMv3ButOffscreenDocIsMissing } from '../../../shared/modules/mv3.utils'; - import ActionableMessage from '../../components/ui/actionable-message/actionable-message'; + import { FontWeight, Display, @@ -220,6 +220,7 @@ export default class Home extends PureComponent { custodianDeepLink: PropTypes.object, accountType: PropTypes.string, ///: END:ONLY_INCLUDE_IF + fetchBuyableChains: PropTypes.func.isRequired, }; state = { @@ -362,6 +363,8 @@ export default class Home extends PureComponent { setWaitForConfirmDeepLinkDialog(false); }); ///: END:ONLY_INCLUDE_IF + + this.props.fetchBuyableChains(); } static getDerivedStateFromProps(props) { diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index 7b1334f80e0e..ea9c7171448a 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -85,6 +85,7 @@ import { } from '../../ducks/app/app'; import { getWeb3ShimUsageAlertEnabledness } from '../../ducks/metamask/metamask'; import { getSwapsFeatureIsLive } from '../../ducks/swaps/swaps'; +import { fetchBuyableChains } from '../../ducks/ramps'; import { getEnvironmentType } from '../../../app/scripts/lib/util'; import { getIsBrowserDeprecated } from '../../helpers/utils/util'; import { @@ -319,6 +320,7 @@ const mapDispatchToProps = (dispatch) => { ///: END:ONLY_INCLUDE_IF setBasicFunctionalityModalOpen: () => dispatch(openBasicFunctionalityModal()), + fetchBuyableChains: () => dispatch(fetchBuyableChains()), }; }; diff --git a/ui/pages/routes/routes.component.test.js b/ui/pages/routes/routes.component.test.js index 8c7d127c88e1..3024ea436e35 100644 --- a/ui/pages/routes/routes.component.test.js +++ b/ui/pages/routes/routes.component.test.js @@ -1,7 +1,7 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import { act } from '@testing-library/react'; - +import thunk from 'redux-thunk'; import { SEND_STAGES } from '../../ducks/send'; import { CONFIRMATION_V_NEXT_ROUTE, @@ -14,6 +14,8 @@ import mockState from '../../../test/data/mock-state.json'; import { useIsOriginalNativeTokenSymbol } from '../../hooks/useIsOriginalNativeTokenSymbol'; import Routes from '.'; +const middlewares = [thunk]; + const mockShowNetworkDropdown = jest.fn(); const mockHideNetworkDropdown = jest.fn(); @@ -74,7 +76,7 @@ jest.mock('../../helpers/utils/feature-flags', () => ({ })); const render = async (route, state) => { - const store = configureMockStore()({ + const store = configureMockStore(middlewares)({ ...mockSendState, ...state, }); diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index 5fb2badd8bcb..0dc3f89009cb 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -141,7 +141,7 @@ import { import { GAS_FEES_LEARN_MORE_URL } from '../../../../shared/lib/ui-utils'; import ExchangeRateDisplay from '../exchange-rate-display'; import InfoTooltip from '../../../components/ui/info-tooltip'; -import useRamps from '../../../hooks/experiences/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import ViewQuotePriceDifference from './view-quote-price-difference'; import SlippageNotificationModal from './slippage-notification-modal'; diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index f613015efb7c..cd049873dfa4 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -13,7 +13,6 @@ import { TransactionStatus } from '@metamask/transaction-controller'; import { addHexPrefix, getEnvironmentType } from '../../app/scripts/lib/util'; import { TEST_CHAINS, - BUYABLE_CHAINS_MAP, MAINNET_DISPLAY_NAME, BSC_DISPLAY_NAME, POLYGON_DISPLAY_NAME, @@ -1307,11 +1306,6 @@ export function getIsBridgeChain(state) { const chainId = getCurrentChainId(state); return ALLOWED_BRIDGE_CHAIN_IDS.includes(chainId); } - -export function getIsBuyableChain(state) { - const chainId = getCurrentChainId(state); - return Object.keys(BUYABLE_CHAINS_MAP).includes(chainId); -} export function getNativeCurrencyImage(state) { const chainId = getCurrentChainId(state); return CHAIN_ID_TOKEN_IMAGE_MAP[chainId];