@w3lib/evm-batch-call 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +206 -1
- package/dist/dividends.d.ts +135 -0
- package/dist/dividends.d.ts.map +1 -0
- package/dist/dividends.js +253 -0
- package/dist/dividends.js.map +1 -0
- package/dist/history.d.ts +154 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +247 -0
- package/dist/history.js.map +1 -0
- package/dist/historySources.d.ts +32 -0
- package/dist/historySources.d.ts.map +1 -0
- package/dist/historySources.js +319 -0
- package/dist/historySources.js.map +1 -0
- package/dist/liquidity/v3.d.ts +2 -0
- package/dist/liquidity/v3.d.ts.map +1 -1
- package/dist/liquidity/v3.js +1 -1
- package/dist/liquidity/v3.js.map +1 -1
- package/dist/liquidity/v4.d.ts +21 -0
- package/dist/liquidity/v4.d.ts.map +1 -1
- package/dist/liquidity/v4.js +4 -3
- package/dist/liquidity/v4.js.map +1 -1
- package/dist/lp.d.ts +8 -0
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +17 -1
- package/dist/lp.js.map +1 -1
- package/dist/lpV4.d.ts +4 -0
- package/dist/lpV4.d.ts.map +1 -1
- package/dist/lpV4.js +27 -3
- package/dist/lpV4.js.map +1 -1
- package/dist/permit.d.ts +137 -0
- package/dist/permit.d.ts.map +1 -0
- package/dist/permit.js +355 -0
- package/dist/permit.js.map +1 -0
- package/dist/portfolio.d.ts +2 -0
- package/dist/portfolio.d.ts.map +1 -1
- package/dist/portfolio.js +9 -4
- package/dist/portfolio.js.map +1 -1
- package/dist/rewards/farms.d.ts +26 -0
- package/dist/rewards/farms.d.ts.map +1 -0
- package/dist/rewards/farms.js +210 -0
- package/dist/rewards/farms.js.map +1 -0
- package/dist/rewards/index.d.ts +42 -0
- package/dist/rewards/index.d.ts.map +1 -0
- package/dist/rewards/index.js +382 -0
- package/dist/rewards/index.js.map +1 -0
- package/dist/rewards/types.d.ts +170 -0
- package/dist/rewards/types.d.ts.map +1 -0
- package/dist/rewards/types.js +2 -0
- package/dist/rewards/types.js.map +1 -0
- package/dist/simulate.d.ts +186 -0
- package/dist/simulate.d.ts.map +1 -0
- package/dist/simulate.js +559 -0
- package/dist/simulate.js.map +1 -0
- package/dist/simulateDecode.d.ts +33 -0
- package/dist/simulateDecode.d.ts.map +1 -0
- package/dist/simulateDecode.js +321 -0
- package/dist/simulateDecode.js.map +1 -0
- package/dist/smartAccount/account.d.ts +31 -0
- package/dist/smartAccount/account.d.ts.map +1 -0
- package/dist/smartAccount/account.js +33 -0
- package/dist/smartAccount/account.js.map +1 -0
- package/dist/smartAccount/clients.d.ts +77 -0
- package/dist/smartAccount/clients.d.ts.map +1 -0
- package/dist/smartAccount/clients.js +122 -0
- package/dist/smartAccount/clients.js.map +1 -0
- package/dist/smartAccount/eip5792.d.ts +77 -0
- package/dist/smartAccount/eip5792.d.ts.map +1 -0
- package/dist/smartAccount/eip5792.js +152 -0
- package/dist/smartAccount/eip5792.js.map +1 -0
- package/dist/smartAccount/send.d.ts +40 -0
- package/dist/smartAccount/send.d.ts.map +1 -0
- package/dist/smartAccount/send.js +271 -0
- package/dist/smartAccount/send.js.map +1 -0
- package/dist/smartAccount/userOp.d.ts +91 -0
- package/dist/smartAccount/userOp.d.ts.map +1 -0
- package/dist/smartAccount/userOp.js +124 -0
- package/dist/smartAccount/userOp.js.map +1 -0
- package/dist/subpaths/dividends.d.ts +28 -0
- package/dist/subpaths/dividends.d.ts.map +1 -0
- package/dist/subpaths/dividends.js +29 -0
- package/dist/subpaths/dividends.js.map +1 -0
- package/dist/subpaths/history.d.ts +22 -0
- package/dist/subpaths/history.d.ts.map +1 -0
- package/dist/subpaths/history.js +23 -0
- package/dist/subpaths/history.js.map +1 -0
- package/dist/subpaths/permit.d.ts +14 -0
- package/dist/subpaths/permit.d.ts.map +1 -0
- package/dist/subpaths/permit.js +14 -0
- package/dist/subpaths/permit.js.map +1 -0
- package/dist/subpaths/price.js +2 -2
- package/dist/subpaths/price.js.map +1 -1
- package/dist/subpaths/rewards.d.ts +27 -0
- package/dist/subpaths/rewards.d.ts.map +1 -0
- package/dist/subpaths/rewards.js +25 -0
- package/dist/subpaths/rewards.js.map +1 -0
- package/dist/subpaths/simulate.d.ts +25 -0
- package/dist/subpaths/simulate.d.ts.map +1 -0
- package/dist/subpaths/simulate.js +28 -0
- package/dist/subpaths/simulate.js.map +1 -0
- package/dist/subpaths/smart-account.d.ts +24 -0
- package/dist/subpaths/smart-account.d.ts.map +1 -0
- package/dist/subpaths/smart-account.js +24 -0
- package/dist/subpaths/smart-account.js.map +1 -0
- package/dist/swap.d.ts +45 -5
- package/dist/swap.d.ts.map +1 -1
- package/dist/swap.js +108 -12
- package/dist/swap.js.map +1 -1
- package/dist/write/batch.d.ts +34 -2
- package/dist/write/batch.d.ts.map +1 -1
- package/dist/write/batch.js +22 -3
- package/dist/write/batch.js.map +1 -1
- package/dist/write/collect.d.ts +9 -0
- package/dist/write/collect.d.ts.map +1 -1
- package/dist/write/collect.js +41 -1
- package/dist/write/collect.js.map +1 -1
- package/dist/write/send.d.ts +4 -0
- package/dist/write/send.d.ts.map +1 -1
- package/dist/write/send.js.map +1 -1
- package/dist/write/signer.d.ts +5 -1
- package/dist/write/signer.d.ts.map +1 -1
- package/dist/write/signer.js.map +1 -1
- package/dist/write/transfer.d.ts +5 -3
- package/dist/write/transfer.d.ts.map +1 -1
- package/dist/write/transfer.js +12 -6
- package/dist/write/transfer.js.map +1 -1
- package/package.json +35 -1
- package/src/dividends.ts +360 -0
- package/src/history.ts +367 -0
- package/src/historySources.ts +360 -0
- package/src/liquidity/v3.ts +1 -1
- package/src/liquidity/v4.ts +4 -3
- package/src/lp.ts +18 -1
- package/src/lpV4.ts +32 -2
- package/src/permit.ts +457 -0
- package/src/portfolio.ts +11 -4
- package/src/rewards/farms.ts +229 -0
- package/src/rewards/index.ts +431 -0
- package/src/rewards/types.ts +179 -0
- package/src/simulate.ts +727 -0
- package/src/simulateDecode.ts +352 -0
- package/src/smartAccount/account.ts +63 -0
- package/src/smartAccount/clients.ts +180 -0
- package/src/smartAccount/eip5792.ts +187 -0
- package/src/smartAccount/send.ts +341 -0
- package/src/smartAccount/userOp.ts +180 -0
- package/src/subpaths/dividends.ts +47 -0
- package/src/subpaths/history.ts +41 -0
- package/src/subpaths/permit.ts +39 -0
- package/src/subpaths/price.ts +2 -2
- package/src/subpaths/rewards.ts +46 -0
- package/src/subpaths/simulate.ts +51 -0
- package/src/subpaths/smart-account.ts +40 -0
- package/src/swap.ts +139 -18
- package/src/write/batch.ts +44 -5
- package/src/write/collect.ts +60 -1
- package/src/write/send.ts +4 -0
- package/src/write/signer.ts +5 -1
- package/src/write/transfer.ts +17 -10
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 历史记录的数据来源(适配器)。每个适配器按 HistoryQuery 返回原始的交易和转账(RawPage),合并和分类由 history.ts 完成。
|
|
3
|
+
*
|
|
4
|
+
* - blockscout:各链的 Blockscout 实例(免 Key)的 Etherscan 兼容接口(`/api?module=account&action=txlist …`)。
|
|
5
|
+
* 没用 v2 REST(`/api/v2/addresses/…`):同样的数据,v2 每页带大量标签 / 元数据(50 条约 500KB),这个接口只有必要字段,
|
|
6
|
+
* 并且与 Etherscan 系完全相同,一套解析两处用
|
|
7
|
+
* - etherscan:Etherscan v2(一个 Key 覆盖 60+ 条链)及其他 Etherscan 兼容的接口
|
|
8
|
+
* - rpc:只用 `eth_getLogs`(ERC20 / 721 / 1155 的 Transfer 日志按地址所在的 topic 查)+ 交易 / 回执 / 区块。没有主币转账(顶层的 value 只有
|
|
9
|
+
* 带代币转账的交易才有)、没有内部交易;要按区块窗口往回扫,稀疏的地址一次调用可能扫不到记录(返回 nextCursor 继续)
|
|
10
|
+
* - trongrid:TronGrid 的 `/v1/accounts/{address}/transactions` 和 `/transactions/trc20`(内部的 TRX 转账查不到)
|
|
11
|
+
*/
|
|
12
|
+
import { AbiCoder, id, zeroPadValue } from 'ethers'
|
|
13
|
+
|
|
14
|
+
import { HttpStatusError, fetchJson } from './http.js'
|
|
15
|
+
import type { HistoryAdapter, HistoryQuery, RawMove, RawPage, RawTx } from './history.js'
|
|
16
|
+
import { errorText } from './fallback.js'
|
|
17
|
+
import { isTronChain, toTronAddress } from './tron.js'
|
|
18
|
+
|
|
19
|
+
export type HistorySource = 'blockscout' | 'etherscan' | 'rpc' | 'trongrid' | HistoryAdapter
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 各链的 Blockscout 实例。2026-10 验证过可用:1、10、100、534352、324、130、42220、34443、57073;
|
|
23
|
+
* 8453 / 42161 / 137 的官方实例前面有 Cloudflare 人机验证,脚本请求可能被 403(此时自动换下一个来源)
|
|
24
|
+
*/
|
|
25
|
+
export const BLOCKSCOUT_APIS: Readonly<Record<number, string>> = {
|
|
26
|
+
1: 'https://eth.blockscout.com/api',
|
|
27
|
+
10: 'https://explorer.optimism.io/api',
|
|
28
|
+
100: 'https://gnosisscan.io/api',
|
|
29
|
+
130: 'https://unichain.blockscout.com/api',
|
|
30
|
+
137: 'https://polygon.blockscout.com/api',
|
|
31
|
+
324: 'https://zksync.blockscout.com/api',
|
|
32
|
+
8453: 'https://base.blockscout.com/api',
|
|
33
|
+
34443: 'https://explorer.mode.network/api',
|
|
34
|
+
42161: 'https://arbitrum.blockscout.com/api',
|
|
35
|
+
42220: 'https://celo.blockscout.com/api',
|
|
36
|
+
57073: 'https://explorer.inkonchain.com/api',
|
|
37
|
+
534352: 'https://scrollscan.com/api',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const num = (v: unknown) => Number(v ?? 0)
|
|
41
|
+
const big = (v: unknown) => {
|
|
42
|
+
try {
|
|
43
|
+
return BigInt(String(v ?? '0') || '0')
|
|
44
|
+
} catch {
|
|
45
|
+
return 0n
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
49
|
+
|
|
50
|
+
type Row = Record<string, string>
|
|
51
|
+
|
|
52
|
+
/** 各个列表(每个都按位置从新到旧、最多 limit 条)合成一页:列表满了说明后面还有,被截断的位置要留到下一页 */
|
|
53
|
+
function paged(txs: RawTx[], moves: RawMove[], lists: ReadonlyArray<{ count: number; lastPos: number }>, limit: number): RawPage {
|
|
54
|
+
const full = lists.filter((l) => l.count >= limit && l.count > 0)
|
|
55
|
+
if (!full.length) return { txs, moves, completeFrom: 0 }
|
|
56
|
+
const last = Math.max(...full.map((l) => l.lastPos))
|
|
57
|
+
return { txs, moves, completeFrom: last + 1, next: last }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Etherscan 兼容接口(Blockscout / Etherscan v2)
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
const ACTIONS = { txs: 'txlist', internal: 'txlistinternal', tokens: 'tokentx', nfts: 'tokennfttx', multi: 'token1155tx' } as const
|
|
65
|
+
|
|
66
|
+
async function explorerCall(q: HistoryQuery, base: string, action: string, secret?: string): Promise<Row[]> {
|
|
67
|
+
const url = new URL(base)
|
|
68
|
+
const params: Record<string, string> = { module: 'account', action, address: q.address, startblock: '0', page: '1', offset: String(q.limit), sort: 'desc' }
|
|
69
|
+
if (q.endPos !== undefined) params.endblock = String(q.endPos)
|
|
70
|
+
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v)
|
|
71
|
+
for (let attempt = 0; ; attempt++) {
|
|
72
|
+
let json: { status?: string; message?: string; result?: unknown }
|
|
73
|
+
try {
|
|
74
|
+
json = await fetchJson(q.fetch, url.toString(), undefined, secret)
|
|
75
|
+
} catch (err) {
|
|
76
|
+
// 公共实例限流(HTTP 429):等一下重试
|
|
77
|
+
if (err instanceof HttpStatusError && err.status === 429 && attempt < 3) {
|
|
78
|
+
await sleep(1200 * (attempt + 1))
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
throw err
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(json.result)) {
|
|
84
|
+
const rows = json.result as Row[]
|
|
85
|
+
// 个别实例的某些列表不认 endblock,返回的还是最新的记录:翻页会重复 / 漏记录,换下一个来源
|
|
86
|
+
if (q.endPos !== undefined && rows.some((r) => num(r.blockNumber) > (q.endPos as number))) throw new Error(`${new URL(base).host}: ${action} ignores endblock`)
|
|
87
|
+
return rows
|
|
88
|
+
}
|
|
89
|
+
const text = typeof json.result === 'string' ? json.result : (json.message ?? 'unexpected response')
|
|
90
|
+
if (/no (transactions|records|token)/i.test(`${json.message} ${text}`)) return []
|
|
91
|
+
if (/rate limit|too many/i.test(text) && attempt < 3) {
|
|
92
|
+
await sleep(1200 * (attempt + 1))
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`${new URL(base).host}: ${text}`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Blockscout / Etherscan 系适配器:`url` 是接口地址(可以带 chainid 等固定参数) */
|
|
100
|
+
export function explorerSource(name: string, url: (chainId: number) => string | undefined, secret?: string): HistoryAdapter {
|
|
101
|
+
return {
|
|
102
|
+
name,
|
|
103
|
+
supports: (chainId) => url(chainId) !== undefined,
|
|
104
|
+
async fetch(q) {
|
|
105
|
+
const base = url(q.chainId)
|
|
106
|
+
if (!base) throw new Error(`${name}: chain ${q.chainId} is not supported`)
|
|
107
|
+
const call = (action: string) => explorerCall(q, base, action, secret)
|
|
108
|
+
// 依次请求(公共实例按请求频率限流,并发会触发 429)
|
|
109
|
+
const txRows = await call(ACTIONS.txs)
|
|
110
|
+
const internalRows = await call(ACTIONS.internal)
|
|
111
|
+
const tokenRows = await call(ACTIONS.tokens)
|
|
112
|
+
const nftRows = q.nft ? await call(ACTIONS.nfts) : []
|
|
113
|
+
const multiRows = q.nft ? await call(ACTIONS.multi) : []
|
|
114
|
+
const txs: RawTx[] = txRows.map((r) => ({
|
|
115
|
+
hash: r.hash as string,
|
|
116
|
+
pos: num(r.blockNumber),
|
|
117
|
+
block: num(r.blockNumber),
|
|
118
|
+
time: num(r.timeStamp),
|
|
119
|
+
from: r.from as string,
|
|
120
|
+
to: r.to || r.contractAddress || null,
|
|
121
|
+
value: big(r.value),
|
|
122
|
+
input: r.input || '0x',
|
|
123
|
+
ok: r.isError !== '1' && r.txreceipt_status !== '0',
|
|
124
|
+
fee: big(r.gasUsed) * big(r.gasPrice),
|
|
125
|
+
}))
|
|
126
|
+
const moves: RawMove[] = []
|
|
127
|
+
const base0 = (r: Row) => ({ hash: (r.hash ?? r.transactionHash) as string, pos: num(r.blockNumber), block: num(r.blockNumber), time: num(r.timeStamp), from: r.from as string, to: r.to as string })
|
|
128
|
+
for (const r of internalRows) if (r.isError !== '1' && big(r.value) > 0n) moves.push({ ...base0(r), asset: 'native', value: big(r.value) })
|
|
129
|
+
for (const r of tokenRows) moves.push({ ...base0(r), asset: r.contractAddress as string, value: big(r.value), symbol: r.tokenSymbol || null, decimals: r.tokenDecimal ? num(r.tokenDecimal) : null })
|
|
130
|
+
for (const r of nftRows) moves.push({ ...base0(r), asset: r.contractAddress as string, value: 1n, tokenId: r.tokenID as string, symbol: r.tokenSymbol || null })
|
|
131
|
+
for (const r of multiRows) moves.push({ ...base0(r), asset: r.contractAddress as string, value: big(r.tokenValue || '1'), tokenId: r.tokenID as string, symbol: r.tokenSymbol || null })
|
|
132
|
+
const lists = [txRows, internalRows, tokenRows, nftRows, multiRows].map((rows) => ({ count: rows.length, lastPos: rows.length ? Math.min(...rows.map((r) => num(r.blockNumber))) : 0 }))
|
|
133
|
+
return paged(txs, moves, lists, q.limit)
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Blockscout:`urls` 覆盖 / 补充内置的实例表(chainId → 接口地址) */
|
|
139
|
+
export function blockscoutSource(urls: Readonly<Record<number, string>> = {}): HistoryAdapter {
|
|
140
|
+
return explorerSource('blockscout', (chainId) => urls[chainId] ?? BLOCKSCOUT_APIS[chainId])
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Etherscan v2:一个 Key 覆盖多条链;`baseUrl` 可换成其他 Etherscan 兼容的地址 */
|
|
144
|
+
export function etherscanSource(options: { apiKey: string; baseUrl?: string }): HistoryAdapter {
|
|
145
|
+
if (!options.apiKey) throw new Error('etherscan source requires an apiKey')
|
|
146
|
+
return explorerSource('etherscan', (chainId) => `${options.baseUrl ?? 'https://api.etherscan.io/v2/api'}?chainid=${chainId}&apikey=${encodeURIComponent(options.apiKey)}`, options.apiKey)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// 纯 RPC 日志
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
const TRANSFER = id('Transfer(address,address,uint256)')
|
|
154
|
+
const SINGLE = id('TransferSingle(address,address,address,uint256,uint256)')
|
|
155
|
+
const BATCH = id('TransferBatch(address,address,address,uint256[],uint256[])')
|
|
156
|
+
const coder = AbiCoder.defaultAbiCoder()
|
|
157
|
+
const topicAddr = (hex: string) => `0x${hex.slice(-40)}`.toLowerCase()
|
|
158
|
+
|
|
159
|
+
export interface RpcSourceOptions {
|
|
160
|
+
/** 每次 getLogs 的区块窗口,默认 2000(节点嫌大时自动对半缩小) */
|
|
161
|
+
window?: number
|
|
162
|
+
/** 一次最多扫多少个窗口,默认 10(凑够条数就提前停;扫不到记录时用 nextCursor 继续) */
|
|
163
|
+
maxWindows?: number
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
type Log = { address: string; topics: readonly string[]; data: string; blockNumber: number; transactionHash: string }
|
|
167
|
+
type LogNode = { getTransaction(hash: string): Promise<{ from: string; to: string | null; value: bigint; data: string } | null>; getTransactionReceipt(hash: string): Promise<{ status: number | null; gasUsed: bigint; gasPrice: bigint } | null>; getBlock(n: number): Promise<{ timestamp: number } | null> }
|
|
168
|
+
|
|
169
|
+
async function pool<T, R>(items: readonly T[], size: number, work: (item: T) => Promise<R>): Promise<R[]> {
|
|
170
|
+
const out: R[] = new Array(items.length)
|
|
171
|
+
let next = 0
|
|
172
|
+
await Promise.all(
|
|
173
|
+
Array.from({ length: Math.min(size, items.length) }, async () => {
|
|
174
|
+
while (next < items.length) {
|
|
175
|
+
const i = next++
|
|
176
|
+
out[i] = await work(items[i] as T)
|
|
177
|
+
}
|
|
178
|
+
}),
|
|
179
|
+
)
|
|
180
|
+
return out
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function rpcSource(options: RpcSourceOptions = {}): HistoryAdapter {
|
|
184
|
+
const window = Math.max(10, Math.floor(options.window ?? 2000))
|
|
185
|
+
const maxWindows = Math.max(1, Math.floor(options.maxWindows ?? 10))
|
|
186
|
+
return {
|
|
187
|
+
name: 'rpc',
|
|
188
|
+
supports: (chainId) => !isTronChain(chainId),
|
|
189
|
+
async fetch(q) {
|
|
190
|
+
const addr = zeroPadValue(q.address, 32)
|
|
191
|
+
const filters: Array<Array<string | string[] | null>> = [[TRANSFER, null, addr], [TRANSFER, addr], ...(q.nft ? [[[SINGLE, BATCH], null, addr], [[SINGLE, BATCH], null, null, addr]] : [])]
|
|
192
|
+
const logsIn = async (from: number, to: number, depth = 0): Promise<Log[]> => {
|
|
193
|
+
try {
|
|
194
|
+
const lists = await Promise.all(filters.map((topics) => q.provider.getLogs({ fromBlock: from, toBlock: to, topics } as never)))
|
|
195
|
+
// ERC-721 的 Transfer 和 ERC-20 同一个 topic(4 个 topic):不要 NFT 时丢掉
|
|
196
|
+
return lists.flat().filter((l) => q.nft || l.topics.length === 3) as unknown as Log[]
|
|
197
|
+
} catch (err) {
|
|
198
|
+
// 范围太大 / 结果太多:对半拆开重试(最多拆 4 层)
|
|
199
|
+
if (depth < 4 && to > from && /range|too (large|wide|big|many)|exceed|limit|more than|response size/i.test(errorText(err))) {
|
|
200
|
+
const mid = from + Math.floor((to - from) / 2)
|
|
201
|
+
return [...(await logsIn(from, mid, depth + 1)), ...(await logsIn(mid + 1, to, depth + 1))]
|
|
202
|
+
}
|
|
203
|
+
throw err
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
let to = q.endPos ?? (await q.provider.getBlockNumber())
|
|
207
|
+
let from = to
|
|
208
|
+
const logs: Log[] = []
|
|
209
|
+
for (let w = 0; w < maxWindows; w++) {
|
|
210
|
+
from = Math.max(0, to - window + 1)
|
|
211
|
+
logs.push(...(await logsIn(from, to)))
|
|
212
|
+
if (new Set(logs.map((l) => l.transactionHash)).size >= q.limit || from === 0) break
|
|
213
|
+
to = from - 1
|
|
214
|
+
}
|
|
215
|
+
const next = from > 0 ? from - 1 : undefined
|
|
216
|
+
|
|
217
|
+
// 只保留最新的 limit 笔交易;被丢掉的更老的交易所在的区块留给下一页
|
|
218
|
+
const byBlock = new Map<string, number>()
|
|
219
|
+
for (const l of logs) byBlock.set(l.transactionHash, Math.max(byBlock.get(l.transactionHash) ?? 0, l.blockNumber))
|
|
220
|
+
const hashes = [...byBlock.entries()].sort((a, b) => b[1] - a[1])
|
|
221
|
+
const kept = hashes.slice(0, q.limit)
|
|
222
|
+
const dropped = hashes.slice(q.limit)
|
|
223
|
+
const completeFrom = dropped.length ? (dropped[0] as [string, number])[1] + 1 : from
|
|
224
|
+
const keepSet = new Set(kept.map(([h]) => h))
|
|
225
|
+
|
|
226
|
+
const { runOnNodes } = await import('./write/rpc.js')
|
|
227
|
+
const node = <T>(method: string, fn: (n: LogNode) => Promise<T>) => runOnNodes(q.provider.rpc, method, (n) => fn(n as unknown as LogNode))
|
|
228
|
+
const blockTimes = new Map<number, number>()
|
|
229
|
+
const blocks = [...new Set(kept.map(([, b]) => b))]
|
|
230
|
+
await pool(blocks, 6, async (b) => {
|
|
231
|
+
blockTimes.set(b, (await node('getBlock', (n) => n.getBlock(b)))?.timestamp ?? 0)
|
|
232
|
+
})
|
|
233
|
+
const txs = (
|
|
234
|
+
await pool(kept, 6, async ([hash, block]): Promise<RawTx | null> => {
|
|
235
|
+
const [tx, receipt] = await Promise.all([node('getTransaction', (n) => n.getTransaction(hash)), node('getTransactionReceipt', (n) => n.getTransactionReceipt(hash))])
|
|
236
|
+
if (!tx) return null
|
|
237
|
+
return { hash, pos: block, block, time: blockTimes.get(block) ?? 0, from: tx.from, to: tx.to, value: tx.value, input: tx.data, ok: receipt?.status !== 0, ...(receipt ? { fee: receipt.gasUsed * receipt.gasPrice } : {}) }
|
|
238
|
+
})
|
|
239
|
+
).filter((t): t is RawTx => t !== null)
|
|
240
|
+
|
|
241
|
+
const moves: RawMove[] = []
|
|
242
|
+
for (const l of logs) {
|
|
243
|
+
if (!keepSet.has(l.transactionHash)) continue
|
|
244
|
+
const base = { hash: l.transactionHash, pos: l.blockNumber, block: l.blockNumber, time: blockTimes.get(l.blockNumber) ?? 0, asset: l.address.toLowerCase() }
|
|
245
|
+
if (l.topics[0] === TRANSFER && l.topics.length === 3) moves.push({ ...base, from: topicAddr(l.topics[1] as string), to: topicAddr(l.topics[2] as string), value: big(l.data) })
|
|
246
|
+
else if (l.topics[0] === TRANSFER && l.topics.length === 4) moves.push({ ...base, from: topicAddr(l.topics[1] as string), to: topicAddr(l.topics[2] as string), value: 1n, tokenId: BigInt(l.topics[3] as string).toString() })
|
|
247
|
+
else if (l.topics[0] === SINGLE) {
|
|
248
|
+
const [tokenId, value] = coder.decode(['uint256', 'uint256'], l.data) as unknown as [bigint, bigint]
|
|
249
|
+
moves.push({ ...base, from: topicAddr(l.topics[2] as string), to: topicAddr(l.topics[3] as string), value, tokenId: tokenId.toString() })
|
|
250
|
+
} else if (l.topics[0] === BATCH) {
|
|
251
|
+
const [ids, values] = coder.decode(['uint256[]', 'uint256[]'], l.data) as unknown as [bigint[], bigint[]]
|
|
252
|
+
ids.forEach((tokenId, i) => moves.push({ ...base, from: topicAddr(l.topics[2] as string), to: topicAddr(l.topics[3] as string), value: values[i] as bigint, tokenId: tokenId.toString() }))
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// 代币的 symbol / decimals(一次 multicall);NFT 合约没有 decimals,symbol 能读到就用
|
|
256
|
+
const assets = [...new Set(moves.map((m) => m.asset))]
|
|
257
|
+
if (assets.length) {
|
|
258
|
+
const details = await q.provider.tokens(assets, { fields: ['symbol', 'decimals'] }).catch(() => [])
|
|
259
|
+
const meta = new Map(assets.map((a, i) => [a, details[i]]))
|
|
260
|
+
for (const m of moves) {
|
|
261
|
+
const d = meta.get(m.asset)
|
|
262
|
+
m.symbol = d?.symbol ?? null
|
|
263
|
+
m.decimals = typeof d?.decimals === 'number' ? d.decimals : null
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { txs, moves, completeFrom: dropped.length ? completeFrom : from, ...(dropped.length ? { next: completeFrom - 1 } : next === undefined ? {} : { next }) }
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// TronGrid
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
export interface TrongridSourceOptions {
|
|
276
|
+
/** 全节点地址,默认 https://api.trongrid.io */
|
|
277
|
+
host?: string
|
|
278
|
+
apiKey?: string
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
interface TronTx {
|
|
282
|
+
txID: string
|
|
283
|
+
blockNumber?: number
|
|
284
|
+
block_timestamp?: number
|
|
285
|
+
net_fee?: number
|
|
286
|
+
energy_fee?: number
|
|
287
|
+
ret?: Array<{ contractRet?: string; fee?: number }>
|
|
288
|
+
raw_data?: { contract?: Array<{ type?: string; parameter?: { value?: Record<string, unknown> } }> }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
interface TronTrc20 {
|
|
292
|
+
transaction_id: string
|
|
293
|
+
block_timestamp: number
|
|
294
|
+
from: string
|
|
295
|
+
to: string
|
|
296
|
+
value: string
|
|
297
|
+
type?: string
|
|
298
|
+
token_info?: { symbol?: string; address?: string; decimals?: number }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function trongridSource(options: TrongridSourceOptions = {}): HistoryAdapter {
|
|
302
|
+
const host = (options.host ?? 'https://api.trongrid.io').replace(/\/+$/, '')
|
|
303
|
+
return {
|
|
304
|
+
name: 'trongrid',
|
|
305
|
+
supports: isTronChain,
|
|
306
|
+
async fetch(q) {
|
|
307
|
+
const headers: Record<string, string> = options.apiKey ? { 'TRON-PRO-API-KEY': options.apiKey } : {}
|
|
308
|
+
const qs = `limit=${Math.min(200, q.limit)}&order_by=block_timestamp,desc&only_confirmed=true${q.endPos === undefined ? '' : `&max_timestamp=${q.endPos}`}`
|
|
309
|
+
const get = async <T>(path: string): Promise<T[]> => {
|
|
310
|
+
const json = await fetchJson<{ success?: boolean; data?: T[]; error?: string }>(q.fetch, `${host}/v1/accounts/${q.address}/${path}?${qs}`, { headers }, options.apiKey)
|
|
311
|
+
if (json.success === false || !Array.isArray(json.data)) throw new Error(`trongrid: ${json.error ?? 'unexpected response'}`)
|
|
312
|
+
return json.data
|
|
313
|
+
}
|
|
314
|
+
const [txRows, trc20Rows] = await Promise.all([get<TronTx>('transactions'), get<TronTrc20>('transactions/trc20')])
|
|
315
|
+
const txs: RawTx[] = txRows.map((r) => {
|
|
316
|
+
const c = r.raw_data?.contract?.[0]
|
|
317
|
+
const v = (c?.parameter?.value ?? {}) as Record<string, string | number | undefined>
|
|
318
|
+
const hexAddr = (a: unknown) => (typeof a === 'string' && a ? toTronAddress(a) : undefined)
|
|
319
|
+
const trigger = c?.type === 'TriggerSmartContract'
|
|
320
|
+
const ms = r.block_timestamp ?? 0
|
|
321
|
+
return {
|
|
322
|
+
hash: r.txID,
|
|
323
|
+
pos: ms,
|
|
324
|
+
...(r.blockNumber === undefined ? {} : { block: r.blockNumber }),
|
|
325
|
+
time: Math.floor(ms / 1000),
|
|
326
|
+
from: hexAddr(v.owner_address) ?? q.address,
|
|
327
|
+
to: hexAddr(trigger ? v.contract_address : v.to_address) ?? null,
|
|
328
|
+
value: big(trigger ? v.call_value : v.amount),
|
|
329
|
+
input: trigger && typeof v.data === 'string' && v.data ? `0x${v.data}` : '0x',
|
|
330
|
+
ok: r.ret?.[0]?.contractRet === 'SUCCESS',
|
|
331
|
+
fee: big(r.ret?.[0]?.fee ?? (r.net_fee ?? 0) + (r.energy_fee ?? 0)),
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
const moves: RawMove[] = trc20Rows
|
|
335
|
+
.filter((r) => (r.type ?? 'Transfer') === 'Transfer')
|
|
336
|
+
.map((r) => ({ hash: r.transaction_id, pos: r.block_timestamp, time: Math.floor(r.block_timestamp / 1000), asset: r.token_info?.address ?? '', from: r.from, to: r.to, value: big(r.value), symbol: r.token_info?.symbol ?? null, decimals: r.token_info?.decimals ?? null }))
|
|
337
|
+
.filter((m) => m.asset)
|
|
338
|
+
const lists = [txRows.map((r) => r.block_timestamp ?? 0), trc20Rows.map((r) => r.block_timestamp)].map((ts) => ({ count: ts.length, lastPos: ts.length ? Math.min(...ts) : 0 }))
|
|
339
|
+
return paged(txs, moves, lists, Math.min(200, q.limit))
|
|
340
|
+
},
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
/** 来源列表 → 适配器:名字按默认配置创建;没传时按链选默认(传了 apiKey 时 etherscan 排最前) */
|
|
347
|
+
export function historySourcesFor(chainId: number, source: HistorySource | readonly HistorySource[] | undefined, apiKey?: string): HistoryAdapter[] {
|
|
348
|
+
const tron = isTronChain(chainId)
|
|
349
|
+
const defaults: HistorySource[] = tron ? ['trongrid'] : [...(apiKey ? (['etherscan'] as const) : []), ...(BLOCKSCOUT_APIS[chainId] ? (['blockscout'] as const) : []), 'rpc']
|
|
350
|
+
const list = source === undefined ? defaults : Array.isArray(source) ? source : [source as HistorySource]
|
|
351
|
+
return (list as readonly HistorySource[])
|
|
352
|
+
.map((s): HistoryAdapter => {
|
|
353
|
+
if (typeof s !== 'string') return s
|
|
354
|
+
if (s === 'blockscout') return blockscoutSource()
|
|
355
|
+
if (s === 'etherscan') return etherscanSource({ apiKey: apiKey ?? '' })
|
|
356
|
+
if (s === 'rpc') return rpcSource()
|
|
357
|
+
return trongridSource({ ...(apiKey ? { apiKey } : {}) })
|
|
358
|
+
})
|
|
359
|
+
.filter((a) => a.supports?.(chainId) !== false)
|
|
360
|
+
}
|
package/src/liquidity/v3.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from './common.js'
|
|
22
22
|
import type { IncreaseLiquidityParams, LiquidityPlan, LiquidityTx, NewPositionParams, PositionRef, RemoveLiquidityParams } from './types.js'
|
|
23
23
|
|
|
24
|
-
const npmInterface = new Interface([
|
|
24
|
+
export const npmInterface = new Interface([
|
|
25
25
|
'function WETH9() view returns (address)',
|
|
26
26
|
'function factory() view returns (address)',
|
|
27
27
|
'function ownerOf(uint256 tokenId) view returns (address)',
|
package/src/liquidity/v4.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from './common.js'
|
|
21
21
|
import type { IncreaseLiquidityParams, LiquidityPlan, NewPositionParams, PositionRef, RemoveLiquidityParams } from './types.js'
|
|
22
22
|
|
|
23
|
-
const pmInterface = new Interface([
|
|
23
|
+
export const pmInterface = new Interface([
|
|
24
24
|
'function ownerOf(uint256 tokenId) view returns (address)',
|
|
25
25
|
'function getApproved(uint256 tokenId) view returns (address)',
|
|
26
26
|
'function isApprovedForAll(address owner, address operator) view returns (bool)',
|
|
@@ -37,12 +37,13 @@ const stateViewInterface = new Interface([
|
|
|
37
37
|
])
|
|
38
38
|
|
|
39
39
|
/** v4-periphery 的 Actions */
|
|
40
|
-
const Actions = {
|
|
40
|
+
export const Actions = {
|
|
41
41
|
INCREASE_LIQUIDITY: 0x00,
|
|
42
42
|
DECREASE_LIQUIDITY: 0x01,
|
|
43
43
|
MINT_POSITION: 0x02,
|
|
44
44
|
BURN_POSITION: 0x03,
|
|
45
45
|
SETTLE_PAIR: 0x0d,
|
|
46
|
+
TAKE: 0x0e,
|
|
46
47
|
TAKE_PAIR: 0x11,
|
|
47
48
|
CLOSE_CURRENCY: 0x12,
|
|
48
49
|
SWEEP: 0x14,
|
|
@@ -65,7 +66,7 @@ interface PoolKey {
|
|
|
65
66
|
const keyTuple = (k: PoolKey) => [k.currency0, k.currency1, k.fee, k.tickSpacing, k.hooks]
|
|
66
67
|
|
|
67
68
|
/** 依次执行的 action 列表 → modifyLiquidities 的 unlockData */
|
|
68
|
-
class Planner {
|
|
69
|
+
export class Planner {
|
|
69
70
|
readonly actions: number[] = []
|
|
70
71
|
readonly params: string[] = []
|
|
71
72
|
|
package/src/lp.ts
CHANGED
|
@@ -467,7 +467,6 @@ async function v2Positions(provider: Provider, owner: string, protocols: LpProto
|
|
|
467
467
|
}
|
|
468
468
|
|
|
469
469
|
async function v3Positions(provider: Provider, owner: string, protocols: LpProtocol[], options: LpPositionsOptions): Promise<RawConcentrated[]> {
|
|
470
|
-
const { includeClosed = false } = options
|
|
471
470
|
const maxPositions = Math.floor(options.maxPositions ?? 500)
|
|
472
471
|
const overrides = callOverrides(options)
|
|
473
472
|
if (!protocols.length || !(maxPositions > 0)) {
|
|
@@ -495,7 +494,16 @@ async function v3Positions(provider: Provider, owner: string, protocols: LpProto
|
|
|
495
494
|
}
|
|
496
495
|
const tokenIds = await provider.tryAll<bigint>(indexCalls, overrides)
|
|
497
496
|
const ids = tokenIds.flatMap((id, i) => (id === null ? [] : [{ protocol: indexOwners[i] as LpProtocol, tokenId: id }]))
|
|
497
|
+
return v3Details(provider, ids, options)
|
|
498
|
+
}
|
|
498
499
|
|
|
500
|
+
/** 已知 tokenId 的 V3 仓位:读仓位信息、池子状态、算数量和未领取的手续费(不看 NFT 现在在谁手里,如质押在挖矿合约里的仓位) */
|
|
501
|
+
async function v3Details(provider: Provider, ids: ReadonlyArray<{ protocol: LpProtocol; tokenId: bigint }>, options: LpPositionsOptions): Promise<RawConcentrated[]> {
|
|
502
|
+
const { includeClosed = false } = options
|
|
503
|
+
const overrides = callOverrides(options)
|
|
504
|
+
if (!ids.length) {
|
|
505
|
+
return []
|
|
506
|
+
}
|
|
499
507
|
// 仓位详情
|
|
500
508
|
type PositionData = { token0: string; token1: string; fee: bigint; tickLower: bigint; tickUpper: bigint; liquidity: bigint; feeGrowthInside0LastX128: bigint; feeGrowthInside1LastX128: bigint; tokensOwed0: bigint; tokensOwed1: bigint }
|
|
501
509
|
const positions = await provider.tryAll<PositionData>(
|
|
@@ -723,6 +731,15 @@ export function lpPositions(provider: Provider, owner: string, options: LpPositi
|
|
|
723
731
|
return withSignal(options.signal, () => queryLpPositions(provider, owner, options))
|
|
724
732
|
}
|
|
725
733
|
|
|
734
|
+
/**
|
|
735
|
+
* 按 tokenId 查 V3 仓位(不要求持有者是谁),返回与 lpPositions 相同的结果(含美元价值)。
|
|
736
|
+
* 给 `/rewards` 读质押在挖矿合约里的仓位用
|
|
737
|
+
*/
|
|
738
|
+
export async function lpPositionsByIds(provider: Provider, ids: ReadonlyArray<{ protocol: LpProtocol; tokenId: bigint }>, options: LpPositionsOptions = {}): Promise<LpPosition[]> {
|
|
739
|
+
const chainId = await provider.getChainId()
|
|
740
|
+
return finalize(provider, chainId, await v3Details(provider, ids, options), options)
|
|
741
|
+
}
|
|
742
|
+
|
|
726
743
|
async function queryLpPositions(provider: Provider, owner: string, options: LpPositionsOptions): Promise<LpPosition[]> {
|
|
727
744
|
const chainId = await provider.getChainId()
|
|
728
745
|
const protocols = resolveLpProtocols(chainId, options)
|
package/src/lpV4.ts
CHANGED
|
@@ -94,6 +94,10 @@ export interface V4ScanProgress {
|
|
|
94
94
|
done: boolean
|
|
95
95
|
/** 节点不开放 getLogs 时暂停到这个时间(毫秒时间戳),之前的调用不再扫描 */
|
|
96
96
|
pausedUntil?: number
|
|
97
|
+
/** 地址没有持有这个仓位合约的 NFT(balanceOf 为 0):没有扫描,done 为 true */
|
|
98
|
+
skipped?: true
|
|
99
|
+
/** 地址当前持有这个仓位合约的 NFT 个数(balanceOf;查不到时没有这个字段) */
|
|
100
|
+
held?: number
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
export interface LpV4Options {
|
|
@@ -113,6 +117,7 @@ const DYNAMIC_FEE_FLAG = 0x800000
|
|
|
113
117
|
|
|
114
118
|
const POSITION_MANAGER_ABI = [
|
|
115
119
|
'function ownerOf(uint256 tokenId) view returns (address)',
|
|
120
|
+
'function balanceOf(address owner) view returns (uint256)',
|
|
116
121
|
'function getPoolAndPositionInfo(uint256 tokenId) view returns ((address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) poolKey, uint256 info)',
|
|
117
122
|
'function getPositionLiquidity(uint256 tokenId) view returns (uint128)',
|
|
118
123
|
]
|
|
@@ -164,6 +169,7 @@ async function scanTokenIds(
|
|
|
164
169
|
owner: string,
|
|
165
170
|
options: V4ScanOptions,
|
|
166
171
|
signal: AbortSignal | undefined,
|
|
172
|
+
held: number | undefined,
|
|
167
173
|
): Promise<{ ids: string[]; cursor: number; prune(ids: readonly string[]): Promise<void> }> {
|
|
168
174
|
const storage = options.storage ?? defaultStorage
|
|
169
175
|
const key = `${chainId}:${(protocol.positionManager as string).toLowerCase()}:${owner.toLowerCase()}`
|
|
@@ -188,6 +194,7 @@ async function scanTokenIds(
|
|
|
188
194
|
target,
|
|
189
195
|
done: target !== null && state.cursor >= target,
|
|
190
196
|
...(state.pausedUntil ? { pausedUntil: state.pausedUntil } : {}),
|
|
197
|
+
...(held === undefined ? {} : { held }),
|
|
191
198
|
})
|
|
192
199
|
} catch {
|
|
193
200
|
// 回调出错不影响扫描和保存进度
|
|
@@ -284,10 +291,33 @@ export async function v4Positions(
|
|
|
284
291
|
return []
|
|
285
292
|
}
|
|
286
293
|
|
|
294
|
+
// 先查持有几个仓位 NFT:一个都没有就不用扫描(最常见的情况;扫描在公共节点上要很久)。查不到时照常扫描
|
|
295
|
+
let held: unknown[] = []
|
|
296
|
+
if (scanOptions) {
|
|
297
|
+
try {
|
|
298
|
+
held = await provider.tryAll(
|
|
299
|
+
protocols.map((p) => provider.contract(p.positionManager as string, POSITION_MANAGER_ABI).balanceOf(owner)),
|
|
300
|
+
overrides,
|
|
301
|
+
)
|
|
302
|
+
} catch (err) {
|
|
303
|
+
if (overrides.signal?.aborted) throw err
|
|
304
|
+
// 节点出错 / 限频:不影响查询,照常扫描
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const skip = (protocol: V4Protocol) => {
|
|
308
|
+
try {
|
|
309
|
+
scanOptions?.onProgress?.({ chainId, positionManager: protocol.positionManager as string, startBlock: -1, cursor: -1, target: null, done: true, skipped: true, held: 0 })
|
|
310
|
+
} catch {
|
|
311
|
+
// 回调出错不影响查询
|
|
312
|
+
}
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
315
|
+
|
|
287
316
|
// 每个协议的候选 tokenId:太多时只核对最大的 maxPositions 个(tokenId 递增,越大越新)
|
|
288
317
|
const candidates = await Promise.all(
|
|
289
|
-
protocols.map(async (protocol) => {
|
|
290
|
-
const
|
|
318
|
+
protocols.map(async (protocol, i) => {
|
|
319
|
+
const count = typeof held[i] === 'bigint' ? Number(held[i]) : undefined
|
|
320
|
+
const scan = !scanOptions ? null : count === 0 ? skip(protocol) : await scanTokenIds(provider, chainId, protocol, owner, scanOptions, overrides.signal, count)
|
|
291
321
|
const ids = [...new Set([...explicit, ...(scan?.ids ?? [])])].map(BigInt).sort((a, b) => (a < b ? 1 : a > b ? -1 : 0))
|
|
292
322
|
return { protocol, ids: ids.slice(0, limit), scan }
|
|
293
323
|
}),
|