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