1import * as _ from 'lodash-es' 2import { DateTime } from 'luxon' 3import { EtherscanProvider, formatEther } from 'ethers' 8const latestBlock = 'latest' 10// Normalize Alchemy alchemy_getAssetTransfers response to Etherscan-compatible format 11const normalizeAlchemyTx = (tx: {blockNum: string, hash: string, from: string, to: string, rawContract: {value: string}, metadata: {blockTimestamp: string}}) => { 12 const {blockNum, hash, from, to, rawContract, metadata} = tx 14 blockNumber: String(parseInt(blockNum, 16)), 18 value: String(BigInt(rawContract.value)), // hex wei -> decimal wei string 19 timestamp: Math.floor(new Date(metadata.blockTimestamp).getTime() / 1000), 21 confirmations: '1', // Alchemy only returns confirmed txs 25// Normalize Etherscan response format 26const normalizeEtherscanTx = (tx: Record<string, string>) => { 27 const normalized: Record<string, string | number> = {...tx} 28 _.each(['contractAddress', 'to'] as const, (key) => { 29 if (normalized[key] === '') delete normalized[key] 31 if (normalized.contractAddress || normalized.creates) return null // skip contract creation 32 delete normalized.type 33 normalized.nonce ||= '12345' 34 if (normalized.timeStamp) normalized.timestamp = parseInt(String(normalized.timeStamp)) 38// endblock default must exceed chain block height (Arbitrum ~445M) or V2 API returns empty results 39const fetchEtherscanTxs = async ({provider, address, startBlock, endBlock}: {provider: EtherscanProvider, address: string, startBlock?: number, endBlock?: number}) => { 40 const results = await Promise.all(_.map(['txlist', 'txlistinternal'] as const, async (action) => { 42 return await provider.fetch('account', { 44 startblock: startBlock ?? 0, 45 endblock: endBlock ?? 9999999999, 49 (err as any).dumpDebugH = {...(err as any).dumpDebugH, address, startBlock, endBlock, action} 53 return _.compact(_.flatten(results).map((tx) => normalizeEtherscanTx(tx as Record<string, string>))) 56const fetchAlchemyTxs = async ({provider, address, startBlock, endBlock}: {provider: {send: (method: string, params: unknown[]) => Promise<{transfers?: unknown[]}>}, address: string, startBlock?: number, endBlock?: number}) => { 57 // Note: 'internal' category only supported for ETH/MATIC, so we use 'external' only 58 // Use 'latest' for toBlock to support L2s with high block numbers (Arbitrum: 400M+) 59 // Query both toAddress and fromAddress to catch self-transfers (where from === to) 61 category: ['external'], 62 fromBlock: `0x${Number(startBlock ?? 0).toString(16)}`, 63 toBlock: endBlock ? `0x${Number(endBlock).toString(16)}` : latestBlock, 67 const [toResult, fromResult] = await Promise.all([ 68 provider.send('alchemy_getAssetTransfers', [{...baseParams, toAddress: address}]), 69 provider.send('alchemy_getAssetTransfers', [{...baseParams, fromAddress: address}]), 71 const allTransfers = [...(toResult.transfers || []), ...(fromResult.transfers || [])] as Parameters<typeof normalizeAlchemyTx>[0][] 72 return _.uniqBy(_.map(allTransfers, normalizeAlchemyTx), 'hash') 74 (err as any).dumpDebugH = {...(err as any).dumpDebugH, address, startBlock, endBlock} 79type EthTx = {to?: string, isError: string, confirmations: string, from: string, value: string, timestamp: number, hash: string, blockNumber: string} 80export const getEtherTxs = async ({address, startBlock, endBlock, ethersProvider: provider}: {address: string, startBlock?: number, endBlock?: number, ethersProvider: EtherscanProvider | {send: (method: string, params: unknown[]) => Promise<{transfers?: unknown[]}>}}) => { 81 const isEtherscan = provider instanceof EtherscanProvider 82 let retA: EthTx[] = isEtherscan 83 ? await fetchEtherscanTxs({provider, address, startBlock, endBlock}) as EthTx[] 84 : await fetchAlchemyTxs({provider, address, startBlock, endBlock}) as EthTx[] 86 retA = _.filter(retA, (trans: EthTx) => { 87 const {to, isError, confirmations} = trans 88 const aaa = _.toLower(to) != _.toLower(address) 89 const bbb = isError != '0' 90 const ccc = _.parseInt(confirmations) == 0 92 if (aaa) return false // outgoing tx 102 return _.map(retA, decorEthTx) 105export const decorTsDate = <T extends {timestamp: number}>(tx: T) => { 106 const {timestamp} = tx 107 const tsDt = DateTime.fromSeconds(timestamp) 111export const decorEthTx = (tx: EthTx) => { 112 const {from, value, timestamp, hash, ...restH} = tx 114 const amtEthF = _.toNumber(formatEther(value)) 115 return decorTsDate({...tx, amtEthF, eth_gwei})