From 8bda667e0077f6ae927022c91736053e033287a3 Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Mon, 15 Apr 2024 18:47:14 -0600 Subject: [PATCH 01/24] feat(ramps): introduce dynamic support for rampable networks --- builds.yml | 2 + privacy-snapshot.json | 1 + shared/constants/network.ts | 110 --------------- test/data/mock-send-state.json | 82 ++++++++++++ test/data/mock-state.json | 82 ++++++++++++ .../errors-after-init-opt-in-ui-state.json | 3 +- .../app/add-network/add-network.test.js | 1 + ui/components/app/asset-list/asset-list.js | 6 +- ui/components/app/nfts-tab/nfts-tab.js | 4 +- .../selected-account-component.test.js | 1 + .../transaction-list.component.js | 5 +- .../app/wallet-overview/eth-overview.js | 6 +- .../app/wallet-overview/eth-overview.test.js | 26 ++-- .../app/wallet-overview/token-overview.js | 6 +- .../wallet-overview/token-overview.test.js | 29 ++-- .../multichain/ramps-card/ramps-card.js | 4 +- ui/ducks/index.js | 2 + ui/ducks/ramps/constants.ts | 82 ++++++++++++ ui/ducks/ramps/index.ts | 1 + ui/ducks/ramps/ramps.test.ts | 125 ++++++++++++++++++ ui/ducks/ramps/ramps.ts | 58 ++++++++ ui/ducks/ramps/types.ts | 6 + ui/hooks/experiences/useRamps.test.js | 2 +- ui/hooks/useRamps/rampAPI.test.ts | 22 +++ ui/hooks/useRamps/rampAPI.ts | 24 ++++ ui/hooks/useRamps/useRamps.test.tsx | 109 +++++++++++++++ .../{experiences => useRamps}/useRamps.ts | 0 ui/hooks/useTheme.test.ts | 1 + .../confirm-page-container.component.js | 10 +- .../confirm-page-container.container.js | 3 - .../confirm-transaction-base.container.js | 4 +- .../confirm-transaction-base.test.js | 5 + .../hooks/useTransactionFunction.test.js | 1 + .../send/gas-display/gas-display.js | 6 +- ui/pages/confirmations/send/send.test.js | 4 + ui/pages/home/home.component.js | 5 +- ui/pages/home/home.container.js | 2 + ui/pages/routes/routes.component.test.js | 7 +- .../swaps/prepare-swap-page/review-quote.js | 2 +- ui/selectors/selectors.js | 6 - 40 files changed, 666 insertions(+), 189 deletions(-) create mode 100644 ui/ducks/ramps/constants.ts create mode 100644 ui/ducks/ramps/index.ts create mode 100644 ui/ducks/ramps/ramps.test.ts create mode 100644 ui/ducks/ramps/ramps.ts create mode 100644 ui/ducks/ramps/types.ts create mode 100644 ui/hooks/useRamps/rampAPI.test.ts create mode 100644 ui/hooks/useRamps/rampAPI.ts create mode 100644 ui/hooks/useRamps/useRamps.test.tsx rename ui/hooks/{experiences => useRamps}/useRamps.ts (100%) diff --git a/builds.yml b/builds.yml index 446ce72139ec..79f59dc4ff19 100644 --- a/builds.yml +++ b/builds.yml @@ -277,6 +277,8 @@ env: # Enables the notifications feature within the build: - NOTIFICATIONS: '' + - METAMASK_RAMP_API_BASE_URL: https://on-ramp-content.metaswap.codefi.network + ### # Meta variables ### diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 4ce6cbbe0253..1312b44bec65 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -26,6 +26,7 @@ "mainnet.infura.io", "metamask.github.io", "min-api.cryptocompare.com", + "on-ramp-content.metaswap.codefi.network", "phishing-detection.metafi.codefi.network", "portfolio.metamask.io", "price-api.metafi.codefi.network", diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 3b2908cea942..a09986188cb3 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 @@ -861,100 +845,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 - >]: 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 badc99d28341..532d2e3772c4 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -1216,6 +1216,88 @@ } ] }, + "ramps": { + "buyableChains": [ + { + "active": true, + "chainId": 1, + "chainName": "Ethereum Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 10, + "chainName": "Optimism Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 25, + "chainName": "Cronos Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 56, + "chainName": "BNB Chain Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 137, + "chainName": "Polygon Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 250, + "chainName": "Fantom Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1285, + "chainName": "Moonriver Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42161, + "chainName": "Arbitrum Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42220, + "chainName": "Celo Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 43114, + "chainName": "Avalanche C-Chain Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1313161554, + "chainName": "Aurora Mainnet", + "nativeTokenSupported": false + }, + { + "active": true, + "chainId": 1666600000, + "chainName": "Harmony Mainnet (Shard 0)", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 11297108109, + "chainName": "Palm", + "nativeTokenSupported": false + } + ] + }, "send": { "amountMode": "INPUT", "currentTransactionUUID": "1-tx", diff --git a/test/data/mock-state.json b/test/data/mock-state.json index d4d6555504f8..0b7ffce36f5f 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -1871,6 +1871,88 @@ } } }, + "ramps": { + "buyableChains": [ + { + "active": true, + "chainId": 1, + "chainName": "Ethereum Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 10, + "chainName": "Optimism Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 25, + "chainName": "Cronos Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 56, + "chainName": "BNB Chain Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 137, + "chainName": "Polygon Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 250, + "chainName": "Fantom Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1285, + "chainName": "Moonriver Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42161, + "chainName": "Arbitrum Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 42220, + "chainName": "Celo Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 43114, + "chainName": "Avalanche C-Chain Mainnet", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 1313161554, + "chainName": "Aurora Mainnet", + "nativeTokenSupported": false + }, + { + "active": true, + "chainId": 1666600000, + "chainName": "Harmony Mainnet (Shard 0)", + "nativeTokenSupported": true + }, + { + "active": true, + "chainId": 11297108109, + "chainName": "Palm", + "nativeTokenSupported": false + } + ] + }, "send": { "amountMode": "INPUT", "currentTransactionUUID": null, diff --git a/test/e2e/tests/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/state-snapshots/errors-after-init-opt-in-ui-state.json index 06928aa7da9d..007376727faa 100644 --- a/test/e2e/tests/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -75,7 +75,6 @@ "showAccountBanner": true, "trezorModel": null, "hadAdvancedGasFeesSetPriorToMigration92_3": false, - "hasDismissedOpenSeaToBlockaidBanner": false, "nftsDropdownState": {}, "termsOfUseLastAgreed": "number", "qrHardware": {}, @@ -103,6 +102,7 @@ "dismissSeedBackUpReminder": true, "disabledRpcMethodPreferences": { "eth_sign": false }, "useMultiAccountBalanceChecker": true, + "hasDismissedOpenSeaToBlockaidBanner": false, "useSafeChainsListValidation": true, "useTokenDetection": false, "useNftDetection": false, @@ -229,6 +229,7 @@ "storageMetadata": {}, "versionFileETag": "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 8e47993eac17..8bccfe42f32f 100644 --- a/ui/components/app/asset-list/asset-list.js +++ b/ui/components/app/asset-list/asset-list.js @@ -11,9 +11,6 @@ import { getDetectedTokensInCurrentNetwork, getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, getShouldHideZeroBalanceTokens, - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - getIsBuyableChain, - ///: END:ONLY_INCLUDE_IF getCurrentNetwork, getSelectedAccount, getPreferences, @@ -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 }) => { @@ -107,7 +105,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 7ee4e120510f..63ef2c6c5a3f 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 @@ -48,6 +47,7 @@ import { RampsCard, } from '../../multichain/ramps-card/ramps-card'; import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF export default function NftsTab() { @@ -68,7 +68,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 298d8dcba44b..17c722c52d2a 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; @@ -138,8 +138,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/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js index abd5351ed712..e65876f793a3 100644 --- a/ui/components/app/wallet-overview/eth-overview.js +++ b/ui/components/app/wallet-overview/eth-overview.js @@ -38,7 +38,6 @@ import { getSwapsDefaultToken, getCurrentKeyring, getIsBridgeChain, - getIsBuyableChain, getMetaMetricsId, ///: END:ONLY_INCLUDE_IF } from '../../../selectors'; @@ -61,8 +60,9 @@ import { AssetType } from '../../../../shared/constants/transaction'; import { Icon, IconName } from '../../component-library'; import { IconColor } from '../../../helpers/constants/design-system'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) -import useRamps from '../../../hooks/experiences/useRamps'; +import useRamps from '../../../hooks/useRamps/useRamps'; import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF import { useIsOriginalNativeTokenSymbol } from '../../../hooks/useIsOriginalNativeTokenSymbol'; import { getProviderConfig } from '../../../ducks/metamask/metamask'; @@ -77,7 +77,7 @@ const EthOverview = ({ className, showAddress }) => { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const location = useLocation(); const isBridgeChain = useSelector(getIsBridgeChain); - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const metaMetricsId = useSelector(getMetaMetricsId); const keyring = useSelector(getCurrentKeyring); const usingHardwareWallet = isHardwareKeyring(keyring?.type); diff --git a/ui/components/app/wallet-overview/eth-overview.test.js b/ui/components/app/wallet-overview/eth-overview.test.js index ed5612b8cf4c..732682bccc91 100644 --- a/ui/components/app/wallet-overview/eth-overview.test.js +++ b/ui/components/app/wallet-overview/eth-overview.test.js @@ -12,25 +12,9 @@ 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 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(), @@ -135,6 +119,9 @@ describe('EthOverview', () => { ], contractExchangeRates: {}, }, + ramps: { + buyableChains: defaultBuyableChains, + }, }; const store = configureMockStore([thunk])(mockStore); @@ -178,6 +165,7 @@ describe('EthOverview', () => { it('should show the cached primary balance', async () => { const mockedStoreWithCachedBalance = { + ...mockStore, metamask: { ...mockStore.metamask, accounts: { @@ -264,6 +252,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: { @@ -372,6 +361,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: { @@ -396,6 +386,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: { @@ -429,6 +420,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/app/wallet-overview/token-overview.js b/ui/components/app/wallet-overview/token-overview.js index b823dd399024..c27fa566e57c 100644 --- a/ui/components/app/wallet-overview/token-overview.js +++ b/ui/components/app/wallet-overview/token-overview.js @@ -17,7 +17,8 @@ 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/useRamps/useRamps'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -32,7 +33,6 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getIsBridgeChain, getCurrentKeyring, - getIsBuyableChain, getMetaMetricsId, ///: END:ONLY_INCLUDE_IF } from '../../../selectors'; @@ -80,7 +80,7 @@ const TokenOverview = ({ className, token }) => { 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(); diff --git a/ui/components/app/wallet-overview/token-overview.test.js b/ui/components/app/wallet-overview/token-overview.test.js index 45d70265e3fc..c4c59bbc16f4 100644 --- a/ui/components/app/wallet-overview/token-overview.test.js +++ b/ui/components/app/wallet-overview/token-overview.test.js @@ -6,24 +6,9 @@ import { EthAccountType, EthMethod } from '@metamask/keyring-api'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { renderWithProvider } from '../../../../test/jest/rendering'; import { KeyringType } from '../../../../shared/constants/keyring'; +import { defaultBuyableChains } from '../../../ducks/ramps/constants'; import TokenOverview from './token-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', - }, - }, -})); let openTabSpy; describe('TokenOverview', () => { @@ -79,6 +64,9 @@ describe('TokenOverview', () => { url: 'https://metamask-institutional.io', }, }, + ramps: { + buyableChains: defaultBuyableChains, + }, }; const store = configureMockStore([thunk])(mockStore); @@ -134,6 +122,7 @@ describe('TokenOverview', () => { it('should always show the Buy button regardless of chain Id', () => { const mockedStoreWithUnbuyableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { type: 'test', chainId: CHAIN_IDS.PALM }, @@ -167,9 +156,10 @@ describe('TokenOverview', () => { it('should have the Buy token button disabled if chain id is not part of supported buyable chains', () => { const mockedStoreWithUnbuyableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, - providerConfig: { type: 'test', chainId: CHAIN_IDS.FANTOM }, + providerConfig: { type: 'test', chainId: CHAIN_IDS.GOERLI }, }, }; const mockedStore = configureMockStore([thunk])( @@ -187,6 +177,7 @@ describe('TokenOverview', () => { it('should have the Buy token button enabled if chain id is part of supported buyable chains', () => { const mockedStoreWithBuyableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { type: 'test', chainId: CHAIN_IDS.POLYGON }, @@ -212,6 +203,7 @@ describe('TokenOverview', () => { }; const mockedStoreWithBuyableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { type: 'test', chainId: CHAIN_IDS.POLYGON }, @@ -232,6 +224,7 @@ describe('TokenOverview', () => { it('should open the buy crypto URL for a buyable chain ID', async () => { const mockedStoreWithBuyableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { type: 'test', chainId: CHAIN_IDS.POLYGON }, @@ -270,6 +263,7 @@ describe('TokenOverview', () => { }; const mockedStoreWithBridgeableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { type: 'test', chainId: CHAIN_IDS.POLYGON }, @@ -308,6 +302,7 @@ describe('TokenOverview', () => { }; const mockedStoreWithBridgeableChainId = { + ...mockStore, metamask: { ...mockStore.metamask, providerConfig: { type: 'test', chainId: CHAIN_IDS.FANTOM }, diff --git a/ui/components/multichain/ramps-card/ramps-card.js b/ui/components/multichain/ramps-card/ramps-card.js index 9b1956ac6b7d..3c783675deb0 100644 --- a/ui/components/multichain/ramps-card/ramps-card.js +++ b/ui/components/multichain/ramps-card/ramps-card.js @@ -16,9 +16,7 @@ import { MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { MetaMetricsContext } from '../../../contexts/metametrics'; -import useRamps, { - RampsMetaMaskEntry, -} from '../../../hooks/experiences/useRamps'; +import useRamps, { RampsMetaMaskEntry } from '../../../hooks/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 e976fa60e33d..f36e9bade19e 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'; export default combineReducers({ [AlertTypes.invalidCustomNetwork]: invalidCustomNetwork, @@ -24,6 +25,7 @@ export default combineReducers({ confirm: confirmReducer, 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..597730fe3ff4 --- /dev/null +++ b/ui/ducks/ramps/constants.ts @@ -0,0 +1,82 @@ +import { AggregatorNetwork } from './types'; + +export const defaultBuyableChains: AggregatorNetwork[] = [ + { + active: true, + chainId: 1, + chainName: 'Ethereum Mainnet', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 10, + chainName: 'Optimism Mainnet', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 25, + chainName: 'Cronos Mainnet', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 56, + chainName: 'BNB Chain Mainnet', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 137, + chainName: 'Polygon Mainnet', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 250, + chainName: 'Fantom Mainnet', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 1285, + chainName: 'Moonriver Mainnet', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 42161, + chainName: 'Arbitrum Mainnet', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 42220, + chainName: 'Celo Mainnet', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 43114, + chainName: 'Avalanche C-Chain Mainnet', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 1313161554, + chainName: 'Aurora Mainnet', + nativeTokenSupported: false, + }, + { + active: true, + chainId: 1666600000, + chainName: 'Harmony Mainnet (Shard 0)', + nativeTokenSupported: true, + }, + { + active: true, + chainId: 11297108109, + chainName: '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..b9724d11943f --- /dev/null +++ b/ui/ducks/ramps/ramps.test.ts @@ -0,0 +1,125 @@ +import { configureStore, Store } from '@reduxjs/toolkit'; +import RampAPI from '../../hooks/useRamps/rampAPI'; +import { getCurrentChainId } from '../../selectors'; +import { CHAIN_IDS } from '../../../shared/constants/network'; +import rampsReducer, { + fetchBuyableChains, + getBuyableChains, + getIsNativeTokenBuyable, +} from './ramps'; +import { defaultBuyableChains } from './constants'; + +jest.mock('../../hooks/useRamps/rampAPI'); + +jest.mock('../../selectors', () => ({ + getCurrentChainId: jest.fn(), + getNames: jest.fn(), +})); + +describe('rampsSlice', () => { + let store: Store; + + beforeEach(() => { + store = configureStore({ + reducer: { + ramps: rampsReducer, + }, + }); + // @ts-expect-error mocked API has mockReset method + RampAPI.getNetworks.mockReset(); + }); + + it('should set the initial state to defaultBuyableChains', () => { + const { ramps: rampsState } = store.getState(); + expect(rampsState).toEqual({ + buyableChains: defaultBuyableChains, + }); + }); + + 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); + }); + + describe('getBuyableChains', () => { + it('returns buyableChains', () => { + const state = store.getState(); + expect(getBuyableChains(state)).toBe(state.ramps.buyableChains); + }); + }); + + describe('fetchBuyableChains', () => { + it('should call RampAPI.getNetworks', async () => { + // @ts-expect-error this is a valid action + await store.dispatch(fetchBuyableChains()); + expect(RampAPI.getNetworks).toHaveBeenCalledTimes(1); + }); + + it('should update the state with the data that is returned', async () => { + const mockBuyableChains = [ + { + active: true, + chainId: 1, + chainName: 'Ethereum Mainnet', + nativeTokenSupported: true, + }, + ]; + 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); + }); + }); +}); diff --git a/ui/ducks/ramps/ramps.ts b/ui/ducks/ramps/ramps.ts new file mode 100644 index 000000000000..8b74a2d2b800 --- /dev/null +++ b/ui/ducks/ramps/ramps.ts @@ -0,0 +1,58 @@ +import { createSelector } from 'reselect'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { getCurrentChainId } from '../../selectors'; +import RampAPI from '../../hooks/useRamps/rampAPI'; +import { hexToDecimal } from '../../../shared/modules/conversion.utils'; +import { defaultBuyableChains } from './constants'; +import { AggregatorNetwork } from './types'; + +export const fetchBuyableChains = createAsyncThunk( + 'ramps/fetchBuyableChains', + async () => { + return await RampAPI.getNetworks(); + }, +); + +const rampsSlice = createSlice({ + name: 'ramps', + initialState: { + buyableChains: defaultBuyableChains, + }, + reducers: { + setBuyableChains: (state, action) => { + state.buyableChains = action.payload; + }, + }, + extraReducers: (builder) => { + builder + .addCase(fetchBuyableChains.fulfilled, (state, action) => { + const networks = action.payload; + if (networks && networks.length > 0) { + state.buyableChains = action.payload; + } 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; + +export const getIsNativeTokenBuyable = createSelector( + [getCurrentChainId, getBuyableChains], + (currentChainId, buyableChains) => { + return buyableChains.some( + (network: AggregatorNetwork) => + String(network.chainId) === hexToDecimal(currentChainId), + ); + }, +); + +export default reducer; diff --git a/ui/ducks/ramps/types.ts b/ui/ducks/ramps/types.ts new file mode 100644 index 000000000000..0d4373e20b43 --- /dev/null +++ b/ui/ducks/ramps/types.ts @@ -0,0 +1,6 @@ +export type AggregatorNetwork = { + active: boolean; + chainId: number; + chainName: string; + nativeTokenSupported: boolean; +}; diff --git a/ui/hooks/experiences/useRamps.test.js b/ui/hooks/experiences/useRamps.test.js index dcb1dc2eb43c..4f473e6c83a5 100644 --- a/ui/hooks/experiences/useRamps.test.js +++ b/ui/hooks/experiences/useRamps.test.js @@ -2,7 +2,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { renderHook } from '@testing-library/react-hooks'; import configureStore from '../../store/store'; -import useRamps, { RampsMetaMaskEntry } from './useRamps'; +import useRamps, { RampsMetaMaskEntry } from '../useRamps/useRamps'; const mockedMetametricsId = '0xtestMetaMetricsId'; diff --git a/ui/hooks/useRamps/rampAPI.test.ts b/ui/hooks/useRamps/rampAPI.test.ts new file mode 100644 index 000000000000..54d29d1f9847 --- /dev/null +++ b/ui/hooks/useRamps/rampAPI.test.ts @@ -0,0 +1,22 @@ +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.dev.mmcx.codefi.network') + .get('/regions/networks') + .query(true) + .reply(200, mockedResponse); + const result = await rampAPI.getNetworks(); + expect(result).toEqual(mockedResponse.networks); + }); +}); diff --git a/ui/hooks/useRamps/rampAPI.ts b/ui/hooks/useRamps/rampAPI.ts new file mode 100644 index 000000000000..089028515951 --- /dev/null +++ b/ui/hooks/useRamps/rampAPI.ts @@ -0,0 +1,24 @@ +import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; +import { AggregatorNetwork } from '../../ducks/ramps/types'; +import { defaultBuyableChains } from '../../ducks/ramps/constants'; + +const fetchWithTimeout = getFetchWithTimeout(); + +const rampApiBaseUrl = + process.env.METAMASK_RAMP_API_BASE_URL || + 'https://on-ramp-content.metaswap.codefi.network'; + +const RampAPI = { + async getNetworks(): Promise { + try { + const url = `${rampApiBaseUrl}/regions/networks?context=extension`; + const response = await fetchWithTimeout(url); + const { networks } = await response.json(); + return networks; + } catch (error) { + return defaultBuyableChains; + } + }, +}; + +export default RampAPI; diff --git a/ui/hooks/useRamps/useRamps.test.tsx b/ui/hooks/useRamps/useRamps.test.tsx new file mode 100644 index 000000000000..5dec2adfd2f5 --- /dev/null +++ b/ui/hooks/useRamps/useRamps.test.tsx @@ -0,0 +1,109 @@ +import React, { FC } from 'react'; +import { Provider } from 'react-redux'; +import { renderHook } from '@testing-library/react-hooks'; +import configureStore from '../../store/store'; +import useRamps, { RampsMetaMaskEntry } from './useRamps'; + +const mockedMetametricsId = '0xtestMetaMetricsId'; + +let mockStoreState = { + metamask: { + providerConfig: { + chainId: '0x1', + }, + metaMetricsId: mockedMetametricsId, + }, +}; + +const wrapper: FC = ({ children }) => ( + {children} +); + +describe('useRamps', () => { + // 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', () => { + const metaMaskEntry = 'ext_buy_sell_button'; + const mockChainId = '0x1'; + + mockStoreState = { + ...mockStoreState, + metamask: { + ...mockStoreState.metamask, + providerConfig: { + chainId: mockChainId, + }, + }, + }; + + const mockBuyURI = `${process.env.PORTFOLIO_URL}/buy?metamaskEntry=${metaMaskEntry}&chainId=${mockChainId}&metametricsId=${mockedMetametricsId}`; + const openTabSpy = jest.spyOn(global.platform, 'openTab'); + + const { result } = renderHook(() => useRamps(), { wrapper }); // default metamask entry + + result.current.openBuyCryptoInPdapp(); + expect(openTabSpy).toHaveBeenCalledWith({ + url: mockBuyURI, + }); + }); + + it('should use the correct metamask entry param when opening the buy crypto URL', () => { + const metaMaskEntry = 'ext_buy_banner_tokens'; + const mockChainId = '0x1'; + + mockStoreState = { + ...mockStoreState, + metamask: { + ...mockStoreState.metamask, + providerConfig: { + chainId: mockChainId, + }, + }, + }; + + const mockBuyURI = `${process.env.PORTFOLIO_URL}/buy?metamaskEntry=${metaMaskEntry}&chainId=${mockChainId}&metametricsId=${mockedMetametricsId}`; + const openTabSpy = jest.spyOn(global.platform, 'openTab'); + + const { result } = renderHook( + () => useRamps(RampsMetaMaskEntry.TokensBanner), + { wrapper }, + ); + + result.current.openBuyCryptoInPdapp(); + expect(openTabSpy).toHaveBeenCalledWith({ + url: mockBuyURI, + }); + }); + + it.each(['0x1', '0x38', '0xa'])( + 'should open the buy crypto URL with the currently connected chain ID', + (mockChainId) => { + mockStoreState = { + ...mockStoreState, + metamask: { + ...mockStoreState.metamask, + providerConfig: { + chainId: mockChainId, + }, + }, + }; + + const mockBuyURI = `${process.env.PORTFOLIO_URL}/buy?metamaskEntry=ext_buy_sell_button&chainId=${mockChainId}&metametricsId=${mockedMetametricsId}`; + const openTabSpy = jest.spyOn(global.platform, 'openTab'); + const { result } = renderHook(() => useRamps(), { wrapper }); + + result.current.openBuyCryptoInPdapp(); + + expect(openTabSpy).toHaveBeenCalledWith({ + url: mockBuyURI, + }); + }, + ); +}); diff --git a/ui/hooks/experiences/useRamps.ts b/ui/hooks/useRamps/useRamps.ts similarity index 100% rename from ui/hooks/experiences/useRamps.ts rename to ui/hooks/useRamps/useRamps.ts 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/confirmations/components/confirm-page-container/confirm-page-container.component.js b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js index 82fd617131b7..9450e1622dba 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 @@ -45,12 +45,11 @@ import { getAccountName, getAddressBookEntry, getInternalAccounts, - getIsBuyableChain, getMetadataContractName, getNetworkIdentifier, getSwapsDefaultToken, } from '../../../../selectors'; -import useRamps from '../../../../hooks/experiences/useRamps'; +import useRamps from '../../../../hooks/useRamps/useRamps'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { @@ -60,6 +59,7 @@ import { ///: END:ONLY_INCLUDE_IF import { BlockaidResultType } from '../../../../../shared/constants/security-provider'; +import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; import { ConfirmPageContainerHeader, ConfirmPageContainerContent, @@ -122,7 +122,7 @@ const ConfirmPageContainer = (props) => { const [isShowingTxInsightWarnings, setIsShowingTxInsightWarnings] = useState(false); ///: END:ONLY_INCLUDE_IF - const isBuyableChain = useSelector(getIsBuyableChain); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); const contact = useSelector((state) => getAddressBookEntry(state, toAddress)); const networkIdentifier = useSelector(getNetworkIdentifier); const defaultToken = useSelector(getSwapsDefaultToken); @@ -135,10 +135,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 d0c4cf629073..9c9d5a18c574 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 @@ -47,7 +47,6 @@ import { doesAddressRequireLedgerHidConnection, getTokenList, getIsMultiLayerFeeNetwork, - getIsBuyableChain, getEnsResolutionByAddress, getUnapprovedTransaction, getFullTxData, @@ -111,6 +110,7 @@ import { showCustodyConfirmLink } from '../../../store/institutional/institution import { getTokenAddressParam } from '../../../helpers/utils/token-util'; 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 = ''; @@ -146,7 +146,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 d399bcad2073..fe702474add3 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 ConfirmTransactionBase from './confirm-transaction-base.container'; jest.mock('../components/simulation-details/useSimulationMetrics'); @@ -210,6 +211,9 @@ const baseStore = { appState: { sendInputCurrencySwitched: false, }, + ramps: { + buyableChains: defaultBuyableChains, + }, }; const mockedStoreWithConfirmTxParams = ( @@ -572,6 +576,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/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 fad12aecbe79..53baa2eeb75f 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/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/confirmations/send/send.test.js b/ui/pages/confirmations/send/send.test.js index 195038ce8091..1f5af441e502 100644 --- a/ui/pages/confirmations/send/send.test.js +++ b/ui/pages/confirmations/send/send.test.js @@ -16,6 +16,7 @@ import { renderWithProvider } from '../../../../test/jest'; import { GasEstimateTypes } from '../../../../shared/constants/gas'; import { KeyringType } from '../../../../shared/constants/keyring'; import { INITIAL_SEND_STATE_FOR_EXISTING_DRAFT } from '../../../../test/jest/mocks'; +import { defaultBuyableChains } from '../../../ducks/ramps/constants'; import Send from './send'; const middleware = [thunk]; @@ -159,6 +160,9 @@ const baseStore = { appState: { sendInputCurrencySwitched: false, }, + ramps: { + buyableChains: defaultBuyableChains, + }, }; const placeholderText = 'Enter public address (0x) or ENS name'; diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 856242d0bded..51b5f2d73116 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -26,8 +26,8 @@ import ConnectedSites from '../connected-sites'; import ConnectedAccounts from '../connected-accounts'; import { Tabs, Tab } from '../../components/ui/tabs'; import { EthOverview } from '../../components/app/wallet-overview'; - import ActionableMessage from '../../components/ui/actionable-message/actionable-message'; + import { FontWeight, Display, @@ -208,6 +208,7 @@ export default class Home extends PureComponent { custodianDeepLink: PropTypes.object, accountType: PropTypes.string, ///: END:ONLY_INCLUDE_IF + fetchBuyableChains: PropTypes.func.isRequired, }; state = { @@ -347,6 +348,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 f34b93b5f16e..977748bd6b9b 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -72,6 +72,7 @@ import { import { hideWhatsNewPopup } 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 { @@ -274,6 +275,7 @@ const mapDispatchToProps = (dispatch) => { ///: END:ONLY_INCLUDE_IF setSurveyLinkLastClickedOrClosed: (time) => dispatch(setSurveyLinkLastClickedOrClosed(time)), + fetchBuyableChains: () => dispatch(fetchBuyableChains()), }; }; diff --git a/ui/pages/routes/routes.component.test.js b/ui/pages/routes/routes.component.test.js index e684fedc32cb..72c8b0c2f9e9 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, fireEvent } from '@testing-library/react'; - +import thunk from 'redux-thunk'; import { SEND_STAGES } from '../../ducks/send'; import { renderWithProvider } from '../../../test/jest'; import mockSendState from '../../../test/data/mock-send-state.json'; @@ -13,6 +13,8 @@ import { import { useIsOriginalNativeTokenSymbol } from '../../hooks/useIsOriginalNativeTokenSymbol'; import Routes from '.'; +const middlewares = [thunk]; + const mockShowNetworkDropdown = jest.fn(); const mockHideNetworkDropdown = jest.fn(); @@ -27,6 +29,7 @@ jest.mock('webextension-polyfill', () => ({ })); jest.mock('../../store/actions', () => ({ + ...jest.requireActual('../../store/actions'), getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()), gasFeeStartPollingByNetworkClientId: jest .fn() @@ -68,7 +71,7 @@ jest.mock( ); 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 b1d68921358f..99273dcfaba6 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -139,7 +139,7 @@ import { addHexPrefix } from '../../../../app/scripts/lib/util'; import { calcTokenValue } from '../../../../shared/lib/swaps-utils'; import ExchangeRateDisplay from '../exchange-rate-display'; import InfoTooltip from '../../../components/ui/info-tooltip'; -import useRamps from '../../../hooks/experiences/useRamps'; +import useRamps from '../../../hooks/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 ff0b686dec0d..de74ccdb92bb 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -16,7 +16,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, @@ -1276,11 +1275,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]; From 8fecf66cba4953aaee5a7c543ae72478a3fbba34 Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Wed, 17 Apr 2024 09:08:59 -0600 Subject: [PATCH 02/24] refactor(ramps): updates file directory to organize ramps hooks and utils --- .../app/wallet-overview/eth-overview.js | 2 +- .../app/wallet-overview/token-overview.js | 2 +- .../multichain/ramps-card/ramps-card.js | 4 +- ui/ducks/ramps/ramps.test.ts | 4 +- ui/ducks/ramps/ramps.ts | 2 +- .../ramps/rampApi}/rampAPI.test.ts | 2 +- .../ramps/rampApi}/rampAPI.ts | 6 +- ui/hooks/experiences/useRamps.test.js | 108 ------------------ .../{ => ramps}/useRamps/useRamps.test.tsx | 2 +- ui/hooks/{ => ramps}/useRamps/useRamps.ts | 4 +- .../confirm-page-container.component.js | 2 +- .../send/gas-display/gas-display.js | 2 +- .../swaps/prepare-swap-page/review-quote.js | 2 +- 13 files changed, 18 insertions(+), 124 deletions(-) rename ui/{hooks/useRamps => helpers/ramps/rampApi}/rampAPI.test.ts (87%) rename ui/{hooks/useRamps => helpers/ramps/rampApi}/rampAPI.ts (71%) delete mode 100644 ui/hooks/experiences/useRamps.test.js rename ui/hooks/{ => ramps}/useRamps/useRamps.test.tsx (98%) rename ui/hooks/{ => ramps}/useRamps/useRamps.ts (90%) diff --git a/ui/components/app/wallet-overview/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js index e65876f793a3..28eb73ffce79 100644 --- a/ui/components/app/wallet-overview/eth-overview.js +++ b/ui/components/app/wallet-overview/eth-overview.js @@ -60,7 +60,7 @@ import { AssetType } from '../../../../shared/constants/transaction'; import { Icon, IconName } from '../../component-library'; import { IconColor } from '../../../helpers/constants/design-system'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) -import useRamps from '../../../hooks/useRamps/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF diff --git a/ui/components/app/wallet-overview/token-overview.js b/ui/components/app/wallet-overview/token-overview.js index c27fa566e57c..4dde273ea185 100644 --- a/ui/components/app/wallet-overview/token-overview.js +++ b/ui/components/app/wallet-overview/token-overview.js @@ -17,7 +17,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/useRamps/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; ///: END:ONLY_INCLUDE_IF diff --git a/ui/components/multichain/ramps-card/ramps-card.js b/ui/components/multichain/ramps-card/ramps-card.js index 3c783675deb0..e2dc519c40b7 100644 --- a/ui/components/multichain/ramps-card/ramps-card.js +++ b/ui/components/multichain/ramps-card/ramps-card.js @@ -16,7 +16,9 @@ import { MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { MetaMetricsContext } from '../../../contexts/metametrics'; -import useRamps, { RampsMetaMaskEntry } from '../../../hooks/useRamps/useRamps'; +import useRamps, { + RampsMetaMaskEntry, +} from '../../../hooks/ramps/useRamps/useRamps'; import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; import { getCurrentLocale } from '../../../ducks/locale/locale'; diff --git a/ui/ducks/ramps/ramps.test.ts b/ui/ducks/ramps/ramps.test.ts index b9724d11943f..ae48128eef9a 100644 --- a/ui/ducks/ramps/ramps.test.ts +++ b/ui/ducks/ramps/ramps.test.ts @@ -1,5 +1,5 @@ import { configureStore, Store } from '@reduxjs/toolkit'; -import RampAPI from '../../hooks/useRamps/rampAPI'; +import RampAPI from '../../helpers/ramps/rampApi/rampAPI'; import { getCurrentChainId } from '../../selectors'; import { CHAIN_IDS } from '../../../shared/constants/network'; import rampsReducer, { @@ -9,7 +9,7 @@ import rampsReducer, { } from './ramps'; import { defaultBuyableChains } from './constants'; -jest.mock('../../hooks/useRamps/rampAPI'); +jest.mock('../../helpers/ramps/rampApi/rampAPI'); jest.mock('../../selectors', () => ({ getCurrentChainId: jest.fn(), diff --git a/ui/ducks/ramps/ramps.ts b/ui/ducks/ramps/ramps.ts index 8b74a2d2b800..ed90eb9f481f 100644 --- a/ui/ducks/ramps/ramps.ts +++ b/ui/ducks/ramps/ramps.ts @@ -1,7 +1,7 @@ import { createSelector } from 'reselect'; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { getCurrentChainId } from '../../selectors'; -import RampAPI from '../../hooks/useRamps/rampAPI'; +import RampAPI from '../../helpers/ramps/rampApi/rampAPI'; import { hexToDecimal } from '../../../shared/modules/conversion.utils'; import { defaultBuyableChains } from './constants'; import { AggregatorNetwork } from './types'; diff --git a/ui/hooks/useRamps/rampAPI.test.ts b/ui/helpers/ramps/rampApi/rampAPI.test.ts similarity index 87% rename from ui/hooks/useRamps/rampAPI.test.ts rename to ui/helpers/ramps/rampApi/rampAPI.test.ts index 54d29d1f9847..97532fd08f4e 100644 --- a/ui/hooks/useRamps/rampAPI.test.ts +++ b/ui/helpers/ramps/rampApi/rampAPI.test.ts @@ -1,5 +1,5 @@ import nock from 'nock'; -import { defaultBuyableChains } from '../../ducks/ramps/constants'; +import { defaultBuyableChains } from '../../../ducks/ramps/constants'; import rampAPI from './rampAPI'; const mockedResponse = { diff --git a/ui/hooks/useRamps/rampAPI.ts b/ui/helpers/ramps/rampApi/rampAPI.ts similarity index 71% rename from ui/hooks/useRamps/rampAPI.ts rename to ui/helpers/ramps/rampApi/rampAPI.ts index 089028515951..ddc53b41341d 100644 --- a/ui/hooks/useRamps/rampAPI.ts +++ b/ui/helpers/ramps/rampApi/rampAPI.ts @@ -1,6 +1,6 @@ -import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; -import { AggregatorNetwork } from '../../ducks/ramps/types'; -import { defaultBuyableChains } from '../../ducks/ramps/constants'; +import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout'; +import { AggregatorNetwork } from '../../../ducks/ramps/types'; +import { defaultBuyableChains } from '../../../ducks/ramps/constants'; const fetchWithTimeout = getFetchWithTimeout(); diff --git a/ui/hooks/experiences/useRamps.test.js b/ui/hooks/experiences/useRamps.test.js deleted file mode 100644 index 4f473e6c83a5..000000000000 --- a/ui/hooks/experiences/useRamps.test.js +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import { Provider } from 'react-redux'; -import { renderHook } from '@testing-library/react-hooks'; -import configureStore from '../../store/store'; -import useRamps, { RampsMetaMaskEntry } from '../useRamps/useRamps'; - -const mockedMetametricsId = '0xtestMetaMetricsId'; - -let mockStoreState = { - metamask: { - providerConfig: { - chainId: '0x1', - }, - metaMetricsId: mockedMetametricsId, - }, -}; - -const wrapper = ({ children }) => ( - {children} -); - -describe('useRamps', () => { - beforeEach(() => { - global.platform = { openTab: jest.fn() }; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should default the metamask entry param when opening the buy crypto URL', () => { - const metaMaskEntry = 'ext_buy_sell_button'; - const mockChainId = '0x1'; - - mockStoreState = { - ...mockStoreState, - metamask: { - ...mockStoreState.metamask, - providerConfig: { - chainId: mockChainId, - }, - }, - }; - - const mockBuyURI = `${process.env.PORTFOLIO_URL}/buy?metamaskEntry=${metaMaskEntry}&chainId=${mockChainId}&metametricsId=${mockedMetametricsId}`; - const openTabSpy = jest.spyOn(global.platform, 'openTab'); - - const { result } = renderHook(() => useRamps(), { wrapper }); // default metamask entry - - result.current.openBuyCryptoInPdapp(); - expect(openTabSpy).toHaveBeenCalledWith({ - url: mockBuyURI, - }); - }); - - it('should use the correct metamask entry param when opening the buy crypto URL', () => { - const metaMaskEntry = 'ext_buy_banner_tokens'; - const mockChainId = '0x1'; - - mockStoreState = { - ...mockStoreState, - metamask: { - ...mockStoreState.metamask, - providerConfig: { - chainId: mockChainId, - }, - }, - }; - - const mockBuyURI = `${process.env.PORTFOLIO_URL}/buy?metamaskEntry=${metaMaskEntry}&chainId=${mockChainId}&metametricsId=${mockedMetametricsId}`; - const openTabSpy = jest.spyOn(global.platform, 'openTab'); - - const { result } = renderHook( - () => useRamps(RampsMetaMaskEntry.TokensBanner), - { wrapper }, - ); - - result.current.openBuyCryptoInPdapp(); - expect(openTabSpy).toHaveBeenCalledWith({ - url: mockBuyURI, - }); - }); - - it.each(['0x1', '0x38', '0xa'])( - 'should open the buy crypto URL with the currently connected chain ID', - (mockChainId) => { - mockStoreState = { - ...mockStoreState, - metamask: { - ...mockStoreState.metamask, - providerConfig: { - chainId: mockChainId, - }, - }, - }; - - const mockBuyURI = `${process.env.PORTFOLIO_URL}/buy?metamaskEntry=ext_buy_sell_button&chainId=${mockChainId}&metametricsId=${mockedMetametricsId}`; - const openTabSpy = jest.spyOn(global.platform, 'openTab'); - const { result } = renderHook(() => useRamps(), { wrapper }); - - result.current.openBuyCryptoInPdapp(); - - expect(openTabSpy).toHaveBeenCalledWith({ - url: mockBuyURI, - }); - }, - ); -}); diff --git a/ui/hooks/useRamps/useRamps.test.tsx b/ui/hooks/ramps/useRamps/useRamps.test.tsx similarity index 98% rename from ui/hooks/useRamps/useRamps.test.tsx rename to ui/hooks/ramps/useRamps/useRamps.test.tsx index 5dec2adfd2f5..fac84c4e9b9f 100644 --- a/ui/hooks/useRamps/useRamps.test.tsx +++ b/ui/hooks/ramps/useRamps/useRamps.test.tsx @@ -1,7 +1,7 @@ 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'; diff --git a/ui/hooks/useRamps/useRamps.ts b/ui/hooks/ramps/useRamps/useRamps.ts similarity index 90% rename from ui/hooks/useRamps/useRamps.ts rename to ui/hooks/ramps/useRamps/useRamps.ts index 76bcdfe47879..7219d5fe4193 100644 --- a/ui/hooks/useRamps/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/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 9450e1622dba..fda0bbe181ba 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 @@ -49,7 +49,7 @@ import { getNetworkIdentifier, getSwapsDefaultToken, } from '../../../../selectors'; -import useRamps from '../../../../hooks/useRamps/useRamps'; +import useRamps from '../../../../hooks/ramps/useRamps/useRamps'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { diff --git a/ui/pages/confirmations/send/gas-display/gas-display.js b/ui/pages/confirmations/send/gas-display/gas-display.js index 53baa2eeb75f..259c941d70c5 100644 --- a/ui/pages/confirmations/send/gas-display/gas-display.js +++ b/ui/pages/confirmations/send/gas-display/gas-display.js @@ -45,7 +45,7 @@ import { MetaMetricsEventName, } from '../../../../../shared/constants/metametrics'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; -import useRamps from '../../../../hooks/useRamps/useRamps'; +import useRamps from '../../../../hooks/ramps/useRamps/useRamps'; import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; export default function GasDisplay({ gasError }) { diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index 99273dcfaba6..d661a7aeb002 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -139,7 +139,7 @@ import { addHexPrefix } from '../../../../app/scripts/lib/util'; import { calcTokenValue } from '../../../../shared/lib/swaps-utils'; import ExchangeRateDisplay from '../exchange-rate-display'; import InfoTooltip from '../../../components/ui/info-tooltip'; -import useRamps from '../../../hooks/useRamps/useRamps'; +import useRamps from '../../../hooks/ramps/useRamps/useRamps'; import ViewQuotePriceDifference from './view-quote-price-difference'; import SlippageNotificationModal from './slippage-notification-modal'; From 988e98509c129d77fb4127cbed2f94b974ee4718 Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Thu, 18 Apr 2024 13:15:40 -0600 Subject: [PATCH 03/24] chore(ramps): cleans up some code based on review comments --- builds.yml | 2 +- test/data/mock-state.json | 59 ++++++++++++++++++++++++++++- ui/ducks/ramps/ramps.ts | 2 +- ui/ducks/ramps/types.ts | 1 + ui/helpers/ramps/rampApi/rampAPI.ts | 19 ++++------ 5 files changed, 68 insertions(+), 15 deletions(-) diff --git a/builds.yml b/builds.yml index 4b7b28eaf549..d71be3883907 100644 --- a/builds.yml +++ b/builds.yml @@ -278,7 +278,7 @@ env: # Enables the notifications feature within the build: - NOTIFICATIONS: '' - - METAMASK_RAMP_API_BASE_URL: https://on-ramp-content.metaswap.codefi.network + - METAMASK_RAMP_API_CONTENT_BASE_URL: https://on-ramp-content.api.cx.metamask.io ### # Meta variables diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 0b7ffce36f5f..9c897861971b 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -1877,78 +1877,133 @@ "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", - "nativeTokenSupported": true + "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", + "chainName": "Palm Mainnet", + "shortName": "Palm", "nativeTokenSupported": false } ] diff --git a/ui/ducks/ramps/ramps.ts b/ui/ducks/ramps/ramps.ts index ed90eb9f481f..e3581ece8464 100644 --- a/ui/ducks/ramps/ramps.ts +++ b/ui/ducks/ramps/ramps.ts @@ -28,7 +28,7 @@ const rampsSlice = createSlice({ .addCase(fetchBuyableChains.fulfilled, (state, action) => { const networks = action.payload; if (networks && networks.length > 0) { - state.buyableChains = action.payload; + state.buyableChains = networks; } else { state.buyableChains = defaultBuyableChains; } diff --git a/ui/ducks/ramps/types.ts b/ui/ducks/ramps/types.ts index 0d4373e20b43..6a1571715dfe 100644 --- a/ui/ducks/ramps/types.ts +++ b/ui/ducks/ramps/types.ts @@ -3,4 +3,5 @@ export type AggregatorNetwork = { chainId: number; chainName: string; nativeTokenSupported: boolean; + shortName: string; }; diff --git a/ui/helpers/ramps/rampApi/rampAPI.ts b/ui/helpers/ramps/rampApi/rampAPI.ts index ddc53b41341d..48d1768fb3bc 100644 --- a/ui/helpers/ramps/rampApi/rampAPI.ts +++ b/ui/helpers/ramps/rampApi/rampAPI.ts @@ -1,23 +1,20 @@ import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout'; import { AggregatorNetwork } from '../../../ducks/ramps/types'; -import { defaultBuyableChains } from '../../../ducks/ramps/constants'; const fetchWithTimeout = getFetchWithTimeout(); const rampApiBaseUrl = - process.env.METAMASK_RAMP_API_BASE_URL || - 'https://on-ramp-content.metaswap.codefi.network'; + process.env.METAMASK_RAMP_API_CONTENT_BASE_URL || + 'https://on-ramp-content.api.cx.metamask.io'; const RampAPI = { async getNetworks(): Promise { - try { - const url = `${rampApiBaseUrl}/regions/networks?context=extension`; - const response = await fetchWithTimeout(url); - const { networks } = await response.json(); - return networks; - } catch (error) { - return defaultBuyableChains; - } + const url = new URL('/regions/networks', rampApiBaseUrl); + url.searchParams.append('context', 'extension'); + const response = await fetchWithTimeout(url.toString()); + + const { networks } = await response.json(); + return networks; }, }; From 4e18dbfc262280289f6c1793faaa12299e72023a Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Thu, 18 Apr 2024 13:17:35 -0600 Subject: [PATCH 04/24] chore(ramps): updates mock-send-state to use up to date list of buyable chains --- test/data/mock-send-state.json | 59 ++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index 532d2e3772c4..e2f314ef48a9 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -1222,78 +1222,133 @@ "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", - "nativeTokenSupported": true + "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", + "chainName": "Palm Mainnet", + "shortName": "Palm", "nativeTokenSupported": false } ] From 7048723a4b1a9670296dd79ae8e6121dbb937d54 Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Thu, 18 Apr 2024 13:28:20 -0600 Subject: [PATCH 05/24] chore: fixes ramp api test --- ui/helpers/ramps/rampApi/rampAPI.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/helpers/ramps/rampApi/rampAPI.test.ts b/ui/helpers/ramps/rampApi/rampAPI.test.ts index 97532fd08f4e..72dd57a0a371 100644 --- a/ui/helpers/ramps/rampApi/rampAPI.test.ts +++ b/ui/helpers/ramps/rampApi/rampAPI.test.ts @@ -12,10 +12,11 @@ describe('rampAPI', () => { }); it('should fetch networks', async () => { - nock('https://on-ramp.dev.mmcx.codefi.network') + nock('https://on-ramp-content.api.cx.metamask.io') .get('/regions/networks') .query(true) .reply(200, mockedResponse); + const result = await rampAPI.getNetworks(); expect(result).toEqual(mockedResponse.networks); }); From 69fb2f83ddca55a1cc6e0cd1cff3edba9a4c9089 Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Fri, 19 Apr 2024 12:19:10 -0600 Subject: [PATCH 06/24] chore: fixes network interface issues --- ui/ducks/ramps/constants.ts | 61 ++++++++++++++++++++++++++++++++++-- ui/ducks/ramps/ramps.test.ts | 1 + 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/ui/ducks/ramps/constants.ts b/ui/ducks/ramps/constants.ts index 597730fe3ff4..caa475c1c578 100644 --- a/ui/ducks/ramps/constants.ts +++ b/ui/ducks/ramps/constants.ts @@ -5,78 +5,133 @@ 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', - nativeTokenSupported: true, + 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', + chainName: 'Palm Mainnet', + shortName: 'Palm', nativeTokenSupported: false, }, ]; diff --git a/ui/ducks/ramps/ramps.test.ts b/ui/ducks/ramps/ramps.test.ts index ae48128eef9a..c769677cd847 100644 --- a/ui/ducks/ramps/ramps.test.ts +++ b/ui/ducks/ramps/ramps.test.ts @@ -67,6 +67,7 @@ describe('rampsSlice', () => { chainId: 1, chainName: 'Ethereum Mainnet', nativeTokenSupported: true, + shortName: 'Ethereum', }, ]; jest.spyOn(RampAPI, 'getNetworks').mockResolvedValue(mockBuyableChains); From 1d29388e61fcc9e1a918b88cf2345cc7374413a6 Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Fri, 19 Apr 2024 12:55:36 -0600 Subject: [PATCH 07/24] chore: fix lint issue --- ui/ducks/ramps/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/ducks/ramps/constants.ts b/ui/ducks/ramps/constants.ts index caa475c1c578..7a451658807e 100644 --- a/ui/ducks/ramps/constants.ts +++ b/ui/ducks/ramps/constants.ts @@ -22,7 +22,7 @@ export const defaultBuyableChains: AggregatorNetwork[] = [ shortName: 'Cronos', nativeTokenSupported: true, }, - + { active: true, chainId: 56, chainName: 'BNB Chain Mainnet', From 2d84d11078d8451db375f77c022d6406da8e9e9a Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Fri, 19 Apr 2024 13:17:05 -0600 Subject: [PATCH 08/24] chore: updates privacy snapshot for e2e tests --- privacy-snapshot.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 1312b44bec65..1211355c829e 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -26,7 +26,7 @@ "mainnet.infura.io", "metamask.github.io", "min-api.cryptocompare.com", - "on-ramp-content.metaswap.codefi.network", + "on-ramp-content.api.cx.metamask.io", "phishing-detection.metafi.codefi.network", "portfolio.metamask.io", "price-api.metafi.codefi.network", From 707f3870587a55d9a996e280c37b9974eab04a2a Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Mon, 22 Apr 2024 08:24:05 -0600 Subject: [PATCH 09/24] chore: use set instead of append for url params --- ui/helpers/ramps/rampApi/rampAPI.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/helpers/ramps/rampApi/rampAPI.ts b/ui/helpers/ramps/rampApi/rampAPI.ts index 48d1768fb3bc..591049483b29 100644 --- a/ui/helpers/ramps/rampApi/rampAPI.ts +++ b/ui/helpers/ramps/rampApi/rampAPI.ts @@ -10,7 +10,7 @@ const rampApiBaseUrl = const RampAPI = { async getNetworks(): Promise { const url = new URL('/regions/networks', rampApiBaseUrl); - url.searchParams.append('context', 'extension'); + url.searchParams.set('context', 'extension'); const response = await fetchWithTimeout(url.toString()); const { networks } = await response.json(); From 04b5c8f873fd8f48f92c93d0c4d499aa6f76a361 Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Mon, 22 Apr 2024 11:20:18 -0600 Subject: [PATCH 10/24] feat(ramps): adds more defensible code to protect against corrupted ramps state --- ui/ducks/ramps/ramps.test.ts | 58 +++++++++++++++++++++++++---- ui/ducks/ramps/ramps.ts | 27 +++++++++++--- ui/helpers/ramps/rampApi/rampAPI.ts | 6 ++- 3 files changed, 77 insertions(+), 14 deletions(-) diff --git a/ui/ducks/ramps/ramps.test.ts b/ui/ducks/ramps/ramps.test.ts index c769677cd847..f2e29e67938d 100644 --- a/ui/ducks/ramps/ramps.test.ts +++ b/ui/ducks/ramps/ramps.test.ts @@ -36,14 +36,42 @@ describe('rampsSlice', () => { }); }); - it('should update the buyableChains state when setBuyableChains is dispatched', () => { - const mockBuyableChains = [{ chainId: '0x1' }]; - store.dispatch({ - type: 'ramps/setBuyableChains', - payload: mockBuyableChains, + 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); }); - const { ramps: rampsState } = store.getState(); - expect(rampsState.buyableChains).toEqual(mockBuyableChains); }); describe('getBuyableChains', () => { @@ -122,5 +150,21 @@ describe('rampsSlice', () => { 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 index e3581ece8464..d43993260da1 100644 --- a/ui/ducks/ramps/ramps.ts +++ b/ui/ducks/ramps/ramps.ts @@ -20,7 +20,15 @@ const rampsSlice = createSlice({ }, reducers: { setBuyableChains: (state, action) => { - state.buyableChains = action.payload; + 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) => { @@ -43,15 +51,22 @@ 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; +export const getBuyableChains = (state: any) => + state.ramps?.buyableChains ?? defaultBuyableChains; export const getIsNativeTokenBuyable = createSelector( [getCurrentChainId, getBuyableChains], (currentChainId, buyableChains) => { - return buyableChains.some( - (network: AggregatorNetwork) => - String(network.chainId) === hexToDecimal(currentChainId), - ); + try { + return buyableChains + .filter(Boolean) + .some( + (network: AggregatorNetwork) => + String(network.chainId) === hexToDecimal(currentChainId), + ); + } catch (e) { + return false; + } }, ); diff --git a/ui/helpers/ramps/rampApi/rampAPI.ts b/ui/helpers/ramps/rampApi/rampAPI.ts index 591049483b29..b2bbe92e6012 100644 --- a/ui/helpers/ramps/rampApi/rampAPI.ts +++ b/ui/helpers/ramps/rampApi/rampAPI.ts @@ -3,9 +3,13 @@ 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 || - 'https://on-ramp-content.api.cx.metamask.io'; + (isProdEnv ? PROD_RAMP_API_BASE_URL : UAT_RAMP_API_BASE_URL); const RampAPI = { async getNetworks(): Promise { From c1d8b09c780998c942a3995ea764dc55155ef1a9 Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Mon, 22 Apr 2024 11:44:08 -0600 Subject: [PATCH 11/24] chore: add https://on-ramp-content.uat-api.cx.metamask.io/ to privacy snapshot --- privacy-snapshot.json | 1 + 1 file changed, 1 insertion(+) diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 1211355c829e..332cca15f36f 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -27,6 +27,7 @@ "metamask.github.io", "min-api.cryptocompare.com", "on-ramp-content.api.cx.metamask.io", + "on-ramp-content.uat-api.cx.metamask.io", "phishing-detection.metafi.codefi.network", "portfolio.metamask.io", "price-api.metafi.codefi.network", From 0091f86b0e3e12dc5f16a47bc4647ebe2cc1d8a4 Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Mon, 22 Apr 2024 12:10:01 -0600 Subject: [PATCH 12/24] chore: fixes ramps api test --- ui/helpers/ramps/rampApi/rampAPI.test.ts | 4 ++-- ui/helpers/ramps/rampApi/rampAPI.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/helpers/ramps/rampApi/rampAPI.test.ts b/ui/helpers/ramps/rampApi/rampAPI.test.ts index 72dd57a0a371..bf6e2297481d 100644 --- a/ui/helpers/ramps/rampApi/rampAPI.test.ts +++ b/ui/helpers/ramps/rampApi/rampAPI.test.ts @@ -12,12 +12,12 @@ describe('rampAPI', () => { }); it('should fetch networks', async () => { - nock('https://on-ramp-content.api.cx.metamask.io') + 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).toEqual(mockedResponse.networks); + expect(result).toStrictEqual(mockedResponse.networks); }); }); diff --git a/ui/helpers/ramps/rampApi/rampAPI.ts b/ui/helpers/ramps/rampApi/rampAPI.ts index b2bbe92e6012..a1da6da8ef0c 100644 --- a/ui/helpers/ramps/rampApi/rampAPI.ts +++ b/ui/helpers/ramps/rampApi/rampAPI.ts @@ -5,7 +5,7 @@ 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 UAT_RAMP_API_BASE_URL = 'https://on-ramp-content.uat-api.cx.metamask.io'; const rampApiBaseUrl = process.env.METAMASK_RAMP_API_CONTENT_BASE_URL || From c193631822dc2ed6a83c1eb6183fc21bdcbf865f Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Thu, 9 May 2024 21:37:43 -0600 Subject: [PATCH 13/24] chore: adds ramp api to privact snapshot --- privacy-snapshot.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/privacy-snapshot.json b/privacy-snapshot.json index beabfddc3e95..147dba581eb0 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -30,6 +30,8 @@ "phishing-detection.metafi.codefi.network", "portfolio.metamask.io", "price-api.metafi.codefi.network", + "on-ramp-content.api.cx.metamask.io", + "on-ramp-content.uat-api.cx.metamask.io", "proxy.metafi.codefi.network", "raw.githubusercontent.com", "registry.npmjs.org", From ca784bf33ca327d351abe8c080c955d3c8e8a6b1 Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Fri, 17 May 2024 10:18:10 -0600 Subject: [PATCH 14/24] chore: removes require-actual from routes component test --- ui/pages/routes/routes.component.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/pages/routes/routes.component.test.js b/ui/pages/routes/routes.component.test.js index 72c8b0c2f9e9..1f0aa06c04b8 100644 --- a/ui/pages/routes/routes.component.test.js +++ b/ui/pages/routes/routes.component.test.js @@ -29,7 +29,6 @@ jest.mock('webextension-polyfill', () => ({ })); jest.mock('../../store/actions', () => ({ - ...jest.requireActual('../../store/actions'), getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()), gasFeeStartPollingByNetworkClientId: jest .fn() From 52088d3e72a6af6c1c3f7829bb7ade2d4a7667d0 Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Tue, 18 Jun 2024 15:42:29 -0600 Subject: [PATCH 15/24] chore(ramps): removes previously deleted files --- .../app/wallet-overview/token-overview.js | 362 ------------------ .../wallet-overview/token-overview.test.js | 329 ---------------- ui/pages/confirmations/send/send.test.js | 321 ---------------- 3 files changed, 1012 deletions(-) delete mode 100644 ui/components/app/wallet-overview/token-overview.js delete mode 100644 ui/components/app/wallet-overview/token-overview.test.js delete mode 100644 ui/pages/confirmations/send/send.test.js diff --git a/ui/components/app/wallet-overview/token-overview.js b/ui/components/app/wallet-overview/token-overview.js deleted file mode 100644 index 4dde273ea185..000000000000 --- a/ui/components/app/wallet-overview/token-overview.js +++ /dev/null @@ -1,362 +0,0 @@ -import React, { useContext, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { useDispatch, useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; - -import CurrencyDisplay from '../../ui/currency-display'; -import { I18nContext } from '../../../contexts/i18n'; -import { - SEND_ROUTE, - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - BUILD_QUOTE_ROUTE, - ///: END:ONLY_INCLUDE_IF -} from '../../../helpers/constants/routes'; -import { useTokenTracker } from '../../../hooks/useTokenTracker'; -import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; -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/ramps/useRamps/useRamps'; -import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; -import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; -///: END:ONLY_INCLUDE_IF -///: BEGIN:ONLY_INCLUDE_IF(build-mmi) -import { - getMmiPortfolioEnabled, - getMmiPortfolioUrl, -} from '../../../selectors/institutional/selectors'; -///: END:ONLY_INCLUDE_IF -import { - getIsSwapsChain, - getCurrentChainId, - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - getIsBridgeChain, - getCurrentKeyring, - getMetaMetricsId, - ///: END:ONLY_INCLUDE_IF -} from '../../../selectors'; - -import IconButton from '../../ui/icon-button'; -import { INVALID_ASSET_TYPE } from '../../../helpers/constants/error-keys'; -import { showModal } from '../../../store/actions'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { - MetaMetricsEventCategory, - MetaMetricsEventName, - MetaMetricsSwapsEventSource, -} from '../../../../shared/constants/metametrics'; -import { AssetType } from '../../../../shared/constants/transaction'; - -import { Icon, IconName } from '../../component-library'; -import { IconColor } from '../../../helpers/constants/design-system'; - -import { useIsOriginalTokenSymbol } from '../../../hooks/useIsOriginalTokenSymbol'; -import WalletOverview from './wallet-overview'; - -const TokenOverview = ({ className, token }) => { - const dispatch = useDispatch(); - const t = useContext(I18nContext); - const trackEvent = useContext(MetaMetricsContext); - const history = useHistory(); - const { tokensWithBalances } = useTokenTracker({ tokens: [token] }); - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - const keyring = useSelector(getCurrentKeyring); - const usingHardwareWallet = isHardwareKeyring(keyring.type); - const balance = tokensWithBalances[0]?.balance; - ///: END:ONLY_INCLUDE_IF - const balanceToRender = tokensWithBalances[0]?.string; - const formattedFiatBalance = useTokenFiatAmount( - token.address, - balanceToRender, - token.symbol, - ); - - const isOriginalTokenSymbol = useIsOriginalTokenSymbol( - token.address, - token.symbol, - ); - const chainId = useSelector(getCurrentChainId); - const isSwapsChain = useSelector(getIsSwapsChain); - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - const isBridgeChain = useSelector(getIsBridgeChain); - const isBuyableChain = useSelector(getIsNativeTokenBuyable); - const metaMetricsId = useSelector(getMetaMetricsId); - - const { openBuyCryptoInPdapp } = useRamps(); - ///: END:ONLY_INCLUDE_IF - - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - const mmiPortfolioEnabled = useSelector(getMmiPortfolioEnabled); - const mmiPortfolioUrl = useSelector(getMmiPortfolioUrl); - - const portfolioEvent = () => { - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.MMIPortfolioButtonClicked, - }); - }; - - const stakingEvent = () => { - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.MMIPortfolioButtonClicked, - }); - }; - ///: END:ONLY_INCLUDE_IF - - useEffect(() => { - if (token.isERC721) { - dispatch( - showModal({ - name: 'CONVERT_TOKEN_TO_NFT', - tokenAddress: token.address, - }), - ); - } - }, [token.isERC721, token.address, dispatch]); - - return ( - -
- -
- {formattedFiatBalance && isOriginalTokenSymbol ? ( - - ) : null} - - } - buttons={ - <> - { - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - - } - label={t('buyAndSell')} - data-testid="token-overview-buy" - onClick={() => { - openBuyCryptoInPdapp(); - trackEvent({ - event: MetaMetricsEventName.NavBuyButtonClicked, - category: MetaMetricsEventCategory.Navigation, - properties: { - location: 'Token Overview', - text: 'Buy', - chain_id: chainId, - token_symbol: token.symbol, - }, - }); - }} - disabled={token.isERC721 || !isBuyableChain} - /> - ///: END:ONLY_INCLUDE_IF - } - - { - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - <> - - } - label={t('stake')} - data-testid="token-overview-mmi-stake" - onClick={() => { - stakingEvent(); - global.platform.openTab({ - url: `${mmiPortfolioUrl}/stake`, - }); - }} - /> - {mmiPortfolioEnabled && ( - - } - label={t('portfolio')} - data-testid="token-overview-mmi-portfolio" - onClick={() => { - portfolioEvent(); - global.platform.openTab({ - url: mmiPortfolioUrl, - }); - }} - /> - )} - - ///: END:ONLY_INCLUDE_IF - } - - { - trackEvent({ - event: MetaMetricsEventName.NavSendButtonClicked, - category: MetaMetricsEventCategory.Navigation, - properties: { - token_symbol: token.symbol, - location: MetaMetricsSwapsEventSource.TokenView, - text: 'Send', - chain_id: chainId, - }, - }); - try { - await dispatch( - startNewDraftTransaction({ - type: AssetType.token, - details: token, - }), - ); - history.push(SEND_ROUTE); - } catch (err) { - if (!err.message.includes(INVALID_ASSET_TYPE)) { - throw err; - } - } - }} - Icon={ - - } - label={t('send')} - data-testid="eth-overview-send" - disabled={token.isERC721} - /> - {isSwapsChain && ( - - } - onClick={() => { - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - global.platform.openTab({ - url: `${mmiPortfolioUrl}/swap`, - }); - ///: END:ONLY_INCLUDE_IF - - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - trackEvent({ - event: MetaMetricsEventName.NavSwapButtonClicked, - category: MetaMetricsEventCategory.Swaps, - properties: { - token_symbol: token.symbol, - location: MetaMetricsSwapsEventSource.TokenView, - text: 'Swap', - chain_id: chainId, - }, - }); - dispatch( - setSwapsFromToken({ - ...token, - address: token.address.toLowerCase(), - iconUrl: token.image, - balance, - string: balanceToRender, - }), - ); - if (usingHardwareWallet) { - global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE); - } else { - history.push(BUILD_QUOTE_ROUTE); - } - ///: END:ONLY_INCLUDE_IF - }} - label={t('swap')} - tooltipRender={null} - /> - )} - - { - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - isBridgeChain && ( - - } - label={t('bridge')} - onClick={() => { - const portfolioUrl = getPortfolioUrl( - 'bridge', - 'ext_bridge_button', - metaMetricsId, - ); - global.platform.openTab({ - url: `${portfolioUrl}&token=${token.address}`, - }); - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.BridgeLinkClicked, - properties: { - location: 'Token Overview', - text: 'Bridge', - url: portfolioUrl, - chain_id: chainId, - token_symbol: token.symbol, - }, - }); - }} - tooltipRender={null} - /> - ) - ///: END:ONLY_INCLUDE_IF - } - - } - className={className} - /> - ); -}; - -TokenOverview.propTypes = { - className: PropTypes.string, - token: PropTypes.shape({ - address: PropTypes.string.isRequired, - decimals: PropTypes.number, - symbol: PropTypes.string, - image: PropTypes.string, - isERC721: PropTypes.bool, - }).isRequired, -}; - -TokenOverview.defaultProps = { - className: undefined, -}; - -export default TokenOverview; diff --git a/ui/components/app/wallet-overview/token-overview.test.js b/ui/components/app/wallet-overview/token-overview.test.js deleted file mode 100644 index 2550b6caccb6..000000000000 --- a/ui/components/app/wallet-overview/token-overview.test.js +++ /dev/null @@ -1,329 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { fireEvent, waitFor } from '@testing-library/react'; -import { EthAccountType, EthMethod } from '@metamask/keyring-api'; -import { CHAIN_IDS } from '../../../../shared/constants/network'; -import { renderWithProvider } from '../../../../test/jest/rendering'; -import { KeyringType } from '../../../../shared/constants/keyring'; -import { defaultBuyableChains } from '../../../ducks/ramps/constants'; -import TokenOverview from './token-overview'; - -let openTabSpy; - -describe('TokenOverview', () => { - const mockStore = { - metamask: { - providerConfig: { - type: 'test', - chainId: CHAIN_IDS.MAINNET, - }, - currencyRates: {}, - preferences: { - useNativeCurrencyAsPrimaryCurrency: true, - }, - internalAccounts: { - accounts: { - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0x1', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Test Account', - keyring: { - type: 'HD Key Tree', - }, - }, - options: {}, - methods: [...Object.values(EthMethod)], - type: EthAccountType.Eoa, - }, - }, - selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - }, - keyrings: [ - { - type: KeyringType.hdKeyTree, - accounts: ['0x1', '0x2'], - }, - { - type: KeyringType.ledger, - accounts: [], - }, - ], - contractExchangeRates: {}, - mmiConfiguration: { - portfolio: { - enabled: true, - }, - url: 'https://metamask-institutional.io', - }, - }, - ramps: { - buyableChains: defaultBuyableChains, - }, - }; - - const store = configureMockStore([thunk])(mockStore); - - afterEach(() => { - store.clearActions(); - }); - - describe('TokenOverview', () => { - beforeAll(() => { - jest.clearAllMocks(); - Object.defineProperty(global, 'platform', { - value: { - openTab: jest.fn(), - }, - }); - openTabSpy = jest.spyOn(global.platform, 'openTab'); - }); - - beforeEach(() => { - openTabSpy.mockClear(); - }); - - const token = { - name: 'test', - isERC721: false, - address: '0x01', - symbol: 'test', - }; - - it('should not show a modal when token passed in props is not an ERC721', () => { - renderWithProvider(, store); - - const actions = store.getActions(); - expect(actions).toHaveLength(0); - }); - - it('should show ConvertTokenToNFT modal when token passed in props is an ERC721', () => { - const nftToken = { - ...token, - isERC721: true, - }; - renderWithProvider(, store); - - const actions = store.getActions(); - expect(actions).toHaveLength(1); - expect(actions[0].type).toBe('UI_MODAL_OPEN'); - expect(actions[0].payload).toStrictEqual({ - name: 'CONVERT_TOKEN_TO_NFT', - tokenAddress: '0x01', - }); - }); - - it('should always show the Buy button regardless of chain Id', () => { - const mockedStoreWithUnbuyableChainId = { - ...mockStore, - metamask: { - ...mockStore.metamask, - providerConfig: { type: 'test', chainId: CHAIN_IDS.PALM }, - }, - }; - const mockedStore = configureMockStore([thunk])( - mockedStoreWithUnbuyableChainId, - ); - - const { queryByTestId } = renderWithProvider( - , - mockedStore, - ); - const buyButton = queryByTestId('token-overview-buy'); - expect(buyButton).toBeInTheDocument(); - }); - - it('should always show the Buy button regardless of token type', () => { - const nftToken = { - ...token, - isERC721: true, - }; - - const { queryByTestId } = renderWithProvider( - , - store, - ); - const buyButton = queryByTestId('token-overview-buy'); - expect(buyButton).toBeInTheDocument(); - }); - - it('should have the Buy token button disabled if chain id is not part of supported buyable chains', () => { - const mockedStoreWithUnbuyableChainId = { - ...mockStore, - metamask: { - ...mockStore.metamask, - providerConfig: { type: 'test', chainId: CHAIN_IDS.GOERLI }, - }, - }; - const mockedStore = configureMockStore([thunk])( - mockedStoreWithUnbuyableChainId, - ); - - const { queryByTestId } = renderWithProvider( - , - mockedStore, - ); - const buyButton = queryByTestId('token-overview-buy'); - expect(buyButton).toBeInTheDocument(); - expect(buyButton).toBeDisabled(); - }); - - it('should have the Buy token button enabled if chain id is part of supported buyable chains', () => { - const mockedStoreWithBuyableChainId = { - ...mockStore, - metamask: { - ...mockStore.metamask, - providerConfig: { type: 'test', chainId: CHAIN_IDS.POLYGON }, - }, - }; - const mockedStore = configureMockStore([thunk])( - mockedStoreWithBuyableChainId, - ); - - const { queryByTestId } = renderWithProvider( - , - mockedStore, - ); - const buyButton = queryByTestId('token-overview-buy'); - expect(buyButton).toBeInTheDocument(); - expect(buyButton).not.toBeDisabled(); - }); - - it('should have the Buy token button disabled for ERC721 tokens', () => { - const nftToken = { - ...token, - isERC721: true, - }; - - const mockedStoreWithBuyableChainId = { - ...mockStore, - metamask: { - ...mockStore.metamask, - providerConfig: { type: 'test', chainId: CHAIN_IDS.POLYGON }, - }, - }; - const mockedStore = configureMockStore([thunk])( - mockedStoreWithBuyableChainId, - ); - - const { queryByTestId } = renderWithProvider( - , - mockedStore, - ); - const buyButton = queryByTestId('token-overview-buy'); - expect(buyButton).toBeInTheDocument(); - expect(buyButton).toBeDisabled(); - }); - - it('should open the buy crypto URL for a buyable chain ID', async () => { - const mockedStoreWithBuyableChainId = { - ...mockStore, - metamask: { - ...mockStore.metamask, - providerConfig: { type: 'test', chainId: CHAIN_IDS.POLYGON }, - }, - }; - const mockedStore = configureMockStore([thunk])( - mockedStoreWithBuyableChainId, - ); - - const { queryByTestId } = renderWithProvider( - , - mockedStore, - ); - const buyButton = queryByTestId('token-overview-buy'); - expect(buyButton).toBeInTheDocument(); - expect(buyButton).not.toBeDisabled(); - - fireEvent.click(buyButton); - expect(openTabSpy).toHaveBeenCalledTimes(1); - - await waitFor(() => - expect(openTabSpy).toHaveBeenCalledWith({ - url: expect.stringContaining( - `/buy?metamaskEntry=ext_buy_sell_button`, - ), - }), - ); - }); - - it('should show the Bridge button if chain id is supported', async () => { - const mockToken = { - name: 'test', - isERC721: false, - address: '0x7ceb23fd6bc0add59e62ac25578270cff1B9f619', - symbol: 'test', - }; - - const mockedStoreWithBridgeableChainId = { - ...mockStore, - metamask: { - ...mockStore.metamask, - providerConfig: { type: 'test', chainId: CHAIN_IDS.POLYGON }, - }, - }; - const mockedStore = configureMockStore([thunk])( - mockedStoreWithBridgeableChainId, - ); - - const { queryByTestId } = renderWithProvider( - , - mockedStore, - ); - const bridgeButton = queryByTestId('token-overview-bridge'); - expect(bridgeButton).toBeInTheDocument(); - expect(bridgeButton).not.toBeDisabled(); - - fireEvent.click(bridgeButton); - expect(openTabSpy).toHaveBeenCalledTimes(1); - - await waitFor(() => - expect(openTabSpy).toHaveBeenCalledWith({ - url: expect.stringContaining( - '/bridge?metamaskEntry=ext_bridge_button&metametricsId=&token=0x7ceb23fd6bc0add59e62ac25578270cff1B9f619', - ), - }), - ); - }); - - it('should not show the Bridge button if chain id is not supported', async () => { - const mockToken = { - name: 'test', - isERC721: false, - address: '0x7ceb23fd6bc0add59e62ac25578270cff1B9f619', - symbol: 'test', - }; - - const mockedStoreWithBridgeableChainId = { - ...mockStore, - metamask: { - ...mockStore.metamask, - providerConfig: { type: 'test', chainId: CHAIN_IDS.FANTOM }, - }, - }; - const mockedStore = configureMockStore([thunk])( - mockedStoreWithBridgeableChainId, - ); - - const { queryByTestId } = renderWithProvider( - , - mockedStore, - ); - const bridgeButton = queryByTestId('token-overview-bridge'); - expect(bridgeButton).not.toBeInTheDocument(); - }); - - it('should show the MMI Portfolio and Stake buttons', () => { - const { queryByTestId } = renderWithProvider( - , - store, - ); - const mmiStakeButton = queryByTestId('token-overview-mmi-stake'); - const mmiPortfolioButton = queryByTestId('token-overview-mmi-portfolio'); - - expect(mmiStakeButton).toBeInTheDocument(); - expect(mmiPortfolioButton).toBeInTheDocument(); - }); - }); -}); diff --git a/ui/pages/confirmations/send/send.test.js b/ui/pages/confirmations/send/send.test.js deleted file mode 100644 index 369062b31079..000000000000 --- a/ui/pages/confirmations/send/send.test.js +++ /dev/null @@ -1,321 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { useLocation } from 'react-router-dom'; -import { NetworkType } from '@metamask/controller-utils'; -import { EthAccountType, EthMethod } from '@metamask/keyring-api'; -import { SEND_STAGES, startNewDraftTransaction } from '../../../ducks/send'; -import { domainInitialState } from '../../../ducks/domains'; -import { setBackgroundConnection } from '../../../store/background-connection'; -import { - CHAIN_IDS, - GOERLI_DISPLAY_NAME, - NETWORK_TYPES, -} from '../../../../shared/constants/network'; -import { renderWithProvider } from '../../../../test/jest'; -import { GasEstimateTypes } from '../../../../shared/constants/gas'; -import { KeyringType } from '../../../../shared/constants/keyring'; -import { INITIAL_SEND_STATE_FOR_EXISTING_DRAFT } from '../../../../test/jest/mocks'; -import { defaultBuyableChains } from '../../../ducks/ramps/constants'; -import Send from './send'; - -const middleware = [thunk]; - -jest.mock('../../../ducks/send/send', () => { - const original = jest.requireActual('../../../ducks/send/send'); - return { - ...original, - // We don't really need to start a draft transaction, and the mock store - // does not update as a result of action calls so instead we just ensure - // that the action WOULD be called. - startNewDraftTransaction: jest.fn(() => ({ - type: 'TEST_START_NEW_DRAFT', - payload: null, - })), - }; -}); - -jest.mock('react-router-dom', () => { - const original = jest.requireActual('react-router-dom'); - return { - ...original, - useLocation: jest.fn(() => ({ search: '' })), - useHistory: () => ({ - push: jest.fn(), - }), - }; -}); - -setBackgroundConnection({ - getGasFeeTimeEstimate: jest.fn(), - promisifiedBackground: jest.fn(), -}); - -jest.mock('@ethersproject/providers', () => { - const originalModule = jest.requireActual('@ethersproject/providers'); - return { - ...originalModule, - Web3Provider: jest.fn().mockImplementation(() => { - return {}; - }), - }; -}); -const baseStore = { - send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, - DNS: domainInitialState, - gas: { - customData: { limit: null, price: null }, - }, - history: { mostRecentOverviewPage: 'activity' }, - metamask: { - transactions: [ - { - id: 1, - txParams: { - value: 'oldTxValue', - }, - }, - ], - gasEstimateType: GasEstimateTypes.legacy, - gasFeeEstimates: { - low: '0', - medium: '1', - fast: '2', - }, - internalAccounts: { - accounts: { - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0x0', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Test Account', - keyring: { - type: 'HD Key Tree', - }, - }, - options: {}, - methods: [...Object.values(EthMethod)], - type: EthAccountType.Eoa, - }, - }, - selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - }, - keyrings: [ - { - type: KeyringType.hdKeyTree, - accounts: ['0x0'], - }, - ], - selectedNetworkClientId: NetworkType.mainnet, - networksMetadata: { - [NetworkType.mainnet]: { - EIPS: {}, - status: 'available', - }, - }, - tokens: [], - preferences: { - useNativeCurrencyAsPrimaryCurrency: false, - }, - currentCurrency: 'USD', - providerConfig: { - chainId: CHAIN_IDS.GOERLI, - }, - currencyRates: {}, - featureFlags: { - sendHexData: false, - }, - addressBook: { - [CHAIN_IDS.GOERLI]: [], - }, - accountsByChainId: { - [CHAIN_IDS.GOERLI]: {}, - }, - accounts: { - '0x0': { balance: '0x0', address: '0x0' }, - }, - tokenAddress: '0x32e6c34cd57087abbd59b5a4aecc4cb495924356', - tokenList: { - '0x32e6c34cd57087abbd59b5a4aecc4cb495924356': { - name: 'BitBase', - symbol: 'BTBS', - decimals: 18, - address: '0x32E6C34Cd57087aBBD59B5A4AECC4cB495924356', - iconUrl: 'BTBS.svg', - occurrences: null, - }, - '0x3fa400483487a489ec9b1db29c4129063eec4654': { - name: 'Cryptokek.com', - symbol: 'KEK', - decimals: 18, - address: '0x3fa400483487A489EC9b1dB29C4129063EEC4654', - iconUrl: 'cryptokek.svg', - occurrences: null, - }, - }, - }, - appState: { - sendInputCurrencySwitched: false, - }, - ramps: { - buyableChains: defaultBuyableChains, - }, -}; - -const placeholderText = 'Enter public address (0x) or ENS name'; - -describe('Send Page', () => { - describe('Send Flow Initialization', () => { - it('should initialize the ENS slice on render', () => { - const store = configureMockStore(middleware)(baseStore); - renderWithProvider(, store); - const actions = store.getActions(); - expect(actions).toStrictEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: 'DNS/enableDomainLookup', - }), - ]), - ); - }); - - it('should showQrScanner when location.search is ?scan=true', () => { - useLocation.mockImplementation(() => ({ search: '?scan=true' })); - const store = configureMockStore(middleware)(baseStore); - renderWithProvider(, store); - const actions = store.getActions(); - expect(actions).toStrictEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: 'DNS/enableDomainLookup', - }), - expect.objectContaining({ - type: 'UI_MODAL_OPEN', - payload: { name: 'QR_SCANNER' }, - }), - ]), - ); - useLocation.mockImplementation(() => ({ search: '' })); - }); - }); - - describe('Send Flow', () => { - it('should render the header with Send to displayed', () => { - const store = configureMockStore(middleware)(baseStore); - const { getByText } = renderWithProvider(, store); - expect(getByText('Send to')).toBeTruthy(); - }); - - it('should render the DomainInput field', () => { - const store = configureMockStore(middleware)(baseStore); - const { getByPlaceholderText } = renderWithProvider(, store); - expect(getByPlaceholderText(placeholderText)).toBeTruthy(); - }); - - it('should not render the footer', () => { - const store = configureMockStore(middleware)(baseStore); - const { queryByText } = renderWithProvider(, store); - expect(queryByText('Next')).toBeNull(); - }); - - it('should render correctly even when a draftTransaction does not exist', () => { - const modifiedStore = { - ...baseStore, - send: { - ...baseStore.send, - currentTransactionUUID: null, - }, - }; - const store = configureMockStore(middleware)(modifiedStore); - const { getByPlaceholderText } = renderWithProvider(, store); - // Ensure that the send flow renders on the add recipient screen when - // there is no draft transaction. - expect(getByPlaceholderText(placeholderText)).toBeTruthy(); - // Ensure we start a new draft transaction when its missing. - expect(startNewDraftTransaction).toHaveBeenCalledTimes(1); - }); - }); - - describe('Send and Edit Flow (draft)', () => { - it('should render the header with Send displayed', () => { - const store = configureMockStore(middleware)({ - ...baseStore, - send: { ...baseStore.send, stage: SEND_STAGES.DRAFT }, - confirmTransaction: { - txData: { - id: 3111025347726181, - time: 1620723786838, - status: 'unapproved', - chainId: '0x5', - loadingDefaults: false, - txParams: { - from: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', - to: '0xaD6D458402F60fD3Bd25163575031ACDce07538D', - value: '0x0', - data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170', - gas: '0xea60', - gasPrice: '0x4a817c800', - }, - type: 'transfer', - origin: 'https://metamask.github.io', - transactionCategory: 'approve', - }, - }, - metamask: { - ...baseStore.metamask, - providerConfig: { - chainId: CHAIN_IDS.GOERLI, - nickname: GOERLI_DISPLAY_NAME, - type: NETWORK_TYPES.GOERLI, - }, - }, - }); - const { getByText } = renderWithProvider(, store); - expect(getByText('Send')).toBeTruthy(); - }); - - it('should render the DomainInput field', () => { - const store = configureMockStore(middleware)(baseStore); - const { getByPlaceholderText } = renderWithProvider(, store); - expect(getByPlaceholderText(placeholderText)).toBeTruthy(); - }); - - it('should render the footer', () => { - const store = configureMockStore(middleware)({ - ...baseStore, - send: { ...baseStore.send, stage: SEND_STAGES.DRAFT }, - confirmTransaction: { - txData: { - id: 3111025347726181, - time: 1620723786838, - status: 'unapproved', - metamaskNetworkId: '5', - chainId: '0x5', - loadingDefaults: false, - txParams: { - from: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', - to: '0xaD6D458402F60fD3Bd25163575031ACDce07538D', - value: '0x0', - data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170', - gas: '0xea60', - gasPrice: '0x4a817c800', - }, - type: 'transfer', - origin: 'https://metamask.github.io', - transactionCategory: 'approve', - }, - }, - metamask: { - ...baseStore.metamask, - providerConfig: { - chainId: CHAIN_IDS.GOERLI, - nickname: GOERLI_DISPLAY_NAME, - type: NETWORK_TYPES.GOERLI, - }, - }, - }); - const { getByText } = renderWithProvider(, store); - expect(getByText('Next')).toBeTruthy(); - }); - }); -}); From e4312d667c7c038f0929c8bb75bdf1539711e30a Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Tue, 18 Jun 2024 16:29:21 -0600 Subject: [PATCH 16/24] chore(ramps): fixes linting errors after rebasing with develop branch --- ui/components/app/wallet-overview/coin-buttons.tsx | 3 ++- ui/hooks/ramps/useRamps/useRamps.test.tsx | 4 +++- ui/pages/asset/components/asset-page.tsx | 4 ++-- ui/pages/asset/components/token-buttons.tsx | 8 +++----- 4 files changed, 10 insertions(+), 9 deletions(-) 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/hooks/ramps/useRamps/useRamps.test.tsx b/ui/hooks/ramps/useRamps/useRamps.test.tsx index fac84c4e9b9f..9c53eb9da247 100644 --- a/ui/hooks/ramps/useRamps/useRamps.test.tsx +++ b/ui/hooks/ramps/useRamps/useRamps.test.tsx @@ -82,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/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..7f1f34240978 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,7 @@ import { } from '../../../helpers/constants/design-system'; import IconButton from '../../../components/ui/icon-button/icon-button'; import { Box, Icon, IconName } from '../../../components/component-library'; +import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; import { Asset } from './asset-page'; const TokenButtons = ({ @@ -71,7 +69,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 From 379a2d8543b076f53a7815e1f3aaf5ebb8d158af Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Thu, 20 Jun 2024 09:32:56 -0600 Subject: [PATCH 17/24] chore(ramps): adds build gate comment to import that failed linting --- ui/pages/asset/components/token-buttons.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/pages/asset/components/token-buttons.tsx b/ui/pages/asset/components/token-buttons.tsx index 7f1f34240978..6cd78fab7693 100644 --- a/ui/pages/asset/components/token-buttons.tsx +++ b/ui/pages/asset/components/token-buttons.tsx @@ -47,7 +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 = ({ From fb2de4827f258c1a379c1425b0b240fa32e8817b Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Thu, 20 Jun 2024 11:15:13 -0600 Subject: [PATCH 18/24] chore(ramps): adds more build gate comments to import that failed linting --- ui/components/app/wallet-overview/eth-overview.js | 2 ++ ui/pages/asset/components/asset-page.tsx | 2 ++ .../confirm-page-container/confirm-page-container.component.js | 2 ++ .../confirm-transaction-base.container.js | 2 ++ ui/pages/confirmations/send/gas-display/gas-display.js | 2 ++ 5 files changed, 10 insertions(+) diff --git a/ui/components/app/wallet-overview/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js index d473a17edc60..684c6cffbf6b 100644 --- a/ui/components/app/wallet-overview/eth-overview.js +++ b/ui/components/app/wallet-overview/eth-overview.js @@ -14,7 +14,9 @@ import { getIsBridgeChain, ///: 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 }) => { diff --git a/ui/pages/asset/components/asset-page.tsx b/ui/pages/asset/components/asset-page.tsx index 38ea46e475f3..717d89744735 100644 --- a/ui/pages/asset/components/asset-page.tsx +++ b/ui/pages/asset/components/asset-page.tsx @@ -41,7 +41,9 @@ 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'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; +///: END:ONLY_INCLUDE_IF import AssetChart from './chart/asset-chart'; import TokenButtons from './token-buttons'; 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 fee3cde06b1f..481cb34f105c 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 @@ -59,7 +59,9 @@ import { ///: END:ONLY_INCLUDE_IF import { BlockaidResultType } from '../../../../../shared/constants/security-provider'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; +///: END:ONLY_INCLUDE_IF import { ConfirmPageContainerHeader, ConfirmPageContainerContent, 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 2bfb67970f6f..1d7c17f5fba2 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 @@ -109,7 +109,9 @@ import { showCustodyConfirmLink } from '../../../store/institutional/institution import { getTokenAddressParam } from '../../../helpers/utils/token-util'; import { calcGasTotal } from '../../../../shared/lib/transactions-controller-utils'; import { subtractHexes } from '../../../../shared/modules/conversion.utils'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; +///: END:ONLY_INCLUDE_IF import ConfirmTransactionBase from './confirm-transaction-base.component'; let customNonceValue = ''; diff --git a/ui/pages/confirmations/send/gas-display/gas-display.js b/ui/pages/confirmations/send/gas-display/gas-display.js index 5fbad8445cd6..205593ce3df5 100644 --- a/ui/pages/confirmations/send/gas-display/gas-display.js +++ b/ui/pages/confirmations/send/gas-display/gas-display.js @@ -46,7 +46,9 @@ import { } from '../../../../../shared/constants/metametrics'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; import useRamps from '../../../../hooks/ramps/useRamps/useRamps'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; +///: END:ONLY_INCLUDE_IF export default function GasDisplay({ gasError }) { const t = useContext(I18nContext); From 6cb9380bb051e12e0f7a785c5e78181a7331573d Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Thu, 20 Jun 2024 11:37:59 -0600 Subject: [PATCH 19/24] chore(ramps): adds more build gate comments to import that failed linting --- .../confirm-transaction-base.container.js | 2 -- 1 file changed, 2 deletions(-) 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 1d7c17f5fba2..2bfb67970f6f 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 @@ -109,9 +109,7 @@ import { showCustodyConfirmLink } from '../../../store/institutional/institution import { getTokenAddressParam } from '../../../helpers/utils/token-util'; import { calcGasTotal } from '../../../../shared/lib/transactions-controller-utils'; import { subtractHexes } from '../../../../shared/modules/conversion.utils'; -///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; -///: END:ONLY_INCLUDE_IF import ConfirmTransactionBase from './confirm-transaction-base.component'; let customNonceValue = ''; From 242d3b92cfb354b9013aa224e01e61dc9be4130e Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Thu, 20 Jun 2024 12:41:00 -0600 Subject: [PATCH 20/24] chore(ramps): adds more build gate comments to import that failed linting --- .../confirm-page-container/confirm-page-container.component.js | 2 -- 1 file changed, 2 deletions(-) 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 481cb34f105c..fee3cde06b1f 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 @@ -59,9 +59,7 @@ import { ///: END:ONLY_INCLUDE_IF import { BlockaidResultType } from '../../../../../shared/constants/security-provider'; -///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; -///: END:ONLY_INCLUDE_IF import { ConfirmPageContainerHeader, ConfirmPageContainerContent, From 2e2dc808b5b4801d75f21c29a907837367bc9520 Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Fri, 21 Jun 2024 10:43:50 -0600 Subject: [PATCH 21/24] chore(ramps): fixes linting errors after rebasing with develop branch --- ui/pages/confirmations/hooks/useConfirmationAlertActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 = () => { From b63725206e6c679cfe5eb59ae9e2b03339ec566a Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Mon, 24 Jun 2024 10:30:52 -0600 Subject: [PATCH 22/24] fix(ramps): use jest.mocked instead of suppressing TS error --- ui/ducks/ramps/ramps.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/ducks/ramps/ramps.test.ts b/ui/ducks/ramps/ramps.test.ts index f2e29e67938d..06e2958d8c69 100644 --- a/ui/ducks/ramps/ramps.test.ts +++ b/ui/ducks/ramps/ramps.test.ts @@ -10,6 +10,7 @@ import rampsReducer, { import { defaultBuyableChains } from './constants'; jest.mock('../../helpers/ramps/rampApi/rampAPI'); +const mockedRampAPI = RampAPI as jest.Mocked; jest.mock('../../selectors', () => ({ getCurrentChainId: jest.fn(), @@ -25,8 +26,7 @@ describe('rampsSlice', () => { ramps: rampsReducer, }, }); - // @ts-expect-error mocked API has mockReset method - RampAPI.getNetworks.mockReset(); + mockedRampAPI.getNetworks.mockReset(); }); it('should set the initial state to defaultBuyableChains', () => { From ba2eefa02016111194ea1e35e031bbb1d04ca74d Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Thu, 27 Jun 2024 06:04:35 -0600 Subject: [PATCH 23/24] feat(ramps): do not make requests to ramp api if basic functionality toggle is on --- ui/ducks/ramps/ramps.test.ts | 21 +++++++++++++++++++-- ui/ducks/ramps/ramps.ts | 9 +++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/ui/ducks/ramps/ramps.test.ts b/ui/ducks/ramps/ramps.test.ts index 06e2958d8c69..c4ca4089815a 100644 --- a/ui/ducks/ramps/ramps.test.ts +++ b/ui/ducks/ramps/ramps.test.ts @@ -1,6 +1,6 @@ import { configureStore, Store } from '@reduxjs/toolkit'; import RampAPI from '../../helpers/ramps/rampApi/rampAPI'; -import { getCurrentChainId } from '../../selectors'; +import { getCurrentChainId, getUseExternalServices } from '../../selectors'; import { CHAIN_IDS } from '../../../shared/constants/network'; import rampsReducer, { fetchBuyableChains, @@ -14,6 +14,7 @@ const mockedRampAPI = RampAPI as jest.Mocked; jest.mock('../../selectors', () => ({ getCurrentChainId: jest.fn(), + getUseExternalServices: jest.fn(), getNames: jest.fn(), })); @@ -82,12 +83,28 @@ describe('rampsSlice', () => { }); describe('fetchBuyableChains', () => { - it('should call RampAPI.getNetworks', async () => { + 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 = [ { diff --git a/ui/ducks/ramps/ramps.ts b/ui/ducks/ramps/ramps.ts index d43993260da1..afff609cd4d8 100644 --- a/ui/ducks/ramps/ramps.ts +++ b/ui/ducks/ramps/ramps.ts @@ -1,6 +1,6 @@ import { createSelector } from 'reselect'; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -import { getCurrentChainId } from '../../selectors'; +import { getCurrentChainId, getUseExternalServices } from '../../selectors'; import RampAPI from '../../helpers/ramps/rampApi/rampAPI'; import { hexToDecimal } from '../../../shared/modules/conversion.utils'; import { defaultBuyableChains } from './constants'; @@ -8,7 +8,12 @@ import { AggregatorNetwork } from './types'; export const fetchBuyableChains = createAsyncThunk( 'ramps/fetchBuyableChains', - async () => { + async (_, { getState }) => { + const state = getState(); + const allowExternalRequests = getUseExternalServices(state); + if (!allowExternalRequests) { + return defaultBuyableChains; + } return await RampAPI.getNetworks(); }, ); From 7ee6250844a691debf2e9cb681957adf168bad2c Mon Sep 17 00:00:00 2001 From: georgeweiler Date: Thu, 27 Jun 2024 07:55:19 -0600 Subject: [PATCH 24/24] fix: resolves mmi e2e test error --- ui/pages/asset/components/asset-page.tsx | 2 -- ui/pages/confirmations/send/gas-display/gas-display.js | 2 -- 2 files changed, 4 deletions(-) diff --git a/ui/pages/asset/components/asset-page.tsx b/ui/pages/asset/components/asset-page.tsx index 717d89744735..38ea46e475f3 100644 --- a/ui/pages/asset/components/asset-page.tsx +++ b/ui/pages/asset/components/asset-page.tsx @@ -41,9 +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'; -///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsNativeTokenBuyable } from '../../../ducks/ramps'; -///: END:ONLY_INCLUDE_IF import AssetChart from './chart/asset-chart'; import TokenButtons from './token-buttons'; diff --git a/ui/pages/confirmations/send/gas-display/gas-display.js b/ui/pages/confirmations/send/gas-display/gas-display.js index 205593ce3df5..5fbad8445cd6 100644 --- a/ui/pages/confirmations/send/gas-display/gas-display.js +++ b/ui/pages/confirmations/send/gas-display/gas-display.js @@ -46,9 +46,7 @@ import { } from '../../../../../shared/constants/metametrics'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; import useRamps from '../../../../hooks/ramps/useRamps/useRamps'; -///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; -///: END:ONLY_INCLUDE_IF export default function GasDisplay({ gasError }) { const t = useContext(I18nContext);