diff --git a/README.md b/README.md index 0fdb753..d03aed0 100644 --- a/README.md +++ b/README.md @@ -24,24 +24,25 @@ For [Deno](https://deno.land), ensure to use [npm specifier](https://deno.land/m For React Native, you may need a [polyfill for getRandomValues](https://github.com/LinusU/react-native-get-random-values). If you don't like NPM, a standalone [eth-signer.js](https://github.com/paulmillr/micro-eth-signer/releases) is also available. -- [Create and sign transactions](#create-and-sign-transactions) -- [Create and checksum addresses](#create-and-checksum-addresses) -- [Generate random keys and addresses](#generate-random-keys-and-addresses) -- [Human-readable hints](#human-readable-hints) - - [Decoding transactions](#decoding-transactions) - - [Decoding events](#decoding-events) +- [Transactions: create, sign](#create-and-sign-transactions) +- [Addresses: create, checksum](#create-and-checksum-addresses) +- [Generate random wallet](#generate-random-keys-and-addresses) +- [Fetch balances and history using RPC](#fetch-balances-and-history-using-rpc) - [Call smart contracts](#call-smart-contracts) - [Fetch Chainlink oracle prices](#fetch-chainlink-oracle-prices) - [Swap tokens with Uniswap](#swap-tokens-with-uniswap) - [ABI type inference](#abi-type-inference) -- [RLP parsing](#rlp-parsing) -- [SSZ parsing](#ssz-parsing) +- Parsing + - [Human-readable transaction hints](#human-readable-transaction-hints) + - [Human-readable event hints](#human-readable-event-hints) + - [RLP parsing](#rlp-parsing) + - [SSZ parsing](#ssz-parsing) - [Sign and verify messages](#sign-and-verify-messages) - [Security](#security) - [Performance](#performance) - [License](#license) -### Create and sign transactions +### Transactions: create, sign ```ts import { addr, amounts, Transaction } from 'micro-eth-signer'; @@ -61,7 +62,7 @@ console.log('address is same', tx.sender === senderAddr); We support legacy, EIP2930, EIP1559 and EIP4844 (Dencun / Cancun) transactions. -### Create and checksum addresses +### Addresses: create, checksum ```ts import { addr } from 'micro-eth-signer'; @@ -78,7 +79,7 @@ console.log( ); ``` -### Generate random keys and addresses +### Generate random wallet ```ts import { addr } from 'micro-eth-signer'; @@ -88,93 +89,20 @@ console.log(random.privateKey, random.address); // 0x26d930712fd2f612a107A70fd0Ad79b777cD87f6 ``` -### Human-readable hints - -#### Decoding transactions - -The transaction sent ERC-20 USDT token between addresses. The library produces a following hint: - -> Transfer 22588 USDT to 0xdac17f958d2ee523a2206206994597c13d831ec7 - -```ts -import { decodeTx } from 'micro-eth-signer/abi'; - -const tx = - '0xf8a901851d1a94a20082c12a94dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000054259870025a066fcb560b50e577f6dc8c8b2e3019f760da78b4c04021382ba490c572a303a42a0078f5af8ac7e11caba9b7dc7a64f7bdc3b4ce1a6ab0a1246771d7cc3524a7200'; -// Decode tx information -deepStrictEqual(decodeTx(tx), { - name: 'transfer', - signature: 'transfer(address,uint256)', - value: { - to: '0xdac17f958d2ee523a2206206994597c13d831ec7', - value: 22588000000n, - }, - hint: 'Transfer 22588 USDT to 0xdac17f958d2ee523a2206206994597c13d831ec7', -}); -``` - -Or if you have already decoded tx: - -```ts -import { decodeData } from 'micro-eth-signer/abi'; - -const to = '0x7a250d5630b4cf539739df2c5dacb4c659f2488d'; -const data = - '7ff36ab5000000000000000000000000000000000000000000000000ab54a98ceb1f0ad30000000000000000000000000000000000000000000000000000000000000080000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000006fd9c6ea0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000106d3c66d22d2dd0446df23d7f5960752994d600'; -const value = 100000000000000000n; - -deepStrictEqual(decodeData(to, data, value, { customContracts }), { - name: 'swapExactETHForTokens', - signature: 'swapExactETHForTokens(uint256,address[],address,uint256)', - value: { - amountOutMin: 12345678901234567891n, - path: [ - '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', - '0x106d3c66d22d2dd0446df23d7f5960752994d600', - ], - to: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', - deadline: 1876543210n, - }, -}); - -// With custom tokens/contracts -const customContracts = { - '0x106d3c66d22d2dd0446df23d7f5960752994d600': { abi: 'ERC20', symbol: 'LABRA', decimals: 9 }, -}; -deepStrictEqual(decodeData(to, data, value, { customContracts }), { - name: 'swapExactETHForTokens', - signature: 'swapExactETHForTokens(uint256,address[],address,uint256)', - value: { - amountOutMin: 12345678901234567891n, - path: [ - '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', - '0x106d3c66d22d2dd0446df23d7f5960752994d600', - ], - to: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', - deadline: 1876543210n, - }, - hint: 'Swap 0.1 ETH for at least 12345678901.234567891 LABRA. Expires at Tue, 19 Jun 2029 06:00:10 GMT', -}); -``` - -#### Decoding events - -Decoding the event produces the following hint: - -> Allow 0xe592427a0aece92de3edee1f18e0157c05861564 spending up to 1000 BAT from 0xd8da6bf26964af9d7eed9e03e53415d37aa96045 +### Fetch balances and history using RPC ```ts -import { decodeEvent } from 'micro-eth-signer/abi'; - -const to = '0x0d8775f648430679a709e98d2b0cb6250d2887ef'; -const topics = [ - '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', - '0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045', - '0x000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564', -]; -const data = '0x00000000000000000000000000000000000000000000003635c9adc5dea00000'; -const einfo = decodeEvent(to, topics, data); -console.log(einfo); +import { TxProvider, calcTransfersDiff } from 'micro-eth-signer/net/tx'; +const txp = TxProvider(fetchProvider(fetch)); +const addr = '0x26d930712fd2f612a107A70fd0Ad79b777cD87f6'; +const _2 = await txp.height(); +const _3 = await txp.transfers(addr); +const _4 = await txp.allowances(addr); +const _5 = await txp.tokenBalances(addr); +// High-level methods are `height`, `unspent`, `transfers`, `allowances` and `tokenBalances`. +// +// Low-level methods are `blockInfo`, `internalTransactions`, `ethLogs`, `tokenTransfers`, `wethTransfers`, +// `tokenInfo` and `txInfo`. ``` ### Call smart contracts @@ -278,7 +206,96 @@ There are following limitations: Check out [`src/net/ens.ts`](./src/net/ens.ts) for type-safe contract execution example. -### RLP parsing +### Parsers + +#### Human-readable transaction hints + +The transaction sent ERC-20 USDT token between addresses. The library produces a following hint: + +> Transfer 22588 USDT to 0xdac17f958d2ee523a2206206994597c13d831ec7 + +```ts +import { decodeTx } from 'micro-eth-signer/abi'; + +const tx = + '0xf8a901851d1a94a20082c12a94dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000054259870025a066fcb560b50e577f6dc8c8b2e3019f760da78b4c04021382ba490c572a303a42a0078f5af8ac7e11caba9b7dc7a64f7bdc3b4ce1a6ab0a1246771d7cc3524a7200'; +// Decode tx information +deepStrictEqual(decodeTx(tx), { + name: 'transfer', + signature: 'transfer(address,uint256)', + value: { + to: '0xdac17f958d2ee523a2206206994597c13d831ec7', + value: 22588000000n, + }, + hint: 'Transfer 22588 USDT to 0xdac17f958d2ee523a2206206994597c13d831ec7', +}); +``` + +Or if you have already decoded tx: + +```ts +import { decodeData } from 'micro-eth-signer/abi'; + +const to = '0x7a250d5630b4cf539739df2c5dacb4c659f2488d'; +const data = + '7ff36ab5000000000000000000000000000000000000000000000000ab54a98ceb1f0ad30000000000000000000000000000000000000000000000000000000000000080000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000006fd9c6ea0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000106d3c66d22d2dd0446df23d7f5960752994d600'; +const value = 100000000000000000n; + +deepStrictEqual(decodeData(to, data, value, { customContracts }), { + name: 'swapExactETHForTokens', + signature: 'swapExactETHForTokens(uint256,address[],address,uint256)', + value: { + amountOutMin: 12345678901234567891n, + path: [ + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + '0x106d3c66d22d2dd0446df23d7f5960752994d600', + ], + to: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', + deadline: 1876543210n, + }, +}); + +// With custom tokens/contracts +const customContracts = { + '0x106d3c66d22d2dd0446df23d7f5960752994d600': { abi: 'ERC20', symbol: 'LABRA', decimals: 9 }, +}; +deepStrictEqual(decodeData(to, data, value, { customContracts }), { + name: 'swapExactETHForTokens', + signature: 'swapExactETHForTokens(uint256,address[],address,uint256)', + value: { + amountOutMin: 12345678901234567891n, + path: [ + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + '0x106d3c66d22d2dd0446df23d7f5960752994d600', + ], + to: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', + deadline: 1876543210n, + }, + hint: 'Swap 0.1 ETH for at least 12345678901.234567891 LABRA. Expires at Tue, 19 Jun 2029 06:00:10 GMT', +}); +``` + +#### Human-readable event hints + +Decoding the event produces the following hint: + +> Allow 0xe592427a0aece92de3edee1f18e0157c05861564 spending up to 1000 BAT from 0xd8da6bf26964af9d7eed9e03e53415d37aa96045 + +```ts +import { decodeEvent } from 'micro-eth-signer/abi'; + +const to = '0x0d8775f648430679a709e98d2b0cb6250d2887ef'; +const topics = [ + '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', + '0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045', + '0x000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564', +]; +const data = '0x00000000000000000000000000000000000000000000003635c9adc5dea00000'; +const einfo = decodeEvent(to, topics, data); +console.log(einfo); +``` + +#### RLP parsing We implement RLP in just 100 lines of code, powered by [packed](https://github.com/paulmillr/micro-packed): @@ -287,7 +304,7 @@ import { RLP } from 'micro-eth-signer/rlp'; RLP.decode(RLP.encode('dog')); ``` -### SSZ parsing +#### SSZ parsing Simple serialize (SSZ) is the serialization method used on the Beacon Chain. We implement RLP in just 900 lines of code, powered by [packed](https://github.com/paulmillr/micro-packed): diff --git a/package.json b/package.json index 9ab10d3..9ea2c5f 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,7 @@ "name": "micro-eth-signer", "version": "0.8.1", "description": "Minimal library for Ethereum transactions, addresses and smart contracts", - "files": [ - "lib", - "src" - ], + "files": ["lib", "src"], "main": "lib/index.js", "module": "lib/esm/index.js", "types": "lib/index.d.ts", @@ -45,6 +42,11 @@ "import": "./lib/esm/net/index.js", "default": "./lib/net/index.js" }, + "./net/tx": { + "types": "./lib/net/tx.d.ts", + "import": "./lib/esm/net/tx.js", + "default": "./lib/net/tx.js" + }, "./rlp": { "types": "./lib/net/rlp.d.ts", "import": "./lib/esm/net/rlp.js", diff --git a/src/net/index.ts b/src/net/index.ts index 43a6240..07d8bd7 100644 --- a/src/net/index.ts +++ b/src/net/index.ts @@ -7,20 +7,76 @@ import { Web3Provider, Web3CallArgs, hexToNumber } from '../utils.js'; // There are many low level APIs inside which are not exported yet. export { Chainlink, ENS, UniswapV2, UniswapV3 }; -export const FetchProvider = ( - fetch: (url: string, opt?: Record) => Promise<{ json: () => Promise }>, +// There is a lot features required for network to make this useful +// This is attempt to create them via small composable wrappers +export type FetchFn = ( url: string, - headers: Record = {} -): Web3Provider => { - const jsonrpc = async (method: string, ...params: any[]) => { - const res = await fetch(url, { + opt?: Record +) => Promise<{ json: () => Promise }>; +type Headers = Record; +type JsonFn = (url: string, headers: Headers, body: unknown) => Promise; +type PromiseCb = { + resolve: (value: T | PromiseLike) => void; + reject: (reason?: any) => void; +}; + +function getJSONUsingFetch(fn: FetchFn): JsonFn { + return async (url: string, headers: Headers = {}, body: unknown) => { + const res = await fn(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, - body: JSON.stringify({ jsonrpc: '2.0', id: 0, method, params }), + body: JSON.stringify(body), + }); + return await res.json(); + }; +} + +// Unsafe. TODO: inspect for race conditions and bugs. +function limitParallel(jsonFn: JsonFn, limit: number): JsonFn { + let cur = 0; + const queue: ({ url: string; headers: Headers; body: unknown } & PromiseCb)[] = []; + const process = () => { + if (cur >= limit) return; + const next = queue.shift(); + if (!next) return; + try { + jsonFn(next.url, next.headers, next.body) + .then(next.resolve) + .catch(next.reject) + .finally(() => { + cur--; + process(); + }); + } catch (e) { + next.reject(e); + cur--; + } + cur++; + }; + return (url, headers, body) => { + return new Promise((resolve, reject) => { + queue.push({ url, headers, body, resolve, reject }); + process(); }); - const json = await res.json(); + }; +} + +type NetworkOpts = { + limitParallel?: number; +}; + +export const FetchProvider = ( + fetch: FetchFn, + url: string, + headers: Headers = {}, + opts: NetworkOpts = {} +): Web3Provider => { + let fn = getJSONUsingFetch(fetch); + if (opts.limitParallel) fn = limitParallel(fn, opts.limitParallel); + const jsonrpc = async (method: string, ...params: any[]) => { + const json = await fn(url, headers, { jsonrpc: '2.0', id: 0, method, params }); if (json && json.error) - throw new Error(`FetchProvider(${json.error.code}): ${json.error.message}`); + throw new Error(`FetchProvider(${json.error.code}): ${json.error.message || json.error}`); return json.result; }; return { @@ -28,5 +84,6 @@ export const FetchProvider = ( jsonrpc('eth_call', args, tag) as Promise, estimateGas: async (args: Web3CallArgs, tag = 'latest') => hexToNumber(await jsonrpc('eth_estimateGas', args, tag)), + call: (method: string, ...args: any[]) => jsonrpc(method, ...args), }; }; diff --git a/src/net/tx.ts b/src/net/tx.ts new file mode 100644 index 0000000..89b25d2 --- /dev/null +++ b/src/net/tx.ts @@ -0,0 +1,690 @@ +import { Web3Provider, amounts } from '../utils.js'; +import { Transaction } from '../index.js'; +import { TxVersions, legacySig } from '../tx.js'; +import { ContractInfo, createContract, events, ERC20, WETH } from '../abi/index.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: `TxProvider#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: number | bigint | undefined) => + `0x${!n ? '0' : n.toString(16).replace(/^0+/, '')}`; + +const ERC_TRANSFER = events(ERC20).Transfer; +const WETH_DEPOSIT = events(WETH).Deposit; +const WETH_WITHDRAW = events(WETH).Withdrawal; + +function group(items: T[], s: string | ((i: T) => string)): Record { + let res: Record = {}; + for (let i of items) { + const key = typeof s === 'function' ? s(i) : (i as any)[s]; + if (!res[key]) res[key] = []; + res[key].push(i); + } + return res; +} +// Output types +export type BlockInfo = { + baseFeePerGas: bigint; + difficulty: bigint; + extraData: string; + gasLimit: bigint; + gasUsed: bigint; + hash: string; + logsBloom: string; + miner: string; + mixHash: string; + nonce: string; + number: number; + parentHash: string; + receiptsRoot: string; + sha3Uncles: string; + size: number; + stateRoot: string; + timestamp: number; + totalDifficulty: bigint; + transactions: string[]; // transaction hashes (if false) + transactionsRoot: string; + uncles: string[]; +}; + +export type Action = { + action: { + from: string; + callType: string; + gas: bigint; + input: string; + to: string; + value: bigint; + }; + blockHash: string; + blockNumber: number; + result: { gasUsed: bigint; output: string }; + subtraces: number; + traceAddress: string[]; + transactionHash: string; + transactionPosition: number; + type: string; +}; + +export type Log = { + address: string; + topics: string[]; + data: string; + blockNumber: number; + transactionHash: string; + transactionIndex: number; + blockHash: string; + logIndex: number; + removed: boolean; +}; + +export type TxInfo = { + blockHash: string; + blockNumber: number; + hash: string; + accessList?: [string, string[]][]; + transactionIndex: number; + type: number; + nonce: bigint; + input: string; + r: bigint; + s: bigint; + chainId: bigint; + v: bigint; + gas: bigint; + maxPriorityFeePerGas?: bigint; + from: string; + to: string; + maxFeePerGas?: bigint; + value: bigint; + gasPrice: bigint; + // blobs + maxFeePerBlobGas?: bigint; + blobVersionedHashes?: string[]; +}; + +export type TxReceipt = { + transactionHash: string; + blockHash: string; + blockNumber: number; + logsBloom: string; + gasUsed: bigint; + contractAddress: string | null; + cumulativeGasUsed: bigint; + transactionIndex: number; + from: string; + to: string; + type: number; + effectiveGasPrice: bigint; + logs: Log[]; + status: number; + blobGasPrice?: bigint; + blobGasUsed?: bigint; +}; + +export type Unspent = { + symbol: 'ETH'; + decimals: number; + balance: bigint; + value?: number; + // useful for wallets to know if there was transactions related to wallet + // NOTE: even if nonce is zero, there can be transfers to wallet + // can be used to check before fetching all transactions + active: boolean; +}; + +export type Topics = (string | null | (string | null)[])[]; +export type TokenInfo = ContractInfo & { contract: string }; +export type Transfer = { from: string; to?: string; value: bigint }; +export type TokenTransfer = Transfer & TokenInfo & { to: string }; + +export type TxTransfers = { + // This is most interesting info about tx for wallets + hash: string; + timestamp?: number; + block?: number; + transfers: Transfer[]; + tokenTransfers: TokenTransfer[]; + reverted: boolean; + // This contains everything about tx in raw format + info: { + type: keyof typeof TxVersions; + info: TxInfo; + receipt: TxReceipt; + raw?: string; + block: BlockInfo; + actions: Action[]; + }; +}; + +/** + * Callbacks are needed, because we want to call getTx / getBlock / getTokenInfo + * requests as fast as possible, to reduce amount of sequential execution. + * If we retrieve 10 pages of transactions, we can call per tx + * callbacks for transaction from first page before all other pages fetched. + * + * Ensure caching: they can be called multiple times for same tx / block. + */ +export type Callbacks = { + txCallback?: (txHash: string) => void; + blockCallback?: (blockNum: number) => void; + contractCallback?: (contrct: string) => void; +}; + +export type Pagination = { fromBlock?: number; toBlock?: number }; +export type TraceOpts = Callbacks & + Pagination & { + perRequest?: number; + limitTrace?: number; + }; +export type LogOpts = Callbacks & + ( + | Pagination + | { + fromBlock: number; + toBlock: number; + limitLogs: number; // limit block range per request + } + ); +export type Balances = { + balances: Record; + tokenBalances: Record>; +}; +export type TxInfoOpts = Callbacks & { ignoreTxRebuildErrors?: boolean }; +export type TxAllowances = Record>; + +function fixBlock(block: BlockInfo) { + 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', + ] as const) { + if (block[i] && block[i] !== null) block[i] = BigInt(block[i]); + } +} +function fixAction(action: Action, opts: Callbacks = {}) { + 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: Log, opts: Callbacks = {}) { + 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: TxInfo) { + for (const i of ['blockNumber', 'type', 'transactionIndex'] as const) info[i] = Number(info[i]); + for (const i of [ + 'nonce', + 'r', + 's', + 'chainId', + 'v', + 'gas', + 'maxPriorityFeePerGas', + 'maxFeePerGas', + 'value', + 'gasPrice', + 'maxFeePerBlobGas', + ] as const) { + if (info[i] !== undefined && info[i] !== null) info[i] = BigInt(info[i]!); + } + // Same API as Transaction, so we can re-create easily + if (info.accessList) + info.accessList = info.accessList.map((i: any) => [i.address, i.storageKeys]); + return info; +} + +function fixTxReceipt(receipt: TxReceipt) { + for (const i of ['blockNumber', 'type', 'transactionIndex', 'status'] as const) + receipt[i] = Number(receipt[i]); + for (const i of [ + 'gasUsed', + 'cumulativeGasUsed', + 'effectiveGasPrice', + 'blobGasPrice', + 'blobGasUsed', + ] as const) { + if (receipt[i] !== undefined) receipt[i] = BigInt(receipt[i]!); + } + for (const log of receipt.logs) fixLog(log); + return receipt; +} +function validateCallbacks(opts: Record) { + 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: Record) { + 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: Record) { + 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: Record) { + 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); +} + +/** + * 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`. + */ +export class TxProvider { + constructor(private provider: Web3Provider) {} + + // The low-level place where network calls are done + private rpc(method: string, ...args: any[]) { + return this.provider.call(method, ...args); + } + + // Timestamp is available only inside blocks + async blockInfo(block: number): Promise { + const res = await this.rpc('eth_getBlockByNumber', ethNum(block), false); + fixBlock(res); + return res; + } + + async unspent(address: string) { + let [balance, nonce] = await Promise.all([ + this.rpc('eth_getBalance', address, 'latest'), + this.rpc('eth_getTransactionCount', address, 'latest'), + ]); + balance = BigInt(balance); + nonce = BigInt(nonce); + return { + symbol: 'ETH', + decimals: amounts.ETH_PRECISION, + balance, + nonce, + // Note: account can be active even if nonce!==0! + active: balance > 0 || nonce !== 0, + }; + } + async height(): Promise { + return Number.parseInt(await this.rpc('eth_blockNumber')); + } + + async traceFilterSingle(address: string, opts: TraceOpts = {}) { + const res = await this.rpc('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: string, opts: TraceOpts = {}) { + 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: Record = {}; + const out: Action[] = []; + for (;;) { + const params: Record = { + 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.rpc('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 ethLogsSingle(topics: Topics, opts: LogOpts): Promise { + const req: Record = { topics, fromBlock: ethNum(opts.fromBlock || 0) }; + if (opts.toBlock !== undefined) req.toBlock = ethNum(opts.toBlock); + const res = await this.rpc('eth_getLogs', req); + return res.map((i: any) => fixLog(i, opts)); + } + + async ethLogs(topics: Topics, opts: LogOpts = {}): Promise { + 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; + } + + // If we want incoming and outgoing token transfers we need to call both + async tokenTransfers(address: string, opts: LogOpts = {}) { + if (typeof address !== 'string') throw new Error('tokenTransfers: wrong address'); + validateLogOpts(opts); + 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: string, opts: LogOpts = {}) { + 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 txInfo(txHash: string, opts: TxInfoOpts = {}) { + let [info, receipt] = await Promise.all([ + this.rpc('eth_getTransactionByHash', txHash), + this.rpc('eth_getTransactionReceipt', txHash), + ]); + info = fixTxInfo(info); + receipt = fixTxReceipt(receipt); + const type = Object.keys(TxVersions)[info.type] as keyof typeof TxVersions; + // 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: string | undefined = undefined; + try { + const rawData: Record = { + nonce: info.nonce, + gasLimit: info.gas, + to: 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, legacySig.encode({ v: info.v, r: info.r, s: info.s })); + const tx = new Transaction(type, rawData as any, 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(address: string): Promise { + // will throw 'Execution reverted' if not ERC20 + try { + let c = createContract(ERC20, this.provider, address); + const [symbol, decimals] = await Promise.all([c.symbol.call(), c.decimals.call()]); + return { contract: address, abi: 'ERC20', symbol, decimals: Number(decimals) }; + } catch (e) { + return; + } + } + // 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 + // trace_filter (web3) returns information only for first two cases, most of explorers returns only first case. + async transfers(address: string, opts: TraceOpts & LogOpts = {}) { + const txCache: Record = {}; + const blockCache: Record = {}; + const tokenCache: Record = {}; + const _opts = { + ...opts, + txCallback: (txHash: string) => { + if (txCache[txHash]) return; + txCache[txHash] = this.txInfo(txHash, opts); + }, + blockCallback: (blockNumber: number) => { + if (blockCache[blockNumber]) return; + blockCache[blockNumber] = this.blockInfo(blockNumber); + }, + contractCallback: (address: string) => { + 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), + ]); + const mapCache = async (cache: Record) => { + 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] as [string, number, number] + ); + _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] as { info: TxInfo; receipt: TxReceipt }; + const actions = actionPerTx[txHash]; + const block = info.blockNumber !== null ? blocks[info.blockNumber] : undefined; + const transfers: Transfer[] = []; + 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: TokenTransfer[] = []; + for (const log of receipt.logs) { + const tokenInfo = tokens[log.address]; + if (tokenInfo) { + try { + tokenTransfers.push({ + contract: log.address, + ...tokenInfo, + ...ERC_TRANSFER.decode(log.topics, log.data), + }); + } 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); + tokenTransfers.push({ + ...tokenInfo, + contract: log.address, + value: decoded.wad, + from: log.address, + to: decoded.dst, + }); + } catch (e) {} + try { + const decoded = WETH_WITHDRAW.decode(log.topics, log.data); + tokenTransfers.push({ + ...tokenInfo, + contract: log.address, + value: decoded.wad, + from: decoded.src, + to: log.address, + }); + } catch (e) {} + } + return { + hash: txHash, + timestamp: block.timestamp, + block: info.blockNumber !== null ? info.blockNumber : undefined, + reverted: !receipt.status, + transfers, + tokenTransfers, + info: { ...tx[txHash], block, actions }, + }; + }) as TxTransfers[]; + } + + async allowances(address: string, opts: LogOpts = {}): Promise { + const approval = events(ERC20).Approval; + const topics = approval.topics({ owner: address, spender: null, value: null }); + const logs = await this.ethLogs(topics, opts); + // res[tokenContract][spender] = value + const res: TxAllowances = {}; + 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; + } + + async tokenBalances(address: string, tokens: string[]): Promise> { + const balances = await Promise.all( + tokens.map((i) => createContract(ERC20, this.provider, i).balanceOf.call(address)) + ); + return Object.fromEntries(tokens.map((i, j) => [i, balances[j]])); + } +} + +/** + * 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). + */ +export function calcTransfersDiff(transfers: TxTransfers[]): (TxTransfers & Balances)[] { + const balances: Record = {}; + const tokenBalances: Record> = {}; + for (const t of transfers) { + for (const it of t.transfers) { + if (it.from) { + if (balances[it.from] === undefined) balances[it.from] = 0n; + balances[it.from] -= it.value; + } + if (it.to) { + if (balances[it.to] === undefined) balances[it.to] = 0n; + balances[it.to] += it.value; + } + } + for (const tt of t.tokenTransfers) { + if (!tokenBalances[tt.contract]) tokenBalances[tt.contract] = {}; + const token = tokenBalances[tt.contract]; + if (token[tt.from] === undefined) token[tt.from] = 0n; + token[tt.from] -= tt.value; + if (token[tt.to] === undefined) token[tt.to] = 0n; + token[tt.to] += tt.value; + } + Object.assign(t, { + balances: { ...balances }, + // deep copy + tokenBalances: Object.fromEntries( + Object.entries(tokenBalances).map(([k, v]) => [k, { ...v }]) + ), + }); + } + return transfers as (TxTransfers & Balances)[]; +} diff --git a/src/tx.ts b/src/tx.ts index 53bbd22..f232388 100644 --- a/src/tx.ts +++ b/src/tx.ts @@ -112,7 +112,7 @@ type YRS = Partial<{ chainId: bigint; yParity: number; r: bigint; s: bigint }>; // Case: unsigned tx for cold wallet for different chains, like mainnet & testnet. // - otherwise v = yParity + 2*chainId + 35 // - allows to keep legacy logic here, instead of copying to Transaction -const legacySig = { +export const legacySig = { encode: (data: VRS) => { const { v, r, s } = data; if (v === undefined) return { chainId: undefined }; diff --git a/src/utils.ts b/src/utils.ts index 7e05dfc..1bd5660 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -17,6 +17,7 @@ export type Web3CallArgs = Partial<{ export type Web3Provider = { ethCall: (args: Web3CallArgs) => Promise; estimateGas: (args: Web3CallArgs) => Promise; + call: (method: string, ...args: any[]) => Promise; }; const ETH_PRECISION = 18; diff --git a/test/eip1191.test.js b/test/eip1191.test.js index 721f992..f190469 100644 --- a/test/eip1191.test.js +++ b/test/eip1191.test.js @@ -1,6 +1,5 @@ import { deepStrictEqual, throws } from 'node:assert'; import { describe, should } from 'micro-should'; -import { hexToBytes } from '@noble/hashes/utils'; import { addr } from '../lib/esm/address.js'; import { Transaction } from '../lib/esm/index.js'; diff --git a/test/net.test.js b/test/net.test.js index b5c36f2..0f8501a 100644 --- a/test/net.test.js +++ b/test/net.test.js @@ -3,10 +3,17 @@ import { describe, should } from 'micro-should'; import { tokenFromSymbol } from '../lib/esm/abi/index.js'; import { FetchProvider, ENS, Chainlink, UniswapV3 } from '../lib/esm/net/index.js'; import { ethDecimal, numberTo0xHex } from '../lib/esm/utils.js'; +import * as netTx from '../lib/esm/net/tx.js'; +// These real network responses from real nodes, captured by fetchReplay +import { default as NET_TX_REPLAY } from './vectors/rpc/transactions.js'; +import { default as NET_ENS_REPLAY } from './vectors/rpc/ens.js'; +import { default as NET_CHAINLINK_REPLAY } from './vectors/rpc/chainlink.js'; +import { default as NET_UNISWAP_REPLAY } from './vectors/rpc/uniswap.js'; +import { default as NET_ESTIMATE_GAS_REPLAY } from './vectors/rpc/estimateGas.js'; +import { default as NET_TX_VECTORS } from './vectors/rpc/parsed-transactions.js'; -// TODO: is it reasonable to leave that in tests? -const NODE_URL = 'https://nodes.mewapi.io/rpc/eth'; -const NODE_HEADERS = { Origin: 'https://www.myetherwallet.com' }; +const NODE_URL = 'https://NODE_URL'; +const NODE_HEADERS = {}; // NOTE: most tests check for specific values and will fail because of price changes const REAL_NETWORK = false; @@ -18,49 +25,39 @@ const getKey = (url, opt) => { return JSON.stringify({ url, opt: _opt }); }; -const fetchLogger = () => { - const logs = {}; +// both fetchReplay & fetchLogger, this allows to replay partially +const fetchReplay = (logs, offline = true) => { + const accessed = new Set(); const ftch = async (url, opt) => { - const res = await fetch(url, opt); - return { - json: async () => { - const json = await res.json(); - const key = getKey(url, opt); - logs[key] = JSON.stringify(json); - return json; - }, - }; - }; - ftch.logs = logs; - return ftch; -}; - -const fetchReplay = (logs) => { - return (url, opt) => { const key = getKey(url, opt); - if (!logs[key]) throw new Error(`fetchReplay: unknown request=${key}`); + accessed.add(key); + if (!logs[key]) { + if (offline) throw new Error(`fetchReplay: unknown request=${key}`); + console.log(`fetchReplay: missing request for ${url}`); + const res = await fetch(url, opt); + return { + json: async () => { + const json = await res.json(); + const key = getKey(url, opt); + logs[key] = JSON.stringify(json); + return json; + }, + }; + } return { json: () => JSON.parse(logs[key]) }; }; + ftch.logs = logs; + ftch.accessed = accessed; + return ftch; }; // FetchProvider(fetch) -> do network request -// FetchProvider(fetchLogger()) -> log request to replay +// FetchProvider(fetchReplay(logs, false)) -> log request to replay // FetchPRovider(fetchReplay(logs)) -> replay recorded logs describe('Network', () => { should('ENS', async () => { - const replay = fetchReplay({ - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e\\",\\"data\\":\\"0x0178b8bfee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000004976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41\\",\\"data\\":\\"0x3b3b57deee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e\\",\\"data\\":\\"0x0178b8bf7aef81fbd30c83431369026d62ee533af8b69f246b63d75b40fe223346e6fa9a\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000005fbb459c49bb06083c33109fa4f14810ec2cf358"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0x5fbb459c49bb06083c33109fa4f14810ec2cf358\\",\\"data\\":\\"0x691f34317aef81fbd30c83431369026d62ee533af8b69f246b63d75b40fe223346e6fa9a\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b766974616c696b2e657468000000000000000000000000000000000000000000"}', - }); - - const logger = fetchLogger(); + const replay = fetchReplay(NET_ENS_REPLAY); const provider = FetchProvider(REAL_NETWORK ? fetch : replay, NODE_URL, NODE_HEADERS); const ens = new ENS(provider); const vitalikAddr = await ens.nameToAddress('vitalik.eth'); @@ -70,11 +67,8 @@ describe('Network', () => { deepStrictEqual(vitalikName, 'vitalik.eth'); }); should('Chainlink', async () => { - const replay = fetchReplay({ - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xf4030086522a5beea4988f8ca5b36dbc97bee88c\\",\\"data\\":\\"0xfeaf968c\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x00000000000000000000000000000000000000000000000600000000000030e40000000000000000000000000000000000000000000000000000064f201292180000000000000000000000000000000000000000000000000000000065ee021b0000000000000000000000000000000000000000000000000000000065ee021b00000000000000000000000000000000000000000000000600000000000030e4"}', - }); - const logger = fetchLogger(); + const replay = fetchReplay(NET_CHAINLINK_REPLAY); + const provider = FetchProvider(REAL_NETWORK ? fetch : replay, NODE_URL, NODE_HEADERS); const chainlink = new Chainlink(provider); const btcPrice = await chainlink.coinPrice('BTC'); @@ -83,117 +77,7 @@ describe('Network', () => { }); should('UniswapV3', async () => { - const replay = fetchReplay({ - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2002710a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x000000000000000000000000000000000000000000000102aed5731e0bb69839"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000001d6abcfd9cc27ed2"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000001d6abcfd9cc27ed2"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x000000000000000000000000000000000000000000000104238ab7cbe7174508"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2002710a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000001d6abcfd9cc27ed2"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4dac17f958d2ee523a2206206994597c13d831ec70001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x000000000000000000000000000000000000000000000103fbcace2ec1edd013"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x000000000000000000000000000000000000000000000103b5c15d83c8d667ca"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000142b976bfa5d87c3eb"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2002710a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000142b976bfa5d87c3eb"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8c00e94cb662c3520282e6f5717214004a7f268880027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"execution reverted"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8dac17f958d2ee523a2206206994597c13d831ec70027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"execution reverted"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4dac17f958d2ee523a2206206994597c13d831ec7000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000098df6cde7c10fd465"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4d46ba6d942050d489dbd938a2c909a5d5039a1610027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: TF","data":"0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000025446000000000000000000000000000000000000000000000000000000000000"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f42260fac5e5542a773aa44fbcfedf7c193bc2c5990027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000006d494d26ad203f81"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2002710dac17f958d2ee523a2206206994597c13d831ec7000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000098df6cde7c10fd465"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000142b976bfa5d87c3eb"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f42260fac5e5542a773aa44fbcfedf7c193bc2c599000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x000000000000000000000000000000000000000000000100f79a4f43a49f53ec"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8d46ba6d942050d489dbd938a2c909a5d5039a1610001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: TF","data":"0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000025446000000000000000000000000000000000000000000000000000000000000"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20027109f8f72aa9304c8b593d555f12ef6589cc3a579a20001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"execution reverted"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb89f8f72aa9304c8b593d555f12ef6589cc3a579a20027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: Unexpected error","data":"0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010556e6578706563746564206572726f7200000000000000000000000000000000"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8c00e94cb662c3520282e6f5717214004a7f26888000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x00000000000000000000000000000000000000000000000068fe473415e9de69"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2002710c00e94cb662c3520282e6f5717214004a7f26888000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x00000000000000000000000000000000000000000000000068fe473415e9de69"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4c00e94cb662c3520282e6f5717214004a7f26888000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"execution reverted"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2002710dac17f958d2ee523a2206206994597c13d831ec70027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"execution reverted"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2002710d46ba6d942050d489dbd938a2c909a5d5039a1610001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: Unexpected error","data":"0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010556e6578706563746564206572726f7200000000000000000000000000000000"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2002710dac17f958d2ee523a2206206994597c13d831ec70001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000ea7b2247311860570f"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8d46ba6d942050d489dbd938a2c909a5d5039a1610027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: TF","data":"0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000025446000000000000000000000000000000000000000000000000000000000000"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4dac17f958d2ee523a2206206994597c13d831ec70027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"execution reverted"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2002710c00e94cb662c3520282e6f5717214004a7f268880027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"execution reverted"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4d46ba6d942050d489dbd938a2c909a5d5039a1610001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: TF","data":"0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000025446000000000000000000000000000000000000000000000000000000000000"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4c00e94cb662c3520282e6f5717214004a7f268880001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"execution reverted"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb82260fac5e5542a773aa44fbcfedf7c193bc2c5990027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000006d494d26ad203f81"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4d46ba6d942050d489dbd938a2c909a5d5039a161000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: TF","data":"0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000025446000000000000000000000000000000000000000000000000000000000000"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8dac17f958d2ee523a2206206994597c13d831ec70001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x00000000000000000000000000000000000000000000010396379405dfd182d2"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb89f8f72aa9304c8b593d555f12ef6589cc3a579a2000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000b5a2b4aa9638226e79"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb82260fac5e5542a773aa44fbcfedf7c193bc2c599000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000001004411c7691c328823"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20027102260fac5e5542a773aa44fbcfedf7c193bc2c5990027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000006d494d26ad203f81"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2002710d46ba6d942050d489dbd938a2c909a5d5039a1610027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: Unexpected error","data":"0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010556e6578706563746564206572726f7200000000000000000000000000000000"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8dac17f958d2ee523a2206206994597c13d831ec7000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000098df6cde7c10fd465"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb89f8f72aa9304c8b593d555f12ef6589cc3a579a20001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"execution reverted"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20027102260fac5e5542a773aa44fbcfedf7c193bc2c599000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000ffb1ab93a6f0faf3e6"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f49f8f72aa9304c8b593d555f12ef6589cc3a579a2000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000020a39c195efc26fe3"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20027102260fac5e5542a773aa44fbcfedf7c193bc2c5990001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: Unexpected error","data":"0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010556e6578706563746564206572726f7200000000000000000000000000000000"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2002710c00e94cb662c3520282e6f5717214004a7f268880001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"execution reverted"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f49f8f72aa9304c8b593d555f12ef6589cc3a579a20001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"execution reverted"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f49f8f72aa9304c8b593d555f12ef6589cc3a579a20027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: Unexpected error","data":"0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010556e6578706563746564206572726f7200000000000000000000000000000000"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f42260fac5e5542a773aa44fbcfedf7c193bc2c5990001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: Unexpected error","data":"0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010556e6578706563746564206572726f7200000000000000000000000000000000"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8d46ba6d942050d489dbd938a2c909a5d5039a161000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: TF","data":"0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000025446000000000000000000000000000000000000000000000000000000000000"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4c00e94cb662c3520282e6f5717214004a7f268880027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"execution reverted"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20027109f8f72aa9304c8b593d555f12ef6589cc3a579a20027106b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: Unexpected error","data":"0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010556e6578706563746564206572726f7200000000000000000000000000000000"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20027109f8f72aa9304c8b593d555f12ef6589cc3a579a2000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000b5437a99f21414fa99"}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8c00e94cb662c3520282e6f5717214004a7f268880001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"execution reverted"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2002710d46ba6d942050d489dbd938a2c909a5d5039a161000bb86b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: Unexpected error","data":"0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010556e6578706563746564206572726f7200000000000000000000000000000000"}}', - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_call\\",\\"params\\":[{\\"to\\":\\"0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6\\",\\"data\\":\\"0xcdca175300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000001111d67bb1bb00000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb82260fac5e5542a773aa44fbcfedf7c193bc2c5990001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"error":{"code":3,"message":"execution reverted: Unexpected error","data":"0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010556e6578706563746564206572726f7200000000000000000000000000000000"}}', - }); - const logger = fetchLogger(); + const replay = fetchReplay(NET_UNISWAP_REPLAY); const provider = FetchProvider(REAL_NETWORK ? fetch : replay, NODE_URL, NODE_HEADERS); const univ3 = new UniswapV3(provider); // Actual code @@ -217,11 +101,7 @@ describe('Network', () => { }); should('estimateGas', async () => { - const replay = fetchReplay({ - '{"url":"https://NODE_URL/","opt":{"method":"POST","headers":{"Content-Type":"application/json"},"body":"{\\"jsonrpc\\":\\"2.0\\",\\"id\\":0,\\"method\\":\\"eth_estimateGas\\",\\"params\\":[{\\"from\\":\\"0xd8da6bf26964af9d7eed9e03e53415d37aa96045\\",\\"to\\":\\"0xe592427a0aece92de3edee1f18e0157c05861564\\",\\"value\\":\\"0x1111d67bb1bb0000\\",\\"data\\":\\"0xc04b8d59000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000000019077fd30000000000000000000000000000000000000000000000000001111d67bb1bb0000000000000000000000000000000000000000000000000102d6906ca33403f40b0000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000000000000000000\\"},\\"latest\\"]}"}}': - '{"jsonrpc":"2.0","id":0,"result":"0x39a32"}', - }); - const logger = fetchLogger(); + const replay = fetchReplay(NET_ESTIMATE_GAS_REPLAY); const provider = FetchProvider(REAL_NETWORK ? fetch : replay, NODE_URL, NODE_HEADERS); const gasLimit = await provider.estimateGas({ from: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', @@ -232,6 +112,130 @@ describe('Network', () => { deepStrictEqual(gasLimit, 236082n); //console.log('LOGS', logger.logs); }); + + should('Transactions', async () => { + // Random address from abi tests which test for fingerprinted data in encoding. + // Perfect for tests: only has a few transactions and provides different types of txs. + const addr = '0x6994eCe772cC4aBb5C9993c065a34C94544A4087'; + const replay = fetchReplay(NET_TX_REPLAY, true); + const provider = FetchProvider(REAL_NETWORK ? fetch : replay, 'NODE_URL', {}); + const tx = new netTx.TxProvider(provider); + // Blocks + deepStrictEqual(await tx.blockInfo(15_010_733), NET_TX_VECTORS.block); + // Internal transactions sanity + const internal = await Promise.all([ + // tx.internalTransactions(addr), + tx.internalTransactions(addr, { + fromBlock: 14_272_357, + toBlock: 15_065_121, + }), + tx.internalTransactions(addr, { + fromBlock: 14_272_357, + toBlock: 15_065_121, + perRequest: 25, + }), + ]); + for (const i of internal) deepStrictEqual(i, NET_TX_VECTORS.internal); + // Make sure that all equal and pagination works + for (let i = 1; i < internal.length; i++) deepStrictEqual(internal[i - 1], internal[i]); + + // 15_065_121 -- last tx from address + const logsTokenFrom = await tx.tokenTransfers(addr, { + fromBlock: 14_200_000, + toBlock: 15_065_121, + limit: 10_000, + }); + deepStrictEqual(logsTokenFrom[0], NET_TX_VECTORS.allFrom); + // works with alchemy, doesn't work with quicknode + deepStrictEqual((await tx.tokenTransfers(addr, {}))[0], NET_TX_VECTORS.allFrom); + deepStrictEqual( + await tx.txInfo('0x01bcf8e4be50fcf0537865f658dc912f43710f2fe579aa46f133105d58945eb5'), + NET_TX_VECTORS.txInfo + ); + deepStrictEqual( + await tx.txInfo('0xba296ea35b5ff390b8c180ae8f536159dc8723871b43ed7f80e0c218cf171a05'), + NET_TX_VECTORS.blobTx + ); + deepStrictEqual( + await tx.txInfo('0x86c5a4350c973cd990105ae461522d01aa313fecbe0a67727e941cd9cee28997'), + NET_TX_VECTORS.legacyTx + ); + // Dynamically get tokenInfo for unknown token + deepStrictEqual( + { price: undefined, ...(await tx.tokenInfo(tokenFromSymbol('BAT').contract)) }, + tokenFromSymbol('BAT') + ); + const transfers = (await tx.transfers(addr)).map((i) => ({ ...i, info: undefined })); + deepStrictEqual(transfers, NET_TX_VECTORS.transfers); + + const diff = netTx.calcTransfersDiff(transfers); + const diffLast = diff[diff.length - 1]; + // From etherscan + // 0.000130036071955215 + // 130036071955215n + deepStrictEqual(diffLast.balances[addr.toLowerCase()], 130036071955215n); + const tokenBalances = { + '0xa1c7d450130bb77c6a23ddfaecbc4a060215384b': 195983216736205891626852908n, + '0xb4bda5036c709e7e3d6cc7fe577fb616363cbb0c': 130626626738232824137856499n, + '0x81db680b1a811b5e9be8b3a01a211f94f7c7fbf3': 1965780268797386852567451n, + '0x528686c89db00e22f58703b2d4b02e200f3255eb': 26027502560778307541998806n, + '0x1db9f66a900c0cb6d50e34d02985fc7bdafcde7e': 2892700371812082121621646155n, + '0x35333e20391c171fc856d2f6e46304410949c452': 60882249518969761112698747n, + }; + const tokenBalancesAll = { + ...tokenBalances, + '0x3ab16af1315dc6c95f83cbf522fecf98d00fd9ba': 13282015786652313746188n, // LooksRare: balance call reverts + // these have different amount. preemine or some other missed method? + '0x106d3c66d22d2dd0446df23d7f5960752994d600': 8123054826641307911n, // LABRA + '0x236d53148f83706c3d670064809577385f923a75': 14539885279977039623402n, // SHUSKY + }; + deepStrictEqual( + Object.fromEntries( + Object.entries(diffLast.tokenBalances) + .map(([k, v]) => [k, v[addr.toLowerCase()]]) + .filter(([k, v]) => v !== 0n) + ), + tokenBalancesAll + ); + deepStrictEqual(await tx.tokenBalances(addr, Object.keys(tokenBalances)), tokenBalances); + + deepStrictEqual(await tx.allowances(addr), { + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': { + '0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45': 1269932532n, + '0x7a250d5630b4cf539739df2c5dacb4c659f2488d': + 115792089237316195423570985008687907853269984665640564039457584007913129639935n, + '0xe592427a0aece92de3edee1f18e0157c05861564': + 115792089237316195423570985008687907853269984665640564039457584007913129639935n, + '0xdef1c0ded9bec7f1a1670819833240f027b25eff': + 115792089237316195423570985008687907853269984665640564039457584007913129639935n, + }, + '0xf4d2888d29d722226fafa5d9b24f9164c092421e': { + '0xbcd7254a1d759efa08ec7c3291b2e85c5dcc12ce': + 115792089237316195423570985008687907853269984665640564030358861248482727152367n, + '0x3ab16af1315dc6c95f83cbf522fecf98d00fd9ba': + 115792089237316195423570985008687907853269984665640564026175568221260815893747n, + }, + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2': { + '0xe592427a0aece92de3edee1f18e0157c05861564': + 115792089237316195423570985008687907853269984665640564039457584007913129639935n, + }, + '0xb4bda5036c709e7e3d6cc7fe577fb616363cbb0c': { + '0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45': + 115792089237316195423570985008687907853269984665640564039457584007913129639935n, + }, + '0x1db9f66a900c0cb6d50e34d02985fc7bdafcde7e': { + '0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45': + 115792089237316195423570985008687907853269984665640564039457584007913129639935n, + }, + }); + + // console.log('---- LOGS ----'); + // console.log( + // JSON.stringify( + // Object.fromEntries(Object.entries(replay.logs).filter(([k, v]) => replay.accessed.has(k))) + // ) + // ); + }); }); // ESM is broken.