Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional incoming transactions support #14219

Merged
merged 3 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 19 additions & 25 deletions app/scripts/controllers/incoming-transactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ import {
TRANSACTION_TYPES,
TRANSACTION_STATUSES,
} 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';

const fetchWithTimeout = getFetchWithTimeout();

Expand Down Expand Up @@ -45,15 +41,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 {
Expand All @@ -72,13 +62,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);
Expand Down Expand Up @@ -155,7 +148,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 {
Expand Down Expand Up @@ -216,12 +212,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) {
Expand Down Expand Up @@ -284,7 +278,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,
Expand Down
34 changes: 14 additions & 20 deletions app/scripts/controllers/incoming-transactions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -139,20 +140,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 () {
Expand Down
106 changes: 106 additions & 0 deletions shared/constants/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,17 +200,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;

/**
Expand Down Expand Up @@ -535,6 +542,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.
Expand Down Expand Up @@ -584,6 +683,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]: {
Expand Down