From 73882ffbb6d3784cbece6677751deacb640b7140 Mon Sep 17 00:00:00 2001 From: tgmichel Date: Mon, 28 Mar 2022 09:48:54 +0200 Subject: [PATCH] Additional incoming transactions support --- .../controllers/incoming-transactions.js | 52 ++++----- .../controllers/incoming-transactions.test.js | 38 +++---- shared/constants/network.js | 105 ++++++++++++++++++ 3 files changed, 138 insertions(+), 57 deletions(-) diff --git a/app/scripts/controllers/incoming-transactions.js b/app/scripts/controllers/incoming-transactions.js index c56509314a0e..0089d5d345ae 100644 --- a/app/scripts/controllers/incoming-transactions.js +++ b/app/scripts/controllers/incoming-transactions.js @@ -9,15 +9,7 @@ import { TRANSACTION_TYPES, TRANSACTION_STATUSES, } from '../../../shared/constants/transaction'; -import { - CHAIN_ID_TO_NETWORK_ID_MAP, - CHAIN_ID_TO_TYPE_MAP, - GOERLI_CHAIN_ID, - KOVAN_CHAIN_ID, - MAINNET_CHAIN_ID, - RINKEBY_CHAIN_ID, - ROPSTEN_CHAIN_ID, -} from '../../../shared/constants/network'; +import { ETHERSCAN_SUPPORTED_NETWORKS } from '../../../shared/constants/network'; import { SECOND } from '../../../shared/constants/time'; const fetchWithTimeout = getFetchWithTimeout(SECOND * 30); @@ -50,17 +42,9 @@ const fetchWithTimeout = getFetchWithTimeout(SECOND * 30); * 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 = [ - GOERLI_CHAIN_ID, - KOVAN_CHAIN_ID, - MAINNET_CHAIN_ID, - RINKEBY_CHAIN_ID, - ROPSTEN_CHAIN_ID, -]; - export default class IncomingTransactionsController { constructor(opts = {}) { const { @@ -79,15 +63,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: { - [GOERLI_CHAIN_ID]: null, - [KOVAN_CHAIN_ID]: null, - [MAINNET_CHAIN_ID]: null, - [RINKEBY_CHAIN_ID]: null, - [ROPSTEN_CHAIN_ID]: null, - }, + incomingTxLastFetchedBlockByChainId, ...opts.initState, }; this.store = new ObservableStore(initState); @@ -164,7 +149,10 @@ export default class IncomingTransactionsController { */ async _update(address, newBlockNumberDec) { const chainId = this.getCurrentChainId(); - if (!etherscanSupportedNetworks.includes(chainId) || !address) { + if ( + !Object.hasOwnProperty.call(ETHERSCAN_SUPPORTED_NETWORKS, chainId) || + !address + ) { return; } try { @@ -225,12 +213,10 @@ export default class IncomingTransactionsController { * @returns {TransactionMeta[]} */ async _getNewIncomingTransactions(address, fromBlock, chainId) { - const etherscanSubdomain = - chainId === MAINNET_CHAIN_ID - ? '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) { @@ -293,7 +279,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 525bf1fdf4f8..231d69996b53 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, GOERLI_CHAIN_ID, KOVAN_CHAIN_ID, MAINNET_CHAIN_ID, @@ -40,13 +40,12 @@ const PREPOPULATED_BLOCKS_BY_NETWORK = { [RINKEBY_CHAIN_ID]: 5, [ROPSTEN_CHAIN_ID]: 4, }; -const EMPTY_BLOCKS_BY_NETWORK = { - [GOERLI_CHAIN_ID]: null, - [KOVAN_CHAIN_ID]: null, - [MAINNET_CHAIN_ID]: null, - [RINKEBY_CHAIN_ID]: null, - [ROPSTEN_CHAIN_ID]: null, -}; +const EMPTY_BLOCKS_BY_NETWORK = Object.keys( + ETHERSCAN_SUPPORTED_NETWORKS, +).reduce((network, chainId) => { + network[chainId] = null; + return network; +}, {}); function getEmptyInitState() { return { @@ -147,22 +146,13 @@ const getFakeEtherscanTransaction = ({ }; function nockEtherscanApiForAllChains(mockResponse) { - for (const chainId of [ - GOERLI_CHAIN_ID, - KOVAN_CHAIN_ID, - MAINNET_CHAIN_ID, - RINKEBY_CHAIN_ID, - ROPSTEN_CHAIN_ID, - 'undefined', - ]) { - nock( - `https://api${ - chainId === MAINNET_CHAIN_ID ? '' : `-${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.js b/shared/constants/network.js index 844533a0c61d..3ca2cc7e1597 100644 --- a/shared/constants/network.js +++ b/shared/constants/network.js @@ -20,12 +20,19 @@ export const GOERLI_CHAIN_ID = '0x5'; export const KOVAN_CHAIN_ID = '0x2a'; export const LOCALHOST_CHAIN_ID = '0x539'; export const BSC_CHAIN_ID = '0x38'; +export const BSC_TESTNET_CHAIN_ID = '0x61'; export const OPTIMISM_CHAIN_ID = '0xa'; export const OPTIMISM_TESTNET_CHAIN_ID = '0x45'; export const POLYGON_CHAIN_ID = '0x89'; +export const POLYGON_TESTNET_CHAIN_ID = '0x13881'; export const AVALANCHE_CHAIN_ID = '0xa86a'; +export const AVALANCHE_TESTNET_CHAIN_ID = '0xa869'; export const FANTOM_CHAIN_ID = '0xfa'; +export const FANTOM_TESTNET_CHAIN_ID = '0xfa2'; export const CELO_CHAIN_ID = '0xa4ec'; +export const MOONBEAM_CHAIN_ID = '0x504'; +export const MOONBEAM_TESTNET_CHAIN_ID = '0x507'; +export const MOONRIVER_CHAIN_ID = '0x505'; /** * The largest possible chain ID we can handle. @@ -254,3 +261,101 @@ export const BUYABLE_CHAINS_MAP = { }, }, }; + +const defaultEtherscanDomain = 'etherscan.io'; +const defaultEtherscanSubdomainPrefix = 'api'; +/** + * Map of all Etherscan supported networks. + */ +export const ETHERSCAN_SUPPORTED_NETWORKS = { + [GOERLI_CHAIN_ID]: { + domain: defaultEtherscanDomain, + subdomain: `${defaultEtherscanSubdomainPrefix}-${CHAIN_ID_TO_TYPE_MAP[GOERLI_CHAIN_ID]}`, + networkId: CHAIN_ID_TO_NETWORK_ID_MAP[GOERLI_CHAIN_ID], + }, + [KOVAN_CHAIN_ID]: { + domain: defaultEtherscanDomain, + subdomain: `${defaultEtherscanSubdomainPrefix}-${CHAIN_ID_TO_TYPE_MAP[KOVAN_CHAIN_ID]}`, + networkId: CHAIN_ID_TO_NETWORK_ID_MAP[KOVAN_CHAIN_ID], + }, + [MAINNET_CHAIN_ID]: { + domain: defaultEtherscanDomain, + subdomain: defaultEtherscanSubdomainPrefix, + networkId: CHAIN_ID_TO_NETWORK_ID_MAP[MAINNET_CHAIN_ID], + }, + [RINKEBY_CHAIN_ID]: { + domain: defaultEtherscanDomain, + subdomain: `${defaultEtherscanSubdomainPrefix}-${CHAIN_ID_TO_TYPE_MAP[RINKEBY_CHAIN_ID]}`, + networkId: CHAIN_ID_TO_NETWORK_ID_MAP[RINKEBY_CHAIN_ID], + }, + [ROPSTEN_CHAIN_ID]: { + domain: defaultEtherscanDomain, + subdomain: `${defaultEtherscanSubdomainPrefix}-${CHAIN_ID_TO_TYPE_MAP[ROPSTEN_CHAIN_ID]}`, + networkId: CHAIN_ID_TO_NETWORK_ID_MAP[ROPSTEN_CHAIN_ID], + }, + [BSC_CHAIN_ID]: { + domain: 'bscscan.com', + subdomain: defaultEtherscanSubdomainPrefix, + networkId: parseInt(BSC_CHAIN_ID, 16).toString(), + }, + [BSC_TESTNET_CHAIN_ID]: { + domain: 'bscscan.com', + subdomain: `${defaultEtherscanSubdomainPrefix}-testnet`, + networkId: parseInt(BSC_TESTNET_CHAIN_ID, 16).toString(), + }, + [OPTIMISM_CHAIN_ID]: { + domain: defaultEtherscanDomain, + subdomain: `${defaultEtherscanSubdomainPrefix}-optimistic`, + networkId: parseInt(OPTIMISM_CHAIN_ID, 16).toString(), + }, + [OPTIMISM_TESTNET_CHAIN_ID]: { + domain: defaultEtherscanDomain, + subdomain: `${defaultEtherscanSubdomainPrefix}-kovan-optimistic`, + networkId: parseInt(OPTIMISM_TESTNET_CHAIN_ID, 16).toString(), + }, + [POLYGON_CHAIN_ID]: { + domain: 'polygonscan.com', + subdomain: defaultEtherscanSubdomainPrefix, + networkId: parseInt(POLYGON_CHAIN_ID, 16).toString(), + }, + [POLYGON_TESTNET_CHAIN_ID]: { + domain: 'polygonscan.com', + subdomain: `${defaultEtherscanSubdomainPrefix}-mumbai`, + networkId: parseInt(POLYGON_TESTNET_CHAIN_ID, 16).toString(), + }, + [AVALANCHE_CHAIN_ID]: { + domain: 'snowtrace.io', + subdomain: defaultEtherscanSubdomainPrefix, + networkId: parseInt(AVALANCHE_CHAIN_ID, 16).toString(), + }, + [AVALANCHE_TESTNET_CHAIN_ID]: { + domain: 'snowtrace.io', + subdomain: `${defaultEtherscanSubdomainPrefix}-testnet`, + networkId: parseInt(AVALANCHE_TESTNET_CHAIN_ID, 16).toString(), + }, + [FANTOM_CHAIN_ID]: { + domain: 'ftmscan.com', + subdomain: defaultEtherscanSubdomainPrefix, + networkId: parseInt(FANTOM_CHAIN_ID, 16).toString(), + }, + [FANTOM_TESTNET_CHAIN_ID]: { + domain: 'ftmscan.com', + subdomain: `${defaultEtherscanSubdomainPrefix}-testnet`, + networkId: parseInt(FANTOM_TESTNET_CHAIN_ID, 16).toString(), + }, + [MOONBEAM_CHAIN_ID]: { + domain: 'moonscan.io', + subdomain: `${defaultEtherscanSubdomainPrefix}-moonbeam`, + networkId: parseInt(MOONBEAM_CHAIN_ID, 16).toString(), + }, + [MOONBEAM_TESTNET_CHAIN_ID]: { + domain: 'moonscan.io', + subdomain: `${defaultEtherscanSubdomainPrefix}-moonbase`, + networkId: parseInt(MOONBEAM_TESTNET_CHAIN_ID, 16).toString(), + }, + [MOONRIVER_CHAIN_ID]: { + domain: 'moonscan.io', + subdomain: `${defaultEtherscanSubdomainPrefix}-moonriver`, + networkId: parseInt(MOONRIVER_CHAIN_ID, 16).toString(), + }, +};