1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Additional incoming transactions support (#14219)

This commit is contained in:
tgmichel 2023-02-14 19:35:42 +01:00 committed by GitHub
parent 3ec042fa68
commit 3a00b5ab54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 136 additions and 45 deletions

View File

@ -9,11 +9,7 @@ import {
TransactionType, TransactionType,
TransactionStatus, TransactionStatus,
} from '../../../shared/constants/transaction'; } from '../../../shared/constants/transaction';
import { import { ETHERSCAN_SUPPORTED_NETWORKS } from '../../../shared/constants/network';
CHAIN_IDS,
CHAIN_ID_TO_NETWORK_ID_MAP,
CHAIN_ID_TO_TYPE_MAP,
} from '../../../shared/constants/network';
import { bnToHex } from '../../../shared/modules/conversion.utils'; import { bnToHex } from '../../../shared/modules/conversion.utils';
const fetchWithTimeout = getFetchWithTimeout(); 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 * 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 * 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 * Note that only Etherscan-compatible networks are supported. We will not attempt to retrieve incoming transactions
* attempt to retrieve incoming transactions on any custom RPC endpoints. * on non-compatible custom RPC endpoints.
*/ */
const etherscanSupportedNetworks = [
CHAIN_IDS.GOERLI,
CHAIN_IDS.MAINNET,
CHAIN_IDS.SEPOLIA,
];
export default class IncomingTransactionsController { export default class IncomingTransactionsController {
constructor(opts = {}) { constructor(opts = {}) {
const { const {
@ -75,13 +65,16 @@ export default class IncomingTransactionsController {
await this._update(selectedAddress, newBlockNumberDec); await this._update(selectedAddress, newBlockNumberDec);
}; };
const incomingTxLastFetchedBlockByChainId = Object.keys(
ETHERSCAN_SUPPORTED_NETWORKS,
).reduce((network, chainId) => {
network[chainId] = null;
return network;
}, {});
const initState = { const initState = {
incomingTransactions: {}, incomingTransactions: {},
incomingTxLastFetchedBlockByChainId: { incomingTxLastFetchedBlockByChainId,
[CHAIN_IDS.GOERLI]: null,
[CHAIN_IDS.MAINNET]: null,
[CHAIN_IDS.SEPOLIA]: null,
},
...opts.initState, ...opts.initState,
}; };
this.store = new ObservableStore(initState); this.store = new ObservableStore(initState);
@ -171,7 +164,7 @@ export default class IncomingTransactionsController {
const { completedOnboarding } = this.onboardingController.store.getState(); const { completedOnboarding } = this.onboardingController.store.getState();
const chainId = this.getCurrentChainId(); const chainId = this.getCurrentChainId();
if ( if (
!etherscanSupportedNetworks.includes(chainId) || !Object.hasOwnProperty.call(ETHERSCAN_SUPPORTED_NETWORKS, chainId) ||
!address || !address ||
!completedOnboarding !completedOnboarding
) { ) {
@ -235,12 +228,10 @@ export default class IncomingTransactionsController {
* @returns {TransactionMeta[]} * @returns {TransactionMeta[]}
*/ */
async _getNewIncomingTransactions(address, fromBlock, chainId) { async _getNewIncomingTransactions(address, fromBlock, chainId) {
const etherscanSubdomain = const etherscanDomain = ETHERSCAN_SUPPORTED_NETWORKS[chainId].domain;
chainId === CHAIN_IDS.MAINNET const etherscanSubdomain = ETHERSCAN_SUPPORTED_NETWORKS[chainId].subdomain;
? 'api'
: `api-${CHAIN_ID_TO_TYPE_MAP[chainId]}`;
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`; let url = `${apiUrl}/api?module=account&action=txlist&address=${address}&tag=latest&page=1`;
if (fromBlock) { if (fromBlock) {
@ -303,7 +294,7 @@ export default class IncomingTransactionsController {
blockNumber: etherscanTransaction.blockNumber, blockNumber: etherscanTransaction.blockNumber,
id: createId(), id: createId(),
chainId, chainId,
metamaskNetworkId: CHAIN_ID_TO_NETWORK_ID_MAP[chainId], metamaskNetworkId: ETHERSCAN_SUPPORTED_NETWORKS[chainId].networkId,
status, status,
time, time,
txParams, txParams,

View File

@ -6,7 +6,7 @@ import { cloneDeep } from 'lodash';
import waitUntilCalled from '../../../test/lib/wait-until-called'; import waitUntilCalled from '../../../test/lib/wait-until-called';
import { import {
CHAIN_ID_TO_TYPE_MAP, ETHERSCAN_SUPPORTED_NETWORKS,
CHAIN_IDS, CHAIN_IDS,
NETWORK_TYPES, NETWORK_TYPES,
NETWORK_IDS, NETWORK_IDS,
@ -34,11 +34,12 @@ const PREPOPULATED_BLOCKS_BY_NETWORK = {
[CHAIN_IDS.MAINNET]: 3, [CHAIN_IDS.MAINNET]: 3,
[CHAIN_IDS.SEPOLIA]: 6, [CHAIN_IDS.SEPOLIA]: 6,
}; };
const EMPTY_BLOCKS_BY_NETWORK = { const EMPTY_BLOCKS_BY_NETWORK = Object.keys(
[CHAIN_IDS.GOERLI]: null, ETHERSCAN_SUPPORTED_NETWORKS,
[CHAIN_IDS.MAINNET]: null, ).reduce((network, chainId) => {
[CHAIN_IDS.SEPOLIA]: null, network[chainId] = null;
}; return network;
}, {});
function getEmptyInitState() { function getEmptyInitState() {
return { return {
@ -150,20 +151,13 @@ const getFakeEtherscanTransaction = ({
}; };
function nockEtherscanApiForAllChains(mockResponse) { function nockEtherscanApiForAllChains(mockResponse) {
for (const chainId of [ Object.values(ETHERSCAN_SUPPORTED_NETWORKS).forEach(
CHAIN_IDS.GOERLI, ({ domain, subdomain }) => {
CHAIN_IDS.MAINNET, nock(`https://${domain}.${subdomain}`)
CHAIN_IDS.SEPOLIA, .get(/api.+/u)
'undefined', .reply(200, JSON.stringify(mockResponse));
]) { },
nock( );
`https://api${
chainId === CHAIN_IDS.MAINNET ? '' : `-${CHAIN_ID_TO_TYPE_MAP[chainId]}`
}.etherscan.io`,
)
.get(/api.+/u)
.reply(200, JSON.stringify(mockResponse));
}
} }
describe('IncomingTransactionsController', function () { describe('IncomingTransactionsController', function () {

View File

@ -201,17 +201,24 @@ export const CHAIN_IDS = {
GOERLI: '0x5', GOERLI: '0x5',
LOCALHOST: '0x539', LOCALHOST: '0x539',
BSC: '0x38', BSC: '0x38',
BSC_TESTNET: '0x61',
OPTIMISM: '0xa', OPTIMISM: '0xa',
OPTIMISM_TESTNET: '0x1a4', OPTIMISM_TESTNET: '0x1a4',
POLYGON: '0x89', POLYGON: '0x89',
POLYGON_TESTNET: '0x13881',
AVALANCHE: '0xa86a', AVALANCHE: '0xa86a',
AVALANCHE_TESTNET: '0xa869',
FANTOM: '0xfa', FANTOM: '0xfa',
FANTOM_TESTNET: '0xfa2',
CELO: '0xa4ec', CELO: '0xa4ec',
ARBITRUM: '0xa4b1', ARBITRUM: '0xa4b1',
HARMONY: '0x63564c40', HARMONY: '0x63564c40',
PALM: '0x2a15c308d', PALM: '0x2a15c308d',
SEPOLIA: '0xaa36a7', SEPOLIA: '0xaa36a7',
AURORA: '0x4e454152', AURORA: '0x4e454152',
MOONBEAM: '0x504',
MOONBEAM_TESTNET: '0x507',
MOONRIVER: '0x505',
} as const; } as const;
/** /**
@ -542,6 +549,98 @@ export const NATIVE_CURRENCY_TOKEN_IMAGE_MAP = {
export const INFURA_BLOCKED_KEY = 'countryBlocked'; 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 * 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. * 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.PALM
| typeof CHAIN_IDS.HARMONY | typeof CHAIN_IDS.HARMONY
| typeof CHAIN_IDS.OPTIMISM_TESTNET | 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; >]: BuyableChainSettings;
} = { } = {
[CHAIN_IDS.MAINNET]: { [CHAIN_IDS.MAINNET]: {