diff --git a/app/scripts/controllers/incoming-transactions.js b/app/scripts/controllers/incoming-transactions.js index 10f79539c439..987f0d87b826 100644 --- a/app/scripts/controllers/incoming-transactions.js +++ b/app/scripts/controllers/incoming-transactions.js @@ -9,11 +9,7 @@ import { TransactionType, TransactionStatus, } from '../../../shared/constants/transaction'; -import { - CHAIN_IDS, - CHAIN_ID_TO_NETWORK_ID_MAP, - CHAIN_ID_TO_TYPE_MAP, -} from '../../../shared/constants/network'; +import { ETHERSCAN_SUPPORTED_NETWORKS } from '../../../shared/constants/network'; import { bnToHex } from '../../../shared/modules/conversion.utils'; const fetchWithTimeout = getFetchWithTimeout(); @@ -46,15 +42,9 @@ const fetchWithTimeout = getFetchWithTimeout(); * This controller is responsible for retrieving incoming transactions. Etherscan is polled once every block to check * for new incoming transactions for the current selected account on the current network * - * Note that only the built-in Infura networks are supported (i.e. anything in `INFURA_PROVIDER_TYPES`). We will not - * attempt to retrieve incoming transactions on any custom RPC endpoints. + * Note that only Etherscan-compatible networks are supported. We will not attempt to retrieve incoming transactions + * on non-compatible custom RPC endpoints. */ -const etherscanSupportedNetworks = [ - CHAIN_IDS.GOERLI, - CHAIN_IDS.MAINNET, - CHAIN_IDS.SEPOLIA, -]; - export default class IncomingTransactionsController { constructor(opts = {}) { const { @@ -75,13 +65,16 @@ export default class IncomingTransactionsController { await this._update(selectedAddress, newBlockNumberDec); }; + const incomingTxLastFetchedBlockByChainId = Object.keys( + ETHERSCAN_SUPPORTED_NETWORKS, + ).reduce((network, chainId) => { + network[chainId] = null; + return network; + }, {}); + const initState = { incomingTransactions: {}, - incomingTxLastFetchedBlockByChainId: { - [CHAIN_IDS.GOERLI]: null, - [CHAIN_IDS.MAINNET]: null, - [CHAIN_IDS.SEPOLIA]: null, - }, + incomingTxLastFetchedBlockByChainId, ...opts.initState, }; this.store = new ObservableStore(initState); @@ -171,7 +164,7 @@ export default class IncomingTransactionsController { const { completedOnboarding } = this.onboardingController.store.getState(); const chainId = this.getCurrentChainId(); if ( - !etherscanSupportedNetworks.includes(chainId) || + !Object.hasOwnProperty.call(ETHERSCAN_SUPPORTED_NETWORKS, chainId) || !address || !completedOnboarding ) { @@ -235,12 +228,10 @@ export default class IncomingTransactionsController { * @returns {TransactionMeta[]} */ async _getNewIncomingTransactions(address, fromBlock, chainId) { - const etherscanSubdomain = - chainId === CHAIN_IDS.MAINNET - ? 'api' - : `api-${CHAIN_ID_TO_TYPE_MAP[chainId]}`; + const etherscanDomain = ETHERSCAN_SUPPORTED_NETWORKS[chainId].domain; + const etherscanSubdomain = ETHERSCAN_SUPPORTED_NETWORKS[chainId].subdomain; - const apiUrl = `https://${etherscanSubdomain}.etherscan.io`; + const apiUrl = `https://${etherscanSubdomain}.${etherscanDomain}`; let url = `${apiUrl}/api?module=account&action=txlist&address=${address}&tag=latest&page=1`; if (fromBlock) { @@ -303,7 +294,7 @@ export default class IncomingTransactionsController { blockNumber: etherscanTransaction.blockNumber, id: createId(), chainId, - metamaskNetworkId: CHAIN_ID_TO_NETWORK_ID_MAP[chainId], + metamaskNetworkId: ETHERSCAN_SUPPORTED_NETWORKS[chainId].networkId, status, time, txParams, diff --git a/app/scripts/controllers/incoming-transactions.test.js b/app/scripts/controllers/incoming-transactions.test.js index 48bea442af46..bdfac55a75d4 100644 --- a/app/scripts/controllers/incoming-transactions.test.js +++ b/app/scripts/controllers/incoming-transactions.test.js @@ -6,7 +6,7 @@ import { cloneDeep } from 'lodash'; import waitUntilCalled from '../../../test/lib/wait-until-called'; import { - CHAIN_ID_TO_TYPE_MAP, + ETHERSCAN_SUPPORTED_NETWORKS, CHAIN_IDS, NETWORK_TYPES, NETWORK_IDS, @@ -34,11 +34,12 @@ const PREPOPULATED_BLOCKS_BY_NETWORK = { [CHAIN_IDS.MAINNET]: 3, [CHAIN_IDS.SEPOLIA]: 6, }; -const EMPTY_BLOCKS_BY_NETWORK = { - [CHAIN_IDS.GOERLI]: null, - [CHAIN_IDS.MAINNET]: null, - [CHAIN_IDS.SEPOLIA]: null, -}; +const EMPTY_BLOCKS_BY_NETWORK = Object.keys( + ETHERSCAN_SUPPORTED_NETWORKS, +).reduce((network, chainId) => { + network[chainId] = null; + return network; +}, {}); function getEmptyInitState() { return { @@ -150,20 +151,13 @@ const getFakeEtherscanTransaction = ({ }; function nockEtherscanApiForAllChains(mockResponse) { - for (const chainId of [ - CHAIN_IDS.GOERLI, - CHAIN_IDS.MAINNET, - CHAIN_IDS.SEPOLIA, - 'undefined', - ]) { - nock( - `https://api${ - chainId === CHAIN_IDS.MAINNET ? '' : `-${CHAIN_ID_TO_TYPE_MAP[chainId]}` - }.etherscan.io`, - ) - .get(/api.+/u) - .reply(200, JSON.stringify(mockResponse)); - } + Object.values(ETHERSCAN_SUPPORTED_NETWORKS).forEach( + ({ domain, subdomain }) => { + nock(`https://${domain}.${subdomain}`) + .get(/api.+/u) + .reply(200, JSON.stringify(mockResponse)); + }, + ); } describe('IncomingTransactionsController', function () { diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 84314ac3dd58..3fd5e4b338cc 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -201,17 +201,24 @@ export const CHAIN_IDS = { GOERLI: '0x5', LOCALHOST: '0x539', BSC: '0x38', + BSC_TESTNET: '0x61', OPTIMISM: '0xa', OPTIMISM_TESTNET: '0x1a4', POLYGON: '0x89', + POLYGON_TESTNET: '0x13881', AVALANCHE: '0xa86a', + AVALANCHE_TESTNET: '0xa869', FANTOM: '0xfa', + FANTOM_TESTNET: '0xfa2', CELO: '0xa4ec', ARBITRUM: '0xa4b1', HARMONY: '0x63564c40', PALM: '0x2a15c308d', SEPOLIA: '0xaa36a7', AURORA: '0x4e454152', + MOONBEAM: '0x504', + MOONBEAM_TESTNET: '0x507', + MOONRIVER: '0x505', } as const; /** @@ -542,6 +549,98 @@ export const NATIVE_CURRENCY_TOKEN_IMAGE_MAP = { export const INFURA_BLOCKED_KEY = 'countryBlocked'; +const defaultEtherscanDomain = 'etherscan.io'; +const defaultEtherscanSubdomainPrefix = 'api'; +/** + * Map of all Etherscan supported networks. + */ +export const ETHERSCAN_SUPPORTED_NETWORKS = { + [CHAIN_IDS.GOERLI]: { + domain: defaultEtherscanDomain, + subdomain: `${defaultEtherscanSubdomainPrefix}-${ + CHAIN_ID_TO_TYPE_MAP[CHAIN_IDS.GOERLI] + }`, + networkId: CHAIN_ID_TO_NETWORK_ID_MAP[CHAIN_IDS.GOERLI], + }, + [CHAIN_IDS.MAINNET]: { + domain: defaultEtherscanDomain, + subdomain: defaultEtherscanSubdomainPrefix, + networkId: CHAIN_ID_TO_NETWORK_ID_MAP[CHAIN_IDS.MAINNET], + }, + [CHAIN_IDS.SEPOLIA]: { + domain: defaultEtherscanDomain, + subdomain: `${defaultEtherscanSubdomainPrefix}-${ + CHAIN_ID_TO_TYPE_MAP[CHAIN_IDS.SEPOLIA] + }`, + networkId: CHAIN_ID_TO_NETWORK_ID_MAP[CHAIN_IDS.SEPOLIA], + }, + [CHAIN_IDS.BSC]: { + domain: 'bscscan.com', + subdomain: defaultEtherscanSubdomainPrefix, + networkId: parseInt(CHAIN_IDS.BSC, 16).toString(), + }, + [CHAIN_IDS.BSC_TESTNET]: { + domain: 'bscscan.com', + subdomain: `${defaultEtherscanSubdomainPrefix}-testnet`, + networkId: parseInt(CHAIN_IDS.BSC_TESTNET, 16).toString(), + }, + [CHAIN_IDS.OPTIMISM]: { + domain: defaultEtherscanDomain, + subdomain: `${defaultEtherscanSubdomainPrefix}-optimistic`, + networkId: parseInt(CHAIN_IDS.OPTIMISM, 16).toString(), + }, + [CHAIN_IDS.OPTIMISM_TESTNET]: { + domain: defaultEtherscanDomain, + subdomain: `${defaultEtherscanSubdomainPrefix}-goerli-optimistic`, + networkId: parseInt(CHAIN_IDS.OPTIMISM_TESTNET, 16).toString(), + }, + [CHAIN_IDS.POLYGON]: { + domain: 'polygonscan.com', + subdomain: defaultEtherscanSubdomainPrefix, + networkId: parseInt(CHAIN_IDS.POLYGON, 16).toString(), + }, + [CHAIN_IDS.POLYGON_TESTNET]: { + domain: 'polygonscan.com', + subdomain: `${defaultEtherscanSubdomainPrefix}-mumbai`, + networkId: parseInt(CHAIN_IDS.POLYGON_TESTNET, 16).toString(), + }, + [CHAIN_IDS.AVALANCHE]: { + domain: 'snowtrace.io', + subdomain: defaultEtherscanSubdomainPrefix, + networkId: parseInt(CHAIN_IDS.AVALANCHE, 16).toString(), + }, + [CHAIN_IDS.AVALANCHE_TESTNET]: { + domain: 'snowtrace.io', + subdomain: `${defaultEtherscanSubdomainPrefix}-testnet`, + networkId: parseInt(CHAIN_IDS.AVALANCHE_TESTNET, 16).toString(), + }, + [CHAIN_IDS.FANTOM]: { + domain: 'ftmscan.com', + subdomain: defaultEtherscanSubdomainPrefix, + networkId: parseInt(CHAIN_IDS.FANTOM, 16).toString(), + }, + [CHAIN_IDS.FANTOM_TESTNET]: { + domain: 'ftmscan.com', + subdomain: `${defaultEtherscanSubdomainPrefix}-testnet`, + networkId: parseInt(CHAIN_IDS.FANTOM_TESTNET, 16).toString(), + }, + [CHAIN_IDS.MOONBEAM]: { + domain: 'moonscan.io', + subdomain: `${defaultEtherscanSubdomainPrefix}-moonbeam`, + networkId: parseInt(CHAIN_IDS.MOONBEAM, 16).toString(), + }, + [CHAIN_IDS.MOONBEAM_TESTNET]: { + domain: 'moonscan.io', + subdomain: `${defaultEtherscanSubdomainPrefix}-moonbase`, + networkId: parseInt(CHAIN_IDS.MOONBEAM_TESTNET, 16).toString(), + }, + [CHAIN_IDS.MOONRIVER]: { + domain: 'moonscan.io', + subdomain: `${defaultEtherscanSubdomainPrefix}-moonriver`, + networkId: parseInt(CHAIN_IDS.MOONRIVER, 16).toString(), + }, +}; + /** * Hardforks are points in the chain where logic is changed significantly * enough where there is a fork and the new fork becomes the active chain. @@ -591,6 +690,13 @@ export const BUYABLE_CHAINS_MAP: { | typeof CHAIN_IDS.PALM | typeof CHAIN_IDS.HARMONY | typeof CHAIN_IDS.OPTIMISM_TESTNET + | typeof CHAIN_IDS.BSC_TESTNET + | typeof CHAIN_IDS.POLYGON_TESTNET + | typeof CHAIN_IDS.AVALANCHE_TESTNET + | typeof CHAIN_IDS.FANTOM_TESTNET + | typeof CHAIN_IDS.MOONBEAM + | typeof CHAIN_IDS.MOONBEAM_TESTNET + | typeof CHAIN_IDS.MOONRIVER >]: BuyableChainSettings; } = { [CHAIN_IDS.MAINNET]: {