Development Artifact Cleanup: ✅ BROTHER_NODE REORGANIZATION: Moved development test node to appropriate location - dev/test-nodes/brother_node/: Moved from root directory for better organization - Contains development configuration, test logs, and test chain data - No impact on production systems - purely development/testing artifact ✅ DEVELOPMENT ARTIFACTS IDENTIFIED: - Chain ID: aitbc-brother-chain (test/development chain) - Ports: 8010 (P2P) and 8011 (RPC) - different from production - Environment: .env file with test configuration - Logs: rpc.log and node.log from development testing session (March 15, 2026) ✅ ROOT DIRECTORY CLEANUP: Removed development clutter from production directory - brother_node/ moved to dev/test-nodes/brother_node/ - Root directory now contains only production-ready components - Development artifacts properly organized in dev/ subdirectory DIRECTORY STRUCTURE IMPROVEMENT: 📁 dev/test-nodes/: Development and testing node configurations 🏗️ Root Directory: Clean production structure with only essential components 🧪 Development Isolation: Test environments separated from production BENEFITS: ✅ Clean Production Directory: No development artifacts in root ✅ Better Organization: Development nodes grouped in dev/ subdirectory ✅ Clear Separation: Production vs development environments clearly distinguished ✅ Maintainability: Easier to identify and manage development components RESULT: Successfully moved brother_node development artifact to dev/test-nodes/ subdirectory, cleaning up the root directory while preserving development testing environment for future use.
772 lines
32 KiB
JavaScript
Executable File
772 lines
32 KiB
JavaScript
Executable File
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.Web3Provider = void 0;
|
|
exports.calcTransfersDiff = calcTransfersDiff;
|
|
const index_ts_1 = require("../abi/index.js");
|
|
const index_ts_2 = require("../index.js");
|
|
const tx_ts_1 = require("../tx.js");
|
|
const utils_ts_1 = require("../utils.js");
|
|
/*
|
|
Methods to fetch list of transactions from any ETH node RPC.
|
|
It should be easy. However, this is sparta^W ethereum, so, prepare to suffer.
|
|
|
|
The network is not directly called: `ArchiveNodeProvider#rpc` calls `Web3Provider`.
|
|
|
|
- There is no simple & fast API inside nodes, all external API create their own namespace for this
|
|
- API is different between nodes: erigon uses streaming, other nodes use pagination
|
|
- Recently, Erigon have been also adding pagination
|
|
- For token transactions: download block headers, look at bloom filter, download affected blocks
|
|
- There is a good `getLogs` API for contracts, but nothing for ETH transfers
|
|
- `trace_filter` is slow: it not only finds the transaction, but also executes them
|
|
- It's good that it allows to get internal transactions
|
|
- The whole thing could be 10x simpler if there was an event in logs for ETH transfer
|
|
- For most cases, we only need to see last transactions and know blocks of last txs, which is 20x faster
|
|
- This creates a lot of requests to node (2 per tx, 1 per block, and some more depends on block range limits)
|
|
|
|
Recommended software:
|
|
|
|
- eth-nodes-for-rent are bad, because of their limits and timeouts
|
|
- erigon nodes are fast, taking ~15 seconds per batch
|
|
- reth has 100-block limit for trace_filter, requiring 190k requests just get transactions
|
|
*/
|
|
// Utils
|
|
const ethNum = (n) => `0x${!n ? '0' : n.toString(16).replace(/^0+/, '')}`;
|
|
const ERC_TRANSFER = (0, index_ts_1.events)(index_ts_1.ERC20).Transfer;
|
|
const WETH_DEPOSIT = (0, index_ts_1.events)(index_ts_1.WETH).Deposit;
|
|
const WETH_WITHDRAW = (0, index_ts_1.events)(index_ts_1.WETH).Withdrawal;
|
|
const ERC721_TRANSFER = (0, index_ts_1.events)(index_ts_1.ERC721).Transfer;
|
|
const ERC1155_SINGLE = (0, index_ts_1.events)(index_ts_1.ERC1155).TransferSingle;
|
|
const ERC1155_BATCH = (0, index_ts_1.events)(index_ts_1.ERC1155).TransferBatch;
|
|
const ERC165 = [
|
|
// function supportsInterface(bytes4 interfaceID) external view returns (bool);
|
|
{
|
|
type: 'function',
|
|
name: 'supportsInterface',
|
|
inputs: [{ name: 'interfaceID', type: 'bytes4' }],
|
|
outputs: [{ type: 'bool' }],
|
|
},
|
|
];
|
|
const CONTRACT_CAPABILITIES = {
|
|
erc165: '0x01ffc9a7',
|
|
erc165_check: '0xffffffff',
|
|
erc20: '0x36372b07',
|
|
erc721: '0x80ac58cd',
|
|
erc721_metadata: '0x5b5e139f',
|
|
erc721_enumerable: '0x780e9d63',
|
|
erc1155: '0xd9b67a26',
|
|
erc1155_tokenreceiver: '0x4e2312e0',
|
|
erc1155_metadata: '0x0e89341c',
|
|
};
|
|
function group(items, s) {
|
|
let res = {};
|
|
for (let i of items) {
|
|
const key = typeof s === 'function' ? s(i) : i[s];
|
|
if (!res[key])
|
|
res[key] = [];
|
|
res[key].push(i);
|
|
}
|
|
return res;
|
|
}
|
|
function fixBlock(block) {
|
|
block.timestamp = Number(block.timestamp) * 1000;
|
|
block.size = Number(block.size);
|
|
if (block.number && block.number !== null)
|
|
block.number = Number(block.number);
|
|
for (const i of [
|
|
'baseFeePerGas',
|
|
'difficulty',
|
|
'gasLimit',
|
|
'gasUsed',
|
|
'totalDifficulty',
|
|
]) {
|
|
if (block[i] && block[i] !== null)
|
|
block[i] = BigInt(block[i]);
|
|
}
|
|
}
|
|
function fixAction(action, opts = {}) {
|
|
action.action.value = BigInt(action.action.value);
|
|
action.action.gas = BigInt(action.action.gas);
|
|
action.result.gasUsed = BigInt(action.result.gasUsed);
|
|
if (opts.txCallback)
|
|
opts.txCallback(action.transactionHash);
|
|
if (opts.blockCallback)
|
|
opts.blockCallback(action.blockNumber);
|
|
}
|
|
// Fixes type of network response inplace
|
|
function fixLog(log, opts = {}) {
|
|
log.blockNumber = Number(log.blockNumber);
|
|
log.transactionIndex = Number(log.transactionIndex);
|
|
log.logIndex = Number(log.logIndex);
|
|
if (opts.txCallback)
|
|
opts.txCallback(log.transactionHash);
|
|
if (opts.blockCallback)
|
|
opts.blockCallback(log.blockNumber);
|
|
if (opts.contractCallback)
|
|
opts.contractCallback(log.address);
|
|
return log;
|
|
}
|
|
function fixTxInfo(info) {
|
|
for (const i of ['blockNumber', 'type', 'transactionIndex'])
|
|
info[i] = Number(info[i]);
|
|
for (const i of [
|
|
'nonce',
|
|
'r',
|
|
's',
|
|
'chainId',
|
|
'v',
|
|
'gas',
|
|
'maxPriorityFeePerGas',
|
|
'maxFeePerGas',
|
|
'value',
|
|
'gasPrice',
|
|
'maxFeePerBlobGas',
|
|
]) {
|
|
if (info[i] !== undefined && info[i] !== null)
|
|
info[i] = BigInt(info[i]);
|
|
}
|
|
return info;
|
|
}
|
|
function fixTxReceipt(receipt) {
|
|
for (const i of ['blockNumber', 'type', 'transactionIndex', 'status'])
|
|
receipt[i] = Number(receipt[i]);
|
|
for (const i of [
|
|
'gasUsed',
|
|
'cumulativeGasUsed',
|
|
'effectiveGasPrice',
|
|
'blobGasPrice',
|
|
'blobGasUsed',
|
|
]) {
|
|
if (receipt[i] !== undefined)
|
|
receipt[i] = BigInt(receipt[i]);
|
|
}
|
|
for (const log of receipt.logs)
|
|
fixLog(log);
|
|
return receipt;
|
|
}
|
|
function validateCallbacks(opts) {
|
|
for (const i of ['txCallback', 'blockCallback', 'contractCallback']) {
|
|
if (opts[i] !== undefined && typeof opts[i] !== 'function')
|
|
throw new Error(`validateCallbacks: ${i} should be function`);
|
|
}
|
|
}
|
|
function validatePagination(opts) {
|
|
for (const i of ['fromBlock', 'toBlock']) {
|
|
if (opts[i] === undefined || Number.isSafeInteger(opts[i]))
|
|
continue;
|
|
throw new Error(`validatePagination: wrong field ${i}=${opts[i]}. Should be integer or undefined`);
|
|
}
|
|
}
|
|
function validateTraceOpts(opts) {
|
|
validatePagination(opts);
|
|
for (const i of ['perRequest', 'limitTrace']) {
|
|
if (opts[i] === undefined || Number.isSafeInteger(opts[i]))
|
|
continue;
|
|
throw new Error(`validateTraceOpts: wrong field ${i}=${opts[i]}. Should be integer or undefined`);
|
|
}
|
|
if (opts.limitTrace !== undefined) {
|
|
if (opts.fromBlock === undefined || opts.toBlock === undefined)
|
|
throw new Error('validateTraceOpts: fromBlock/toBlock required if limitTrace is present');
|
|
}
|
|
validateCallbacks(opts);
|
|
}
|
|
function validateLogOpts(opts) {
|
|
validatePagination(opts);
|
|
for (const i of ['limitLogs']) {
|
|
if (opts[i] === undefined || Number.isSafeInteger(opts[i]))
|
|
continue;
|
|
throw new Error(`validateLogOpts: wrong field ${i}=${opts[i]}. Should be integer or undefined`);
|
|
}
|
|
if (opts.limitLogs !== undefined) {
|
|
if (opts.fromBlock === undefined || opts.toBlock === undefined)
|
|
throw new Error('validateLogOpts: fromBlock/toBlock required if limitLogs is present');
|
|
}
|
|
validateCallbacks(opts);
|
|
}
|
|
// Promise.all for objects, undefined if error
|
|
async function wait(obj) {
|
|
const keys = Object.keys(obj);
|
|
const p = await Promise.allSettled(Object.values(obj));
|
|
const res = p.map((r, i) => [keys[i], r.status === 'fulfilled' ? r.value : undefined]);
|
|
return Object.fromEntries(res);
|
|
}
|
|
const isReverted = (e) => e instanceof Error && e.message.toLowerCase().includes('revert');
|
|
/**
|
|
* Transaction-related code around Web3Provider.
|
|
* High-level methods are `height`, `unspent`, `transfers`, `allowances` and `tokenBalances`.
|
|
*
|
|
* Low-level methods are `blockInfo`, `internalTransactions`, `ethLogs`, `tokenTransfers`, `wethTransfers`,
|
|
* `tokenInfo` and `txInfo`.
|
|
*/
|
|
class Web3Provider {
|
|
constructor(rpc) {
|
|
this.rpc = rpc;
|
|
}
|
|
call(method, ...args) {
|
|
return this.rpc.call(method, ...args);
|
|
}
|
|
ethCall(args, tag = 'latest') {
|
|
return this.rpc.call('eth_call', args, tag);
|
|
}
|
|
async estimateGas(args, tag = 'latest') {
|
|
return (0, utils_ts_1.hexToNumber)(await this.rpc.call('eth_estimateGas', args, tag));
|
|
}
|
|
// Timestamp is available only inside blocks
|
|
async blockInfo(block) {
|
|
const res = await this.call('eth_getBlockByNumber', ethNum(block), false);
|
|
fixBlock(res);
|
|
return res;
|
|
}
|
|
async unspent(address) {
|
|
let [balance, nonce] = await Promise.all([
|
|
this.call('eth_getBalance', address, 'latest'),
|
|
this.call('eth_getTransactionCount', address, 'latest'),
|
|
]);
|
|
balance = BigInt(balance);
|
|
nonce = BigInt(nonce);
|
|
return {
|
|
symbol: 'ETH',
|
|
decimals: utils_ts_1.amounts.ETH_PRECISION,
|
|
balance,
|
|
nonce,
|
|
// Note: account can be active even if nonce!==0!
|
|
active: balance > 0 || nonce !== 0,
|
|
};
|
|
}
|
|
async height() {
|
|
return Number.parseInt(await this.call('eth_blockNumber'));
|
|
}
|
|
async traceFilterSingle(address, opts = {}) {
|
|
const res = await this.call('trace_filter', {
|
|
fromBlock: ethNum(opts.fromBlock),
|
|
toBlock: ethNum(opts.toBlock),
|
|
toAddress: [address],
|
|
fromAddress: [address],
|
|
});
|
|
for (const action of res)
|
|
fixAction(action, opts);
|
|
return res;
|
|
}
|
|
async internalTransactions(address, opts = {}) {
|
|
if (typeof address !== 'string')
|
|
throw new Error('internalTransactions: wrong address');
|
|
validateTraceOpts(opts);
|
|
// For reth
|
|
if (opts.limitTrace) {
|
|
const promises = [];
|
|
for (let i = opts.fromBlock; i <= opts.toBlock; i += opts.limitTrace)
|
|
promises.push(this.traceFilterSingle(address, { fromBlock: i, toBlock: i + opts.limitTrace }));
|
|
const out = [];
|
|
for (const i of await Promise.all(promises))
|
|
out.push(...i);
|
|
return out;
|
|
}
|
|
let lastBlock = opts.fromBlock || 0;
|
|
const perBlock = {};
|
|
const out = [];
|
|
for (;;) {
|
|
const params = {
|
|
fromBlock: ethNum(lastBlock),
|
|
toAddress: [address],
|
|
fromAddress: [address],
|
|
after: perBlock[lastBlock] || 0, // we cannot just store after, since fromBlock changes to last block
|
|
};
|
|
if (opts.toBlock !== undefined)
|
|
params.toBlock = ethNum(opts.toBlock);
|
|
if (opts.perRequest !== undefined)
|
|
params.count = opts.perRequest;
|
|
const res = await this.call('trace_filter', params);
|
|
if (!res.length)
|
|
break;
|
|
for (const action of res) {
|
|
fixAction(action, opts);
|
|
if (perBlock[action.blockNumber] === undefined)
|
|
perBlock[action.blockNumber] = 0;
|
|
perBlock[action.blockNumber]++;
|
|
out.push(action);
|
|
lastBlock = Math.max(lastBlock, action.blockNumber);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
async contractCapabilities(address, capabilities = {}) {
|
|
const all = { ...CONTRACT_CAPABILITIES, ...capabilities };
|
|
let c = (0, index_ts_1.createContract)(ERC165, this, address);
|
|
const keys = Object.keys(all);
|
|
// TODO: what about revert?
|
|
// if reverted -> all capabilities disabled
|
|
try {
|
|
const promises = await Promise.all(Object.values(all).map((i) => c.supportsInterface.call(utils_ts_1.ethHex.decode(i))));
|
|
const res = Object.fromEntries(keys.map((k, i) => [k, promises[i]]));
|
|
// if somehow there is same method, but it doesn't support erc165, then it is different method!
|
|
// erc165_check if sailsafe when there is method that always returns true
|
|
if (!res.erc165 || res.erc165_check)
|
|
for (const k in res)
|
|
res[k] = false;
|
|
return res;
|
|
}
|
|
catch (e) {
|
|
// If execution reverted: contract doesn't support ERC165
|
|
if (isReverted(e))
|
|
return Object.fromEntries(keys.map((k) => [k, false]));
|
|
throw e;
|
|
}
|
|
}
|
|
async ethLogsSingle(topics, opts) {
|
|
const req = { topics, fromBlock: ethNum(opts.fromBlock || 0) };
|
|
if (opts.toBlock !== undefined)
|
|
req.toBlock = ethNum(opts.toBlock);
|
|
const res = await this.call('eth_getLogs', req);
|
|
return res.map((i) => fixLog(i, opts));
|
|
}
|
|
async ethLogs(topics, opts = {}) {
|
|
validateLogOpts(opts);
|
|
const fromBlock = opts.fromBlock || 0;
|
|
if (!('limitLogs' in opts))
|
|
return this.ethLogsSingle(topics, opts);
|
|
const promises = [];
|
|
for (let i = fromBlock; i <= opts.toBlock; i += opts.limitLogs)
|
|
promises.push(this.ethLogsSingle(topics, { fromBlock: i, toBlock: i + opts.limitLogs }));
|
|
const out = [];
|
|
for (const i of await Promise.all(promises))
|
|
out.push(...i);
|
|
return out;
|
|
}
|
|
// NOTE: this is very low-level methods that return parts used for .transfers method,
|
|
// you will need to decode data yourself.
|
|
async tokenTransfers(address, opts = {}) {
|
|
if (typeof address !== 'string')
|
|
throw new Error('tokenTransfers: wrong address');
|
|
validateLogOpts(opts);
|
|
// If we want incoming and outgoing token transfers we need to call both
|
|
return await Promise.all([
|
|
this.ethLogs(ERC_TRANSFER.topics({ from: address, to: null, value: null }), opts), // From
|
|
this.ethLogs(ERC_TRANSFER.topics({ from: null, to: address, value: null }), opts), // To
|
|
]);
|
|
}
|
|
async wethTransfers(address, opts = {}) {
|
|
if (typeof address !== 'string')
|
|
throw new Error('tokenTransfers: wrong address');
|
|
validateLogOpts(opts);
|
|
const depositTopic = WETH_DEPOSIT.topics({ dst: address, wad: null });
|
|
const withdrawTopic = WETH_WITHDRAW.topics({ src: address, wad: null });
|
|
// OR query
|
|
return await Promise.all([
|
|
this.ethLogs([[depositTopic[0], withdrawTopic[0]], depositTopic[1]], opts),
|
|
]);
|
|
}
|
|
async erc1155Transfers(address, opts = {}) {
|
|
if (typeof address !== 'string')
|
|
throw new Error('tokenTransfers: wrong address');
|
|
validateLogOpts(opts);
|
|
return await Promise.all([
|
|
// Single
|
|
this.ethLogs(ERC1155_SINGLE.topics({ operator: null, from: address, to: null, id: null, value: null }), opts),
|
|
this.ethLogs(ERC1155_SINGLE.topics({ operator: null, from: null, to: address, id: null, value: null }), opts),
|
|
// Batch
|
|
this.ethLogs(ERC1155_BATCH.topics({ operator: null, from: address, to: null, ids: null, values: null }), opts),
|
|
this.ethLogs(ERC1155_BATCH.topics({ operator: null, from: null, to: address, ids: null, values: null }), opts),
|
|
]);
|
|
}
|
|
async txInfo(txHash, opts = {}) {
|
|
let [info, receipt] = await Promise.all([
|
|
this.call('eth_getTransactionByHash', txHash),
|
|
this.call('eth_getTransactionReceipt', txHash),
|
|
]);
|
|
info = fixTxInfo(info);
|
|
receipt = fixTxReceipt(receipt);
|
|
const type = Object.keys(tx_ts_1.TxVersions)[info.type];
|
|
// This is not strictly neccessary, but allows to store tx info in very compact format and remove unneccessary fields
|
|
// Also, there is additional validation that node returned actual with correct hash/sender and not corrupted stuff.
|
|
let raw = undefined;
|
|
try {
|
|
const rawData = {
|
|
nonce: info.nonce,
|
|
gasLimit: info.gas,
|
|
to: info.to === null ? '0x' : info.to,
|
|
value: info.value,
|
|
data: info.input,
|
|
r: info.r,
|
|
s: info.s,
|
|
yParity: Number(info.v),
|
|
chainId: info.chainId,
|
|
};
|
|
if (info.accessList)
|
|
rawData.accessList = info.accessList;
|
|
if (info.maxFeePerBlobGas)
|
|
rawData.maxFeePerBlobGas = info.maxFeePerBlobGas;
|
|
if (info.blobVersionedHashes)
|
|
rawData.blobVersionedHashes = info.blobVersionedHashes;
|
|
if (info.maxFeePerGas) {
|
|
rawData.maxFeePerGas = info.maxFeePerGas;
|
|
rawData.maxPriorityFeePerGas = info.maxPriorityFeePerGas;
|
|
}
|
|
else if (info.gasPrice)
|
|
rawData.gasPrice = info.gasPrice;
|
|
if (type === 'legacy')
|
|
Object.assign(rawData, tx_ts_1.legacySig.encode({ v: info.v, r: info.r, s: info.s }));
|
|
const tx = new index_ts_2.Transaction(type, rawData, false, true);
|
|
if (tx.recoverSender().address.toLowerCase() !== info.from.toLowerCase())
|
|
throw new Error('txInfo: wrong sender');
|
|
if (receipt.transactionHash !== `0x${tx.hash}`)
|
|
throw new Error('txInfo: wrong hash');
|
|
raw = tx.toHex();
|
|
}
|
|
catch (err) {
|
|
// This can crash if something wrong with our parser or limits, so
|
|
// we have option to make network code to work even if rebuilding is crashed
|
|
if (!opts.ignoreTxRebuildErrors)
|
|
throw err;
|
|
}
|
|
if (opts.blockCallback && info.blockNumber !== null)
|
|
opts.blockCallback(info.blockNumber);
|
|
return { type, info, receipt, raw };
|
|
}
|
|
async tokenInfo(contract) {
|
|
const c = (0, index_ts_1.createContract)(index_ts_1.ERC20, this, contract);
|
|
const t = await wait({
|
|
code: this.call('eth_getCode', contract, 'latest'),
|
|
capabilities: this.contractCapabilities(contract),
|
|
// We call all stuff at same time to reduce latency (should be done in single req if batched)
|
|
name: c.name.call(), // ERC-20 (optional), ERC-721 (metada)
|
|
symbol: c.symbol.call(), // ERC-20 (optional), ERC-721 (metadata)
|
|
decimals: c.decimals.call(), // ERC-20 (optional), ERC-721 (enumarable)
|
|
totalSupply: c.totalSupply.call(), // ERC-20 (required), ERC-721
|
|
});
|
|
// No code, probably self-destructed
|
|
if (t.code === '0x')
|
|
return { contract, error: 'not contract or destructed' };
|
|
if (t.capabilities && t.capabilities.erc1155) {
|
|
// All metadata is inside URI per tokenId to outside network stuff (maybe ipfs), so nothing to do here.
|
|
return { contract, abi: 'ERC1155' };
|
|
}
|
|
if (t.capabilities && t.capabilities.erc721) {
|
|
const res = { contract, abi: 'ERC721' };
|
|
if (t.capabilities.erc721_metadata) {
|
|
if (t.name === undefined)
|
|
return { contract, error: 'ERC721+Metadata without name' };
|
|
if (t.symbol === undefined)
|
|
return { contract, error: 'ERC721+Metadata without symbol' };
|
|
Object.assign(res, { name: t.name, symbol: t.symbol, metadata: true });
|
|
}
|
|
if (t.capabilities.erc721_enumerable) {
|
|
if (t.totalSupply === undefined)
|
|
return { contract, error: 'ERC721+Enumerable without totalSupply' };
|
|
Object.assign(res, { totalSupply: t.totalSupply, enumerable: true });
|
|
}
|
|
return res;
|
|
}
|
|
if (t.totalSupply === undefined)
|
|
return { contract, error: 'not ERC20 token' }; // If there is no totalSupply, it is not ERC20!
|
|
return {
|
|
contract,
|
|
abi: 'ERC20',
|
|
name: t.name,
|
|
symbol: t.symbol,
|
|
totalSupply: t.totalSupply,
|
|
decimals: t.decimals ? Number(t.decimals) : undefined,
|
|
};
|
|
}
|
|
async tokenBalanceSingle(address, token, tokenIds) {
|
|
if ('error' in token)
|
|
return token;
|
|
if (token.abi === 'ERC20') {
|
|
const balance = await (0, index_ts_1.createContract)(index_ts_1.ERC20, this, token.contract).balanceOf.call(address);
|
|
if (tokenIds && (tokenIds.size > 1 || Array.from(tokenIds)[0] !== 1n)) {
|
|
return { contract: token.contract, error: 'unexpected tokenIds for ERC20' };
|
|
}
|
|
return new Map([[1n, balance]]);
|
|
}
|
|
else if (token.abi === 'ERC721') {
|
|
const c = (0, index_ts_1.createContract)(index_ts_1.ERC721, this, token.contract);
|
|
const balance = await c.balanceOf.call(address);
|
|
if (!token.enumerable) {
|
|
if (!tokenIds) {
|
|
if (!balance)
|
|
return new Map(); // no tokens owned by user
|
|
return {
|
|
contract: token.contract,
|
|
error: 'erc721 contract not enumerable, but owner has ' + balance + ' tokens',
|
|
};
|
|
}
|
|
// if we cannot enumerate, but has tokenIds, we can check if tokenIds still owned by account
|
|
const ids = Array.from(tokenIds);
|
|
const owners = await Promise.all(ids.map((i) => c.ownerOf.call(i)));
|
|
return new Map(ids.map((i, j) => [i, owners[j].toLowerCase() === address.toLowerCase() ? 1n : 0n]));
|
|
}
|
|
// if we can fetch tokenIds: always do this
|
|
const p = [];
|
|
for (let i = 0; i < balance; i++)
|
|
p.push(c.tokenOfOwnerByIndex.call({ owner: address, index: BigInt(i) }));
|
|
tokenIds = new Set(await Promise.all(p));
|
|
const ids = Array.from(tokenIds);
|
|
return new Map(ids.map((i) => [i, 1n]));
|
|
}
|
|
else if (token.abi === 'ERC1155') {
|
|
// This is pretty bad standard, because it doesn't allow enumeration of tokenIds for owner
|
|
if (!tokenIds)
|
|
return { contract: token.contract, error: 'cannot fetch erc1155 without tokenIds' };
|
|
const c = (0, index_ts_1.createContract)(index_ts_1.ERC1155, this, token.contract);
|
|
const ids = Array.from(tokenIds);
|
|
const balances = await c.balanceOfBatch.call({ accounts: ids.map((_) => address), ids });
|
|
const res = new Map(ids.map((i, j) => [i, balances[j]]));
|
|
return res;
|
|
}
|
|
throw new Error('unknown token type');
|
|
}
|
|
async tokenURI(token, tokenId) {
|
|
if (typeof token === 'string')
|
|
token = await this.tokenInfo(token);
|
|
if ('error' in token)
|
|
return token;
|
|
if (token.abi === 'ERC721') {
|
|
const c = (0, index_ts_1.createContract)(index_ts_1.ERC721, this, token.contract);
|
|
if (!token.metadata)
|
|
return { contract: token.contract, error: 'erc721 without metadata' };
|
|
return c.tokenURI.call(tokenId);
|
|
}
|
|
else if (token.abi === 'ERC1155') {
|
|
const c = (0, index_ts_1.createContract)(index_ts_1.ERC1155, this, token.contract);
|
|
return c.uri.call(tokenId);
|
|
}
|
|
return { contract: token.contract, error: 'not supported token type' };
|
|
}
|
|
async tokenBalances(address, tokens, tokenIds) {
|
|
// New API requires data from tokenInfo (which is slow and should be cached).
|
|
// But for compat with old API, we do tokenInfo call if contract address (as string) presented
|
|
const _tokens = await Promise.all(tokens.map((i) => (typeof i === 'string' ? this.tokenInfo(i) : i)));
|
|
const balances = await Promise.all(_tokens.map((i) => this.tokenBalanceSingle(address, i, tokenIds && tokenIds[i.contract])));
|
|
return Object.fromEntries(_tokens.map((i, j) => [i.contract, balances[j]]));
|
|
}
|
|
decodeTokenTransfer(token, log) {
|
|
if ('error' in token)
|
|
return;
|
|
if (token.abi === 'ERC20') {
|
|
try {
|
|
const decoded = ERC_TRANSFER.decode(log.topics, log.data);
|
|
return {
|
|
...token,
|
|
contract: log.address,
|
|
to: decoded.to,
|
|
from: decoded.from,
|
|
tokens: new Map([[1n, decoded.value]]),
|
|
};
|
|
}
|
|
catch (e) { }
|
|
// Weth doesn't issue Transfer event on Deposit/Withdrawal
|
|
// NOTE: we don't filter for WETH_CONTRACT here in case of other contracts with similar API or different networks
|
|
try {
|
|
const decoded = WETH_DEPOSIT.decode(log.topics, log.data);
|
|
return {
|
|
...token,
|
|
contract: log.address,
|
|
from: log.address,
|
|
to: decoded.dst,
|
|
tokens: new Map([[1n, decoded.wad]]),
|
|
};
|
|
}
|
|
catch (e) { }
|
|
try {
|
|
const decoded = WETH_WITHDRAW.decode(log.topics, log.data);
|
|
return {
|
|
...token,
|
|
contract: log.address,
|
|
from: decoded.src,
|
|
to: log.address,
|
|
tokens: new Map([[1n, decoded.wad]]),
|
|
};
|
|
}
|
|
catch (e) { }
|
|
}
|
|
else if (token.abi === 'ERC721') {
|
|
try {
|
|
const decoded = ERC721_TRANSFER.decode(log.topics, log.data);
|
|
return {
|
|
...token,
|
|
from: decoded.from,
|
|
to: decoded.to,
|
|
tokens: new Map([[decoded.tokenId, 1n]]),
|
|
};
|
|
}
|
|
catch (e) { }
|
|
}
|
|
else if (token.abi === 'ERC1155') {
|
|
try {
|
|
const decoded = ERC1155_SINGLE.decode(log.topics, log.data);
|
|
return {
|
|
...token,
|
|
from: decoded.from,
|
|
to: decoded.to,
|
|
tokens: new Map([[decoded.id, decoded.value]]),
|
|
};
|
|
}
|
|
catch (e) { }
|
|
try {
|
|
const decoded = ERC1155_BATCH.decode(log.topics, log.data);
|
|
return {
|
|
...token,
|
|
from: decoded.from,
|
|
to: decoded.to,
|
|
tokens: new Map(decoded.ids.map((i, j) => [i, decoded.values[j]])),
|
|
};
|
|
}
|
|
catch (e) { }
|
|
}
|
|
return; // unknown token type
|
|
}
|
|
// We want to get all transactions related to address, that means:
|
|
// - from or to equals address in tx
|
|
// - any internal tx from or to equals address in tx
|
|
// - any erc20 token transfer which hash address in src or dst
|
|
// - erc721 is exactly same function signature as erc20 (need to detect after getting transactions)
|
|
// - erc1155: from/to + single/batch
|
|
// trace_filter (web3) returns information only for first two cases, most of explorers returns only first case.
|
|
async transfers(address, opts = {}) {
|
|
const txCache = {};
|
|
const blockCache = {};
|
|
const tokenCache = {};
|
|
const _opts = {
|
|
...opts,
|
|
txCallback: (txHash) => {
|
|
if (txCache[txHash])
|
|
return;
|
|
txCache[txHash] = this.txInfo(txHash, opts);
|
|
},
|
|
blockCallback: (blockNumber) => {
|
|
if (blockCache[blockNumber])
|
|
return;
|
|
blockCache[blockNumber] = this.blockInfo(blockNumber);
|
|
},
|
|
contractCallback: (address) => {
|
|
if (tokenCache[address])
|
|
return;
|
|
tokenCache[address] = this.tokenInfo(address);
|
|
},
|
|
};
|
|
if (!_opts.fromBlock)
|
|
_opts.fromBlock = 0;
|
|
// This runs in parallel and executes callbacks
|
|
// Note, we ignore logs and weth, but they will call callbacks and fetch related
|
|
const [actions, _logs, _weth] = await Promise.all([
|
|
this.internalTransactions(address, _opts),
|
|
this.tokenTransfers(address, _opts),
|
|
this.wethTransfers(address, _opts),
|
|
this.erc1155Transfers(address, _opts),
|
|
]);
|
|
const mapCache = async (cache) => {
|
|
const keys = Object.keys(cache);
|
|
const values = await Promise.all(Object.values(cache));
|
|
return Object.fromEntries(values.map((v, i) => [keys[i], v]));
|
|
};
|
|
// it is ok to do this sequentially, since promises already started and probably resolved at this point
|
|
const blocks = await mapCache(blockCache);
|
|
const tx = await mapCache(txCache);
|
|
const tokens = await mapCache(tokenCache);
|
|
const actionPerTx = group(actions, 'transactionHash');
|
|
// Sort transactions by [blockNumber, transactionIndex]
|
|
const _txHashes = Object.entries(tx).map(([k, v]) => [k, v.info.blockNumber, v.info.transactionIndex]);
|
|
_txHashes.sort((a, b) => (a[1] !== b[1] ? a[1] - b[1] : a[2] - b[2]));
|
|
const txHashes = _txHashes.map((i) => i[0]);
|
|
return txHashes.map((txHash) => {
|
|
const { info, receipt } = tx[txHash];
|
|
const actions = actionPerTx[txHash];
|
|
const block = info.blockNumber !== null ? blocks[info.blockNumber] : undefined;
|
|
const transfers = [];
|
|
if (actions) {
|
|
for (const a of actions)
|
|
transfers.push({ from: a.action.from, to: a.action.to, value: a.action.value });
|
|
}
|
|
else {
|
|
// If we have action, it was call to contract and transfer from tx is already added
|
|
transfers.push({ from: info.from, to: info.to, value: info.value });
|
|
}
|
|
// cumulativeGasUsed includes all transactions before that in block, so useless. gasUsed is correct even for internal transactions
|
|
transfers.push({ from: info.from, value: receipt.gasUsed * receipt.effectiveGasPrice });
|
|
// Tokens
|
|
const tokenTransfers = [];
|
|
for (const log of receipt.logs) {
|
|
const tokenInfo = tokens[log.address];
|
|
if (!tokenInfo)
|
|
continue;
|
|
const tt = this.decodeTokenTransfer(tokenInfo, log);
|
|
if (tt)
|
|
tokenTransfers.push(tt);
|
|
}
|
|
return {
|
|
hash: txHash,
|
|
timestamp: block.timestamp,
|
|
block: info.blockNumber !== null ? info.blockNumber : undefined,
|
|
reverted: !receipt.status,
|
|
transfers,
|
|
tokenTransfers,
|
|
info: { ...tx[txHash], block, actions },
|
|
};
|
|
});
|
|
}
|
|
async allowances(address, opts = {}) {
|
|
const approval = (0, index_ts_1.events)(index_ts_1.ERC20).Approval;
|
|
// ERC-721/ERC-1155: +ApprovalForAll
|
|
// ERC-1761 Scoped Approval for partial with 1155/721?
|
|
const topics = approval.topics({ owner: address, spender: null, value: null });
|
|
const logs = await this.ethLogs(topics, opts);
|
|
// res[tokenContract][spender] = value
|
|
const res = {};
|
|
for (const l of logs) {
|
|
const decoded = approval.decode(l.topics, l.data);
|
|
if (decoded.owner.toLowerCase() !== address.toLowerCase())
|
|
continue;
|
|
if (!res[l.address])
|
|
res[l.address] = {};
|
|
res[l.address][decoded.spender] = decoded.value;
|
|
}
|
|
return res;
|
|
}
|
|
}
|
|
exports.Web3Provider = Web3Provider;
|
|
/**
|
|
* Calculates balances at specific point in time after tx.
|
|
* Also, useful as a sanity check in case we've missed something.
|
|
* Info from multiple addresses can be merged (sort everything first).
|
|
*/
|
|
function calcTransfersDiff(transfers) {
|
|
// address -> balance
|
|
const balances = {};
|
|
// contract -> address -> tokenId -> balance
|
|
const tokenBalances = {};
|
|
let _0 = BigInt(0);
|
|
for (const t of transfers) {
|
|
for (const it of t.transfers) {
|
|
if (it.from) {
|
|
if (balances[it.from] === undefined)
|
|
balances[it.from] = _0;
|
|
balances[it.from] -= it.value;
|
|
}
|
|
if (it.to) {
|
|
if (balances[it.to] === undefined)
|
|
balances[it.to] = _0;
|
|
balances[it.to] += it.value;
|
|
}
|
|
}
|
|
for (const tt of t.tokenTransfers) {
|
|
if (!tokenBalances[tt.contract])
|
|
tokenBalances[tt.contract] = {};
|
|
const token = tokenBalances[tt.contract];
|
|
for (const [tokenId, value] of tt.tokens) {
|
|
if (token[tt.from] === undefined)
|
|
token[tt.from] = new Map();
|
|
if (token[tt.to] === undefined)
|
|
token[tt.to] = new Map();
|
|
const fromTokens = token[tt.from];
|
|
const toTokens = token[tt.to];
|
|
fromTokens.set(tokenId, (fromTokens.get(tokenId) || _0) - value);
|
|
toTokens.set(tokenId, (toTokens.get(tokenId) || _0) + value);
|
|
}
|
|
}
|
|
Object.assign(t, {
|
|
balances: { ...balances },
|
|
// deep copy
|
|
tokenBalances: Object.fromEntries(Object.entries(tokenBalances).map(([k, v]) => [k, { ...v }])),
|
|
});
|
|
}
|
|
return transfers;
|
|
}
|
|
//# sourceMappingURL=archive.js.map
|