@w3lib/evm-batch-call 0.6.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 +54 -0
- package/README.md +367 -3
- package/contracts/TokenProbe.sol +103 -0
- package/dist/aggregate.d.ts.map +1 -1
- package/dist/aggregate.js +114 -3
- package/dist/aggregate.js.map +1 -1
- package/dist/approvals.d.ts +169 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +310 -0
- package/dist/approvals.js.map +1 -0
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +4 -0
- package/dist/chains.js.map +1 -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/fees.d.ts +97 -0
- package/dist/fees.d.ts.map +1 -0
- package/dist/fees.js +115 -0
- package/dist/fees.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/http.d.ts +10 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +30 -0
- package/dist/http.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 +10 -1
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +19 -2
- 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/multicall.d.ts.map +1 -1
- package/dist/multicall.js +3 -0
- package/dist/multicall.js.map +1 -1
- package/dist/names.d.ts +55 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +93 -0
- package/dist/names.js.map +1 -0
- package/dist/owner.d.ts +4 -26
- package/dist/owner.d.ts.map +1 -1
- package/dist/owner.js +7 -112
- package/dist/owner.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 +56 -0
- package/dist/portfolio.d.ts.map +1 -0
- package/dist/portfolio.js +54 -0
- package/dist/portfolio.js.map +1 -0
- package/dist/price.d.ts +95 -0
- package/dist/price.d.ts.map +1 -0
- package/dist/price.js +329 -0
- package/dist/price.js.map +1 -0
- 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/risk.d.ts +42 -0
- package/dist/risk.d.ts.map +1 -0
- package/dist/risk.js +134 -0
- package/dist/risk.js.map +1 -0
- package/dist/rpcNodes.d.ts.map +1 -1
- package/dist/rpcNodes.js +18 -0
- package/dist/rpcNodes.js.map +1 -1
- 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/approvals.d.ts +26 -0
- package/dist/subpaths/approvals.d.ts.map +1 -0
- package/dist/subpaths/approvals.js +27 -0
- package/dist/subpaths/approvals.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/fees.d.ts +23 -0
- package/dist/subpaths/fees.d.ts.map +1 -0
- package/dist/subpaths/fees.js +25 -0
- package/dist/subpaths/fees.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/names.d.ts +35 -0
- package/dist/subpaths/names.d.ts.map +1 -0
- package/dist/subpaths/names.js +39 -0
- package/dist/subpaths/names.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.d.ts +38 -0
- package/dist/subpaths/price.d.ts.map +1 -0
- package/dist/subpaths/price.js +40 -0
- package/dist/subpaths/price.js.map +1 -0
- 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/risk.d.ts +19 -0
- package/dist/subpaths/risk.d.ts.map +1 -0
- package/dist/subpaths/risk.js +20 -0
- package/dist/subpaths/risk.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/subpaths/swap.d.ts +19 -0
- package/dist/subpaths/swap.d.ts.map +1 -0
- package/dist/subpaths/swap.js +20 -0
- package/dist/subpaths/swap.js.map +1 -0
- package/dist/swap.d.ts +168 -0
- package/dist/swap.d.ts.map +1 -0
- package/dist/swap.js +387 -0
- package/dist/swap.js.map +1 -0
- package/dist/tokenProbe.d.ts +3 -0
- package/dist/tokenProbe.d.ts.map +1 -0
- package/dist/tokenProbe.js +4 -0
- package/dist/tokenProbe.js.map +1 -0
- 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/dist/write/tron.d.ts +25 -0
- package/dist/write/tron.d.ts.map +1 -1
- package/dist/write/tron.js +26 -0
- package/dist/write/tron.js.map +1 -1
- package/package.json +73 -16
- package/src/aggregate.ts +110 -3
- package/src/approvals.ts +462 -0
- package/src/chains.ts +4 -0
- package/src/dividends.ts +360 -0
- package/src/fees.ts +201 -0
- package/src/history.ts +367 -0
- package/src/historySources.ts +360 -0
- package/src/http.ts +34 -0
- package/src/liquidity/v3.ts +1 -1
- package/src/liquidity/v4.ts +4 -3
- package/src/lp.ts +21 -2
- package/src/lpV4.ts +32 -2
- package/src/multicall.ts +3 -0
- package/src/names.ts +141 -0
- package/src/owner.ts +7 -138
- package/src/permit.ts +457 -0
- package/src/portfolio.ts +98 -0
- package/src/price.ts +415 -0
- package/src/rewards/farms.ts +229 -0
- package/src/rewards/index.ts +431 -0
- package/src/rewards/types.ts +179 -0
- package/src/risk.ts +166 -0
- package/src/rpcNodes.ts +18 -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/approvals.ts +48 -0
- package/src/subpaths/dividends.ts +47 -0
- package/src/subpaths/fees.ts +43 -0
- package/src/subpaths/history.ts +41 -0
- package/src/subpaths/names.ts +43 -0
- package/src/subpaths/permit.ts +39 -0
- package/src/subpaths/price.ts +60 -0
- package/src/subpaths/rewards.ts +46 -0
- package/src/subpaths/risk.ts +23 -0
- package/src/subpaths/simulate.ts +51 -0
- package/src/subpaths/smart-account.ts +40 -0
- package/src/subpaths/swap.ts +36 -0
- package/src/swap.ts +534 -0
- package/src/tokenProbe.ts +5 -0
- 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
- package/src/write/tron.ts +43 -0
|
@@ -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/http.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** HTTP 请求(代币列表、价格服务):超时、状态码检查 */
|
|
2
|
+
|
|
3
|
+
const REQUEST_TIMEOUT = 20_000
|
|
4
|
+
|
|
5
|
+
export class HttpStatusError extends Error {
|
|
6
|
+
constructor(
|
|
7
|
+
readonly status: number,
|
|
8
|
+
target: string,
|
|
9
|
+
) {
|
|
10
|
+
super(`HTTP ${status} from ${target}`)
|
|
11
|
+
this.name = 'HttpStatusError'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param secret 请求里带的 Key:出错时错误信息只写域名,不写完整地址,避免 Key 出现在日志里
|
|
17
|
+
*/
|
|
18
|
+
export function fetchJson<T>(fetchFn: typeof fetch, url: string, init?: RequestInit, secret?: string): Promise<T> {
|
|
19
|
+
return fetchFn(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT) }).then(async (res) => {
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
throw new HttpStatusError(res.status, secret ? safeHost(url) : url)
|
|
22
|
+
}
|
|
23
|
+
return (await res.json()) as T
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeHost(url: string): string {
|
|
28
|
+
try {
|
|
29
|
+
return new URL(url).host
|
|
30
|
+
} catch {
|
|
31
|
+
return 'request'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
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
|
@@ -3,7 +3,8 @@ import type { Call } from './call.js'
|
|
|
3
3
|
import { formatUnits } from './erc20.js'
|
|
4
4
|
import { getAmountsForLiquidity, getFeeGrowthInside, getFeesEarned, getSqrtRatioAtTick, sqrtPriceToPrice, tickToPrice } from './lpMath.js'
|
|
5
5
|
import { v4Positions, type LpV4Options } from './lpV4.js'
|
|
6
|
-
import
|
|
6
|
+
import type { TokenSource } from './owner.js'
|
|
7
|
+
import { defillamaPrices, type PriceSource } from './price.js'
|
|
7
8
|
import type { Provider } from './provider.js'
|
|
8
9
|
import { TRON_CHAIN_ID, isTronAddress, isTronChain, toEvmAddress, toTronAddress } from './tron.js'
|
|
9
10
|
import { withSignal } from './util.js'
|
|
@@ -466,7 +467,6 @@ async function v2Positions(provider: Provider, owner: string, protocols: LpProto
|
|
|
466
467
|
}
|
|
467
468
|
|
|
468
469
|
async function v3Positions(provider: Provider, owner: string, protocols: LpProtocol[], options: LpPositionsOptions): Promise<RawConcentrated[]> {
|
|
469
|
-
const { includeClosed = false } = options
|
|
470
470
|
const maxPositions = Math.floor(options.maxPositions ?? 500)
|
|
471
471
|
const overrides = callOverrides(options)
|
|
472
472
|
if (!protocols.length || !(maxPositions > 0)) {
|
|
@@ -494,7 +494,16 @@ async function v3Positions(provider: Provider, owner: string, protocols: LpProto
|
|
|
494
494
|
}
|
|
495
495
|
const tokenIds = await provider.tryAll<bigint>(indexCalls, overrides)
|
|
496
496
|
const ids = tokenIds.flatMap((id, i) => (id === null ? [] : [{ protocol: indexOwners[i] as LpProtocol, tokenId: id }]))
|
|
497
|
+
return v3Details(provider, ids, options)
|
|
498
|
+
}
|
|
497
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
|
+
}
|
|
498
507
|
// 仓位详情
|
|
499
508
|
type PositionData = { token0: string; token1: string; fee: bigint; tickLower: bigint; tickUpper: bigint; liquidity: bigint; feeGrowthInside0LastX128: bigint; feeGrowthInside1LastX128: bigint; tokensOwed0: bigint; tokensOwed1: bigint }
|
|
500
509
|
const positions = await provider.tryAll<PositionData>(
|
|
@@ -629,6 +638,7 @@ async function finalize(provider: Provider, chainId: number, raw: RawPosition[],
|
|
|
629
638
|
tokens: addresses.filter((a) => !info.get(a)?.native).map((a) => (tron ? toTronAddress(a) : a)),
|
|
630
639
|
nativeSymbol: nativeToken ? nativeToken.symbol : null,
|
|
631
640
|
fetch: fetchFn,
|
|
641
|
+
provider,
|
|
632
642
|
})
|
|
633
643
|
} catch (err) {
|
|
634
644
|
// 同步抛错和 reject 都在这里接住
|
|
@@ -721,6 +731,15 @@ export function lpPositions(provider: Provider, owner: string, options: LpPositi
|
|
|
721
731
|
return withSignal(options.signal, () => queryLpPositions(provider, owner, options))
|
|
722
732
|
}
|
|
723
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
|
+
|
|
724
743
|
async function queryLpPositions(provider: Provider, owner: string, options: LpPositionsOptions): Promise<LpPosition[]> {
|
|
725
744
|
const chainId = await provider.getChainId()
|
|
726
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
|
}),
|
package/src/multicall.ts
CHANGED
|
@@ -81,6 +81,9 @@ const MULTICALL3: Record<number, Multicall> = {
|
|
|
81
81
|
80094: { address: CANONICAL, block: 0 }, // Berachain,创世预置
|
|
82
82
|
534352: { address: CANONICAL, block: 14 }, // Scroll
|
|
83
83
|
86233268: { address: CANONICAL, block: 10854614 }, // Winchain,部署区块未查到
|
|
84
|
+
1030: { address: CANONICAL, block: 159584122 }, // Conflux eSpace,部署区块未查到
|
|
85
|
+
1672: { address: CANONICAL, block: 20812125 }, // Pharos,部署区块未查到
|
|
86
|
+
1776: { address: CANONICAL, block: 188237272 }, // Injective EVM,部署区块未查到
|
|
84
87
|
// 以下两条链不支持 deployless(合约创建式 eth_call 返回空 / 格式不对),只能用 Multicall3;
|
|
85
88
|
// 早于部署区块的历史查询会失败
|
|
86
89
|
42262: { address: CANONICAL, block: 22380155 }, // Oasis Emerald,部署区块未查到
|