diff --git a/src/plugins/etherscan/ZenmoneyManifest.xml b/src/plugins/etherscan/ZenmoneyManifest.xml index 139740d9d..01951976f 100755 --- a/src/plugins/etherscan/ZenmoneyManifest.xml +++ b/src/plugins/etherscan/ZenmoneyManifest.xml @@ -3,11 +3,11 @@ etherscan 15936 - Плагин для синхронизации ETH и BSC с помощью etherscan V2. - Для синхронизации выберите блокчейн, укажите ApiKey etherscan V2 и адреса кошельков. + Plugin for syncing Ethereum, BSC and Arbitrum One via Etherscan V2 API. + Select blockchains, provide your Etherscan V2 API key and wallet addresses. 1.0 - 4 + 5 true public diff --git a/src/plugins/etherscan/__tests__/converters/accounts/accounts.test.ts b/src/plugins/etherscan/__tests__/converters/accounts/accounts.test.ts index 2cf93eb79..84a9ddfbc 100644 --- a/src/plugins/etherscan/__tests__/converters/accounts/accounts.test.ts +++ b/src/plugins/etherscan/__tests__/converters/accounts/accounts.test.ts @@ -16,20 +16,20 @@ describe('convertAccounts', () => { ], [ { - id: '0xe61289f5dc092d685e6e918b6624e273b42b6730', + id: '1-0xe61289f5dc092d685e6e918b6624e273b42b6730', type: AccountType.checking, title: '0xe61289f5dc092d685e6e918b6624e273b42b6730', instrument: 'μETH', balance: 2000000, - syncIds: ['0xe61289f5dc092d685e6e918b6624e273b42b6730'] + syncIds: ['1-0xe61289f5dc092d685e6e918b6624e273b42b6730'] }, { - id: '0xe61289f5dc092d685e6e918b6624e273b42b6740', + id: '1-0xe61289f5dc092d685e6e918b6624e273b42b6740', type: AccountType.checking, title: '0xe61289f5dc092d685e6e918b6624e273b42b6740', instrument: 'μETH', balance: 0, - syncIds: ['0xe61289f5dc092d685e6e918b6624e273b42b6740'] + syncIds: ['1-0xe61289f5dc092d685e6e918b6624e273b42b6740'] } ] ] diff --git a/src/plugins/etherscan/__tests__/converters/ether-scrape.test.ts b/src/plugins/etherscan/__tests__/converters/ether-scrape.test.ts index 4c73e5857..a3f2a2238 100644 --- a/src/plugins/etherscan/__tests__/converters/ether-scrape.test.ts +++ b/src/plugins/etherscan/__tests__/converters/ether-scrape.test.ts @@ -8,6 +8,7 @@ describe('scrape', () => { const result = await scrape( { + chain: 1, preferences: preferencesMock, startBlock: 1, endBlock: 99999999, @@ -18,28 +19,28 @@ describe('scrape', () => { expect(result.accounts).toEqual([ { - id: '1', + id: '1-1', type: AccountType.checking, title: '1', instrument: 'μETH', balance: 2000000, - syncIds: ['1'] + syncIds: ['1-1'] }, { - id: '2', + id: '1-2', type: AccountType.checking, title: '2', instrument: 'μETH', balance: 10000000, - syncIds: ['2'] + syncIds: ['1-2'] }, { - id: '3', + id: '1-3', type: AccountType.checking, title: '3', instrument: 'μETH', balance: 0, - syncIds: ['3'] + syncIds: ['1-3'] } ]) @@ -49,7 +50,7 @@ describe('scrape', () => { date: new Date('2015-07-30T15:26:28.000Z'), movements: [{ id: '1', - account: { id: '1' }, + account: { id: '1-1' }, invoice: null, sum: -1000000, fee: -323 @@ -66,7 +67,7 @@ describe('scrape', () => { date: new Date('2015-07-30T15:26:28.000Z'), movements: [{ id: '2', - account: { id: '1' }, + account: { id: '1-1' }, invoice: null, sum: 2000000, fee: 0 @@ -84,13 +85,13 @@ describe('scrape', () => { movements: [ { id: '3', - account: { id: '1' }, + account: { id: '1-1' }, invoice: null, sum: -1000000, fee: -323 }, { id: '3', - account: { id: '2' }, + account: { id: '1-2' }, invoice: null, sum: 1000000, fee: 0 diff --git a/src/plugins/etherscan/common/config.ts b/src/plugins/etherscan/common/config.ts index 19443618a..de034ea54 100644 --- a/src/plugins/etherscan/common/config.ts +++ b/src/plugins/etherscan/common/config.ts @@ -1,7 +1,27 @@ export const ETHER_MAINNET = 1 export const BNB_MAINNET = 56 +export const ARBITRUM_ONE = 42161 -export const Instruments = { +export const Instruments: Record = { [ETHER_MAINNET]: 'μETH', - [BNB_MAINNET]: 'bnb' -} as const + [BNB_MAINNET]: 'bnb', + [ARBITRUM_ONE]: 'μETH' +} + +export const ChainNames: Record = { + [ETHER_MAINNET]: 'Ethereum', + [BNB_MAINNET]: 'BSC', + [ARBITRUM_ONE]: 'Arbitrum One' +} + +export function chainAccountId (chain: number, address: string): string { + return `${chain}-${address}` +} + +// Minimum timestamps (seconds) — network launch dates +// Requests before these dates return "No closest block found" +export const ChainMinTimestamp: Record = { + [ETHER_MAINNET]: 1438269973, // 2015-07-30 + [BNB_MAINNET]: 1598671449, // 2020-08-29 + [ARBITRUM_ONE]: 1630425600 // 2021-08-31 +} diff --git a/src/plugins/etherscan/common/converters.ts b/src/plugins/etherscan/common/converters.ts index 04fc636df..800010b7a 100644 --- a/src/plugins/etherscan/common/converters.ts +++ b/src/plugins/etherscan/common/converters.ts @@ -16,6 +16,13 @@ function canBeMergedAsTransfer (left: Transaction, right: Transaction): boolean return false } + // Cannot merge into transfer if both movements reference the same account + const leftId = 'id' in leftMovement.account ? leftMovement.account.id : null + const rightId = 'id' in rightMovement.account ? rightMovement.account.id : null + if (leftId != null && rightId != null && leftId === rightId) { + return false + } + if (leftMovement.sum < 0) { return rightMovement.sum > 0 } diff --git a/src/plugins/etherscan/common/index.ts b/src/plugins/etherscan/common/index.ts index d5aeb06a4..70d824337 100644 --- a/src/plugins/etherscan/common/index.ts +++ b/src/plugins/etherscan/common/index.ts @@ -4,7 +4,7 @@ import { delay } from '../../../common/utils' import { Preferences } from '../types' import _ from 'lodash' -import type { BlockNoResponse, Response } from './types' +import type { BlockNoResponse, Chain, Response } from './types' import { TemporaryError } from '../../../errors' const baseUrl = 'https://api.etherscan.io/v2/api' @@ -88,10 +88,11 @@ export async function fetch ( export async function fetchBlockNoByTime ( preferences: Preferences, + chain: Chain, { timestamp }: { timestamp: number } ): Promise { const response = await fetch({ - chainid: preferences.chain, + chainid: chain, module: 'block', action: 'getblocknobytime', closest: 'before', diff --git a/src/plugins/etherscan/common/merchants.ts b/src/plugins/etherscan/common/merchants.ts new file mode 100644 index 000000000..84f652aa1 --- /dev/null +++ b/src/plugins/etherscan/common/merchants.ts @@ -0,0 +1,86 @@ +import { ETHER_MAINNET, BNB_MAINNET, ARBITRUM_ONE } from './config' +import type { Chain } from './types' + +type ContractMap = Record + +const ETHEREUM_CONTRACTS: ContractMap = { + // DEX + '0x7a250d5630b4cf539739df2c5dacb4c659f2488d': 'Uniswap V2 Router', + '0xe592427a0aece92de3edee1f18e0157c05861564': 'Uniswap V3 Router', + '0xef1c6e67703c7bd7107eed8303fbe6ec2554bf6b': 'Uniswap Universal Router', + '0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f': 'SushiSwap Router', + + // Bridges + '0x3ee18b2214aff97000d974cf647e7c347e8fa585': 'Wormhole Bridge', + + // Lending + '0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9': 'Aave V2 Lending Pool', + '0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2': 'Aave V3 Pool', + + // System + '0x0000000000000000000000000000000000000000': 'System' +} + +const BSC_CONTRACTS: ContractMap = { + // DEX + '0x10ed43c718714eb63d5aa57b78b54704e256024e': 'PancakeSwap Router', + '0x1b02da8cb0d097eb8d57a175b88c7d8b47997506': 'SushiSwap Router', + + // Bridges + '0x3c2269811836af69497e5f486a85d7316753cf62': 'LayerZero Endpoint', + '0x8731d54e9d02c286767d56ac03e8037c07e01e98': 'Stargate Router', + + // Lending + '0xc11b1268c1a384e55c48c2391d8d480264a3a7f4': 'Venus Protocol', + + // System + '0x0000000000000000000000000000000000000000': 'System' +} + +const ARBITRUM_CONTRACTS: ContractMap = { + // DEX + '0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45': 'Uniswap V3 Router', + '0x1b02da8cb0d097eb8d57a175b88c7d8b47997506': 'SushiSwap Router', + '0x2df1adb252afb77fe762586fa6c116857f163df7': 'PancakeSwap Router', + + // Bridges + '0x0000000000000000000000000000000000000064': 'Arbitrum Bridge', + '0x3c2269811836af69497e5f486a85d7316753cf62': 'LayerZero Endpoint', + '0x8731d54e9d02c286767d56ac03e8037c07e01e98': 'Stargate Router', + + // Lending / Perps + '0x5f3b5dfeb7b28cdbd7faba78963ee202a494e2a2': 'Aave Lending Pool', + '0x32df62dc3aed2cd6224193052ce665dc18165841': 'Radiant Capital', + '0x489ee077994b6658eafa855c308275ead8097c4a': 'GMX Exchange', + + // System + '0x0000000000000000000000000000000000000000': 'System' +} + +const KNOWN_CONTRACTS: Record = { + [ETHER_MAINNET]: ETHEREUM_CONTRACTS, + [BNB_MAINNET]: BSC_CONTRACTS, + [ARBITRUM_ONE]: ARBITRUM_CONTRACTS +} + +export function normalizeMerchant ( + addr: string, + userAddress: string, + chain: Chain +): string { + if (addr == null || addr === '') return 'Unknown' + + const a = addr.toLowerCase() + const u = userAddress.toLowerCase() + + if (a === u) return 'Self' + + const contracts = KNOWN_CONTRACTS[chain] ?? {} + if (contracts[a] != null) return contracts[a] + + if (a.startsWith('0x') && a.length === 42) { + return `Contract ${addr.slice(0, 6)}…${addr.slice(-4)}` + } + + return addr +} diff --git a/src/plugins/etherscan/common/types.ts b/src/plugins/etherscan/common/types.ts index 51e8ce09e..e02df6439 100644 --- a/src/plugins/etherscan/common/types.ts +++ b/src/plugins/etherscan/common/types.ts @@ -1,4 +1,4 @@ -import { ETHER_MAINNET, BNB_MAINNET } from './config' +import { ETHER_MAINNET, BNB_MAINNET, ARBITRUM_ONE } from './config' export interface Response { status: string @@ -9,4 +9,4 @@ export interface BlockNoResponse extends Response { result: string } -export type Chain = typeof ETHER_MAINNET | typeof BNB_MAINNET +export type Chain = typeof ETHER_MAINNET | typeof BNB_MAINNET | typeof ARBITRUM_ONE diff --git a/src/plugins/etherscan/ether/api.ts b/src/plugins/etherscan/ether/api.ts index 8569d4772..1119a828e 100644 --- a/src/plugins/etherscan/ether/api.ts +++ b/src/plugins/etherscan/ether/api.ts @@ -1,5 +1,6 @@ import { fetch } from '../common' import { Preferences } from '../types' +import type { Chain } from '../common/types' import { AccountResponse, EthereumAccount, @@ -8,10 +9,11 @@ import { } from './types' export async function fetchAccounts ( - preferences: Preferences + preferences: Preferences, + chain: Chain ): Promise { const response = await fetch({ - chainid: preferences.chain, + chainid: chain, module: 'account', action: 'balancemulti', address: preferences.account, @@ -33,13 +35,14 @@ const PAGE_SIZE = 100 export async function fetchAccountTransactions ( preferences: Preferences, + chain: Chain, options: AccountTransactionsOptions ): Promise { const { account, startBlock, endBlock, page = 1 } = options try { const response = await fetch({ - chainid: preferences.chain, + chainid: chain, module: 'account', action: 'txlist', address: account, @@ -56,7 +59,7 @@ export async function fetchAccountTransactions ( if (response.result.length === PAGE_SIZE) { return [ ...transactions, - ...(await fetchAccountTransactions(preferences, { + ...(await fetchAccountTransactions(preferences, chain, { ...options, page: page + 1 })) diff --git a/src/plugins/etherscan/ether/converters.ts b/src/plugins/etherscan/ether/converters.ts index b93dc2635..f9fa7803d 100644 --- a/src/plugins/etherscan/ether/converters.ts +++ b/src/plugins/etherscan/ether/converters.ts @@ -3,7 +3,9 @@ import { AccountType, type Transaction } from '../../../types/zenmoney' -import { ETHER_MAINNET, Instruments } from '../common/config' +import { ETHER_MAINNET, Instruments, chainAccountId } from '../common/config' +import { normalizeMerchant } from '../common/merchants' +import type { Chain } from '../common/types' import type { EthereumAccount, EthereumTransaction } from './types' const MIN_MOVEMENT_SUM = 0.01 @@ -20,29 +22,34 @@ export function getTransactionFee (transaction: EthereumTransaction): number { function convertAccount ( { account, balance }: EthereumAccount, - instrument: string + instrument: string, + chain: Chain ): Account { + const id = chainAccountId(chain, account) return { - id: account, + id, type: AccountType.checking, title: account, instrument, balance: convertWeiToUETH(Number(balance)), - syncIds: [account] + syncIds: [id] } } export function convertAccounts ( accounts: EthereumAccount[], - instrument: string = Instruments[ETHER_MAINNET] + instrument: string = Instruments[ETHER_MAINNET], + chain: Chain = ETHER_MAINNET ): Account[] { - return accounts.map((account) => convertAccount(account, instrument)) + return accounts.map((account) => convertAccount(account, instrument, chain)) } export function convertTransaction ( account: string, - transaction: EthereumTransaction + transaction: EthereumTransaction, + chain: Chain = ETHER_MAINNET ): Transaction | null { + const accountId = chainAccountId(chain, account) const direction = transaction.from === account ? 'PAYMENT' : 'DEPOSIT' const targetAccount = direction === 'PAYMENT' ? transaction.to : transaction.from @@ -63,7 +70,7 @@ export function convertTransaction ( { id: transaction.hash, account: { - id: account + id: accountId }, invoice: null, sum: sign * operationValue, @@ -72,7 +79,7 @@ export function convertTransaction ( } ], merchant: { - fullTitle: targetAccount, + fullTitle: normalizeMerchant(targetAccount, account, chain), mcc: null, location: null }, @@ -82,10 +89,11 @@ export function convertTransaction ( export function convertTransactions ( account: string, - transactions: EthereumTransaction[] + transactions: EthereumTransaction[], + chain: Chain = ETHER_MAINNET ): Transaction[] { const list = transactions - .map((transaction) => convertTransaction(account, transaction)) + .map((transaction) => convertTransaction(account, transaction, chain)) .filter((transaction): transaction is Transaction => Boolean( transaction?.movements.some((movement) => diff --git a/src/plugins/etherscan/ether/index.ts b/src/plugins/etherscan/ether/index.ts index 13339519f..f9adfedbd 100644 --- a/src/plugins/etherscan/ether/index.ts +++ b/src/plugins/etherscan/ether/index.ts @@ -5,22 +5,22 @@ import { mergeTransferTransactions } from '../common/converters' import { convertAccounts, convertTransactions } from './converters' import { Instruments } from '../common/config' -export const scrape: Scrape = async ({ preferences, startBlock, endBlock }) => { +export const scrape: Scrape = async ({ chain, preferences, startBlock, endBlock }) => { const transactions: Transaction[] = [] - const accountsResponse = await fetchAccounts(preferences) + const accountsResponse = await fetchAccounts(preferences, chain) - const instrument = Instruments[preferences.chain] - const accounts: Account[] = convertAccounts(accountsResponse, instrument) + const instrument = Instruments[chain] + const accounts: Account[] = convertAccounts(accountsResponse, instrument, chain) - for (const account of accounts) { - const accountTransactions = await fetchAccountTransactions(preferences, { - account: account.id, + for (const raw of accountsResponse) { + const accountTransactions = await fetchAccountTransactions(preferences, chain, { + account: raw.account, startBlock, endBlock }) - transactions.push(...convertTransactions(account.id, accountTransactions)) + transactions.push(...convertTransactions(raw.account, accountTransactions, chain)) } return { diff --git a/src/plugins/etherscan/index.ts b/src/plugins/etherscan/index.ts index 2267ab926..160ea0636 100644 --- a/src/plugins/etherscan/index.ts +++ b/src/plugins/etherscan/index.ts @@ -1,10 +1,27 @@ -import type { ScrapeFunc } from '../../types/zenmoney' +import type { Account, Transaction, ScrapeFunc } from '../../types/zenmoney' import { fetchBlockNoByTime } from './common' import type { Preferences } from './types' +import type { Chain } from './common/types' import { scrape as scrapeEther } from './ether' import { scrape as scrapeTokens } from './tokens' -import { ETHER_MAINNET } from './common/config' +import { ETHER_MAINNET, BNB_MAINNET, ARBITRUM_ONE, ChainMinTimestamp } from './common/config' + +function parseChains (preferences: Preferences): Chain[] { + const chains: Chain[] = [] + + if (preferences.chainEthereum === true) chains.push(ETHER_MAINNET) + if (preferences.chainBsc === true) chains.push(BNB_MAINNET) + if (preferences.chainArbitrum === true) chains.push(ARBITRUM_ONE) + + // New checkboxes selected — use them + if (chains.length > 0) return chains + + // Legacy fallback: single chain from old config + if (preferences.chain != null) return [preferences.chain] + + return [ETHER_MAINNET] +} export const scrape: ScrapeFunc = async ({ fromDate, @@ -13,38 +30,46 @@ export const scrape: ScrapeFunc = async ({ isFirstRun, isInBackground }) => { - if (preferences.chain === undefined) { - preferences.chain = ETHER_MAINNET - } + const chains = parseChains(preferences) + + const allAccounts: Account[] = [] + const allTransactions: Transaction[] = [] + + for (const chain of chains) { + const minTs = ChainMinTimestamp[chain] ?? 0 + const fromTs = Math.max(Math.floor(fromDate.valueOf() / 1000), minTs) + const toTs = Math.max(Math.floor((toDate ?? new Date()).valueOf() / 1000), minTs) - const [startBlock, endBlock] = await Promise.all([ - fetchBlockNoByTime(preferences, { - timestamp: Math.floor(fromDate.valueOf() / 1000) - }), - fetchBlockNoByTime(preferences, { - timestamp: Math.floor((toDate ?? new Date()).valueOf() / 1000) - }) - ]) - - const [ether, tokens] = await Promise.all([ - scrapeEther({ - preferences, - startBlock, - endBlock, - isFirstRun, - isInBackground - }), - scrapeTokens({ - preferences, - startBlock, - endBlock, - isFirstRun, - isInBackground - }) - ]) + const [startBlock, endBlock] = await Promise.all([ + fetchBlockNoByTime(preferences, chain, { timestamp: fromTs }), + fetchBlockNoByTime(preferences, chain, { timestamp: toTs }) + ]) + + const [ether, tokens] = await Promise.all([ + scrapeEther({ + chain, + preferences, + startBlock, + endBlock, + isFirstRun, + isInBackground + }), + scrapeTokens({ + chain, + preferences, + startBlock, + endBlock, + isFirstRun, + isInBackground + }) + ]) + + allAccounts.push(...ether.accounts, ...tokens.accounts) + allTransactions.push(...ether.transactions, ...tokens.transactions) + } return { - accounts: [...ether.accounts, ...tokens.accounts], - transactions: [...ether.transactions, ...tokens.transactions] + accounts: allAccounts, + transactions: allTransactions } } diff --git a/src/plugins/etherscan/mocks/index.ts b/src/plugins/etherscan/mocks/index.ts index f2d856bf7..34fd5a38a 100644 --- a/src/plugins/etherscan/mocks/index.ts +++ b/src/plugins/etherscan/mocks/index.ts @@ -8,6 +8,9 @@ import { Preferences } from '../types' export const preferencesMock: Preferences = { chain: 1, + chainEthereum: true, + chainBsc: false, + chainArbitrum: false, apiKey: 'API_KEY', account: '1,2' } diff --git a/src/plugins/etherscan/preferences.xml b/src/plugins/etherscan/preferences.xml index 766647db0..0aaa99093 100755 --- a/src/plugins/etherscan/preferences.xml +++ b/src/plugins/etherscan/preferences.xml @@ -1,46 +1,53 @@ - + + diff --git a/src/plugins/etherscan/tokens/config.ts b/src/plugins/etherscan/tokens/config.ts index 8e0ec9bd8..a606d01c9 100644 --- a/src/plugins/etherscan/tokens/config.ts +++ b/src/plugins/etherscan/tokens/config.ts @@ -1,4 +1,4 @@ -import { ETHER_MAINNET, BNB_MAINNET } from '../common/config' +import { ETHER_MAINNET, BNB_MAINNET, ARBITRUM_ONE } from '../common/config' export type TokenConfig = Record @@ -10,10 +10,11 @@ export interface TokenConfigItem { } export const generateTokenAddress = ( + chain: number, address: string, token: TokenConfigItem ): string => { - return `${address}-${token.contractAddress}` + return `${chain}-${address}-${token.contractAddress}` } export const SUPPORTED_TOKENS: TokenConfig = { @@ -50,5 +51,19 @@ export const SUPPORTED_TOKENS: TokenConfig = { instrument: 'USDT', convertBalance: (value: number) => value / 1000000000 / 1000000000 } + ], + [ARBITRUM_ONE]: [ + { + title: 'USDT', + contractAddress: '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9', + instrument: 'USDT', + convertBalance: (value: number) => value / 1000000 + }, + { + title: 'USDC', + contractAddress: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + instrument: 'USDT', + convertBalance: (value: number) => value / 1000000 + } ] } diff --git a/src/plugins/etherscan/tokens/converters.ts b/src/plugins/etherscan/tokens/converters.ts index 5e0b93185..c36bd91f4 100644 --- a/src/plugins/etherscan/tokens/converters.ts +++ b/src/plugins/etherscan/tokens/converters.ts @@ -6,7 +6,8 @@ import { import { generateTokenAddress, SUPPORTED_TOKENS } from './config' import type { TokenAccount, TokenTransaction } from './types' import type { Chain } from '../common/types' -import { ETHER_MAINNET } from '../common/config' +import { ETHER_MAINNET, chainAccountId } from '../common/config' +import { normalizeMerchant } from '../common/merchants' import { getTransactionFee } from '../ether/converters' import type { EthereumTransaction } from '../ether/types' @@ -21,7 +22,7 @@ function convertAccount (account: TokenAccount, chain: Chain): Account | null { return null } - const id = generateTokenAddress(account.id, token) + const id = generateTokenAddress(chain, account.id, token) return { id, @@ -78,7 +79,7 @@ export function convertTransaction ( { id: hash, account: { - id: generateTokenAddress(account.id, token) + id: generateTokenAddress(chain, account.id, token) }, invoice: null, sum: sign * operationValue, @@ -86,7 +87,7 @@ export function convertTransaction ( } ], merchant: { - fullTitle: targetAccount, + fullTitle: normalizeMerchant(targetAccount, account.id, chain), mcc: null, location: null }, @@ -103,7 +104,7 @@ export function convertTransaction ( { id: hash + '_fee', account: { - id: account.id + id: chainAccountId(chain, account.id) }, invoice: null, sum: @@ -113,7 +114,7 @@ export function convertTransaction ( } ], merchant: { - fullTitle: targetAccount, + fullTitle: normalizeMerchant(targetAccount, account.id, chain), mcc: null, location: null }, diff --git a/src/plugins/etherscan/tokens/erc20.ts b/src/plugins/etherscan/tokens/erc20.ts index a4263ad6c..03c2ee88f 100644 --- a/src/plugins/etherscan/tokens/erc20.ts +++ b/src/plugins/etherscan/tokens/erc20.ts @@ -1,6 +1,7 @@ import flatten from 'lodash/flatten' import { fetch } from '../common' import { type Preferences } from '../types' +import type { Chain } from '../common/types' import { AccountResponse, @@ -12,12 +13,13 @@ import { SUPPORTED_TOKENS } from './config' export async function fetchAddressTokens ( preferences: Preferences, + chain: Chain, address: string ): Promise { const result = await Promise.all( - SUPPORTED_TOKENS[preferences.chain].map(async (token) => { + SUPPORTED_TOKENS[chain].map(async (token) => { const response = await fetch({ - chainid: preferences.chain, + chainid: chain, module: 'account', action: 'tokenbalance', contractaddress: token.contractAddress, @@ -44,13 +46,14 @@ export async function fetchAddressTokens ( /* Эндпоинт etherscan для получения инфы про все токены — платный. Поэтому обходим тут все поддерживаемые токены по каждому адресу отдельно */ export async function fetchAccounts ( - preferences: Preferences + preferences: Preferences, + chain: Chain ): Promise { const accounts = preferences.account.split(',') const result = await Promise.all( accounts.map(async (address: string) => { - const tokensAccounts = await fetchAddressTokens(preferences, address) + const tokensAccounts = await fetchAddressTokens(preferences, chain, address) return tokensAccounts }) @@ -69,6 +72,7 @@ interface AccountTransactionsOptions { export async function fetchAccountTransactions ( preferences: Preferences, + chain: Chain, account: TokenAccount, options: AccountTransactionsOptions ): Promise { @@ -76,7 +80,7 @@ export async function fetchAccountTransactions ( try { const response = await fetch({ - chainid: preferences.chain, + chainid: chain, module: 'account', action: 'tokentx', contractaddress: account.contractAddress, @@ -94,7 +98,7 @@ export async function fetchAccountTransactions ( if (response.result.length === PAGE_SIZE) { return [ ...transactions, - ...(await fetchAccountTransactions(preferences, account, { + ...(await fetchAccountTransactions(preferences, chain, account, { ...options, page: page + 1 })) diff --git a/src/plugins/etherscan/tokens/index.ts b/src/plugins/etherscan/tokens/index.ts index 34bbcc1aa..dec023278 100644 --- a/src/plugins/etherscan/tokens/index.ts +++ b/src/plugins/etherscan/tokens/index.ts @@ -5,13 +5,14 @@ import { Scrape } from '../types' import { convertAccounts, convertTransactions } from './converters' import { fetchAccounts, fetchAccountTransactions } from './erc20' -export const scrape: Scrape = async ({ preferences, startBlock, endBlock }) => { +export const scrape: Scrape = async ({ chain, preferences, startBlock, endBlock }) => { const transactions: Transaction[] = [] - const [accounts] = await Promise.all([fetchAccounts(preferences)]) + const [accounts] = await Promise.all([fetchAccounts(preferences, chain)]) for (const account of accounts) { const accountTransactions = await fetchAccountTransactions( preferences, + chain, account, { startBlock, @@ -20,12 +21,12 @@ export const scrape: Scrape = async ({ preferences, startBlock, endBlock }) => { ) transactions.push( - ...convertTransactions(account, accountTransactions, preferences.chain) + ...convertTransactions(account, accountTransactions, chain) ) } return { - accounts: convertAccounts(accounts, preferences.chain), + accounts: convertAccounts(accounts, chain), transactions: mergeTransferTransactions(transactions) } } diff --git a/src/plugins/etherscan/types.ts b/src/plugins/etherscan/types.ts index bc73929ed..bd78f307c 100644 --- a/src/plugins/etherscan/types.ts +++ b/src/plugins/etherscan/types.ts @@ -2,12 +2,18 @@ import type { ScrapeFunc } from '../../types/zenmoney' import type { Chain } from './common/types' export interface Preferences { - chain: Chain + // Legacy single-chain (old configs) + chain?: Chain + // New multi-chain checkboxes + chainEthereum?: boolean + chainBsc?: boolean + chainArbitrum?: boolean apiKey: string account: string } export type Scrape = (args: { + chain: Chain startBlock: number endBlock: number preferences: Preferences