@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.
Files changed (248) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +367 -3
  3. package/contracts/TokenProbe.sol +103 -0
  4. package/dist/aggregate.d.ts.map +1 -1
  5. package/dist/aggregate.js +114 -3
  6. package/dist/aggregate.js.map +1 -1
  7. package/dist/approvals.d.ts +169 -0
  8. package/dist/approvals.d.ts.map +1 -0
  9. package/dist/approvals.js +310 -0
  10. package/dist/approvals.js.map +1 -0
  11. package/dist/chains.d.ts.map +1 -1
  12. package/dist/chains.js +4 -0
  13. package/dist/chains.js.map +1 -1
  14. package/dist/dividends.d.ts +135 -0
  15. package/dist/dividends.d.ts.map +1 -0
  16. package/dist/dividends.js +253 -0
  17. package/dist/dividends.js.map +1 -0
  18. package/dist/fees.d.ts +97 -0
  19. package/dist/fees.d.ts.map +1 -0
  20. package/dist/fees.js +115 -0
  21. package/dist/fees.js.map +1 -0
  22. package/dist/history.d.ts +154 -0
  23. package/dist/history.d.ts.map +1 -0
  24. package/dist/history.js +247 -0
  25. package/dist/history.js.map +1 -0
  26. package/dist/historySources.d.ts +32 -0
  27. package/dist/historySources.d.ts.map +1 -0
  28. package/dist/historySources.js +319 -0
  29. package/dist/historySources.js.map +1 -0
  30. package/dist/http.d.ts +10 -0
  31. package/dist/http.d.ts.map +1 -0
  32. package/dist/http.js +30 -0
  33. package/dist/http.js.map +1 -0
  34. package/dist/liquidity/v3.d.ts +2 -0
  35. package/dist/liquidity/v3.d.ts.map +1 -1
  36. package/dist/liquidity/v3.js +1 -1
  37. package/dist/liquidity/v3.js.map +1 -1
  38. package/dist/liquidity/v4.d.ts +21 -0
  39. package/dist/liquidity/v4.d.ts.map +1 -1
  40. package/dist/liquidity/v4.js +4 -3
  41. package/dist/liquidity/v4.js.map +1 -1
  42. package/dist/lp.d.ts +10 -1
  43. package/dist/lp.d.ts.map +1 -1
  44. package/dist/lp.js +19 -2
  45. package/dist/lp.js.map +1 -1
  46. package/dist/lpV4.d.ts +4 -0
  47. package/dist/lpV4.d.ts.map +1 -1
  48. package/dist/lpV4.js +27 -3
  49. package/dist/lpV4.js.map +1 -1
  50. package/dist/multicall.d.ts.map +1 -1
  51. package/dist/multicall.js +3 -0
  52. package/dist/multicall.js.map +1 -1
  53. package/dist/names.d.ts +55 -0
  54. package/dist/names.d.ts.map +1 -0
  55. package/dist/names.js +93 -0
  56. package/dist/names.js.map +1 -0
  57. package/dist/owner.d.ts +4 -26
  58. package/dist/owner.d.ts.map +1 -1
  59. package/dist/owner.js +7 -112
  60. package/dist/owner.js.map +1 -1
  61. package/dist/permit.d.ts +137 -0
  62. package/dist/permit.d.ts.map +1 -0
  63. package/dist/permit.js +355 -0
  64. package/dist/permit.js.map +1 -0
  65. package/dist/portfolio.d.ts +56 -0
  66. package/dist/portfolio.d.ts.map +1 -0
  67. package/dist/portfolio.js +54 -0
  68. package/dist/portfolio.js.map +1 -0
  69. package/dist/price.d.ts +95 -0
  70. package/dist/price.d.ts.map +1 -0
  71. package/dist/price.js +329 -0
  72. package/dist/price.js.map +1 -0
  73. package/dist/rewards/farms.d.ts +26 -0
  74. package/dist/rewards/farms.d.ts.map +1 -0
  75. package/dist/rewards/farms.js +210 -0
  76. package/dist/rewards/farms.js.map +1 -0
  77. package/dist/rewards/index.d.ts +42 -0
  78. package/dist/rewards/index.d.ts.map +1 -0
  79. package/dist/rewards/index.js +382 -0
  80. package/dist/rewards/index.js.map +1 -0
  81. package/dist/rewards/types.d.ts +170 -0
  82. package/dist/rewards/types.d.ts.map +1 -0
  83. package/dist/rewards/types.js +2 -0
  84. package/dist/rewards/types.js.map +1 -0
  85. package/dist/risk.d.ts +42 -0
  86. package/dist/risk.d.ts.map +1 -0
  87. package/dist/risk.js +134 -0
  88. package/dist/risk.js.map +1 -0
  89. package/dist/rpcNodes.d.ts.map +1 -1
  90. package/dist/rpcNodes.js +18 -0
  91. package/dist/rpcNodes.js.map +1 -1
  92. package/dist/simulate.d.ts +186 -0
  93. package/dist/simulate.d.ts.map +1 -0
  94. package/dist/simulate.js +559 -0
  95. package/dist/simulate.js.map +1 -0
  96. package/dist/simulateDecode.d.ts +33 -0
  97. package/dist/simulateDecode.d.ts.map +1 -0
  98. package/dist/simulateDecode.js +321 -0
  99. package/dist/simulateDecode.js.map +1 -0
  100. package/dist/smartAccount/account.d.ts +31 -0
  101. package/dist/smartAccount/account.d.ts.map +1 -0
  102. package/dist/smartAccount/account.js +33 -0
  103. package/dist/smartAccount/account.js.map +1 -0
  104. package/dist/smartAccount/clients.d.ts +77 -0
  105. package/dist/smartAccount/clients.d.ts.map +1 -0
  106. package/dist/smartAccount/clients.js +122 -0
  107. package/dist/smartAccount/clients.js.map +1 -0
  108. package/dist/smartAccount/eip5792.d.ts +77 -0
  109. package/dist/smartAccount/eip5792.d.ts.map +1 -0
  110. package/dist/smartAccount/eip5792.js +152 -0
  111. package/dist/smartAccount/eip5792.js.map +1 -0
  112. package/dist/smartAccount/send.d.ts +40 -0
  113. package/dist/smartAccount/send.d.ts.map +1 -0
  114. package/dist/smartAccount/send.js +271 -0
  115. package/dist/smartAccount/send.js.map +1 -0
  116. package/dist/smartAccount/userOp.d.ts +91 -0
  117. package/dist/smartAccount/userOp.d.ts.map +1 -0
  118. package/dist/smartAccount/userOp.js +124 -0
  119. package/dist/smartAccount/userOp.js.map +1 -0
  120. package/dist/subpaths/approvals.d.ts +26 -0
  121. package/dist/subpaths/approvals.d.ts.map +1 -0
  122. package/dist/subpaths/approvals.js +27 -0
  123. package/dist/subpaths/approvals.js.map +1 -0
  124. package/dist/subpaths/dividends.d.ts +28 -0
  125. package/dist/subpaths/dividends.d.ts.map +1 -0
  126. package/dist/subpaths/dividends.js +29 -0
  127. package/dist/subpaths/dividends.js.map +1 -0
  128. package/dist/subpaths/fees.d.ts +23 -0
  129. package/dist/subpaths/fees.d.ts.map +1 -0
  130. package/dist/subpaths/fees.js +25 -0
  131. package/dist/subpaths/fees.js.map +1 -0
  132. package/dist/subpaths/history.d.ts +22 -0
  133. package/dist/subpaths/history.d.ts.map +1 -0
  134. package/dist/subpaths/history.js +23 -0
  135. package/dist/subpaths/history.js.map +1 -0
  136. package/dist/subpaths/names.d.ts +35 -0
  137. package/dist/subpaths/names.d.ts.map +1 -0
  138. package/dist/subpaths/names.js +39 -0
  139. package/dist/subpaths/names.js.map +1 -0
  140. package/dist/subpaths/permit.d.ts +14 -0
  141. package/dist/subpaths/permit.d.ts.map +1 -0
  142. package/dist/subpaths/permit.js +14 -0
  143. package/dist/subpaths/permit.js.map +1 -0
  144. package/dist/subpaths/price.d.ts +38 -0
  145. package/dist/subpaths/price.d.ts.map +1 -0
  146. package/dist/subpaths/price.js +40 -0
  147. package/dist/subpaths/price.js.map +1 -0
  148. package/dist/subpaths/rewards.d.ts +27 -0
  149. package/dist/subpaths/rewards.d.ts.map +1 -0
  150. package/dist/subpaths/rewards.js +25 -0
  151. package/dist/subpaths/rewards.js.map +1 -0
  152. package/dist/subpaths/risk.d.ts +19 -0
  153. package/dist/subpaths/risk.d.ts.map +1 -0
  154. package/dist/subpaths/risk.js +20 -0
  155. package/dist/subpaths/risk.js.map +1 -0
  156. package/dist/subpaths/simulate.d.ts +25 -0
  157. package/dist/subpaths/simulate.d.ts.map +1 -0
  158. package/dist/subpaths/simulate.js +28 -0
  159. package/dist/subpaths/simulate.js.map +1 -0
  160. package/dist/subpaths/smart-account.d.ts +24 -0
  161. package/dist/subpaths/smart-account.d.ts.map +1 -0
  162. package/dist/subpaths/smart-account.js +24 -0
  163. package/dist/subpaths/smart-account.js.map +1 -0
  164. package/dist/subpaths/swap.d.ts +19 -0
  165. package/dist/subpaths/swap.d.ts.map +1 -0
  166. package/dist/subpaths/swap.js +20 -0
  167. package/dist/subpaths/swap.js.map +1 -0
  168. package/dist/swap.d.ts +168 -0
  169. package/dist/swap.d.ts.map +1 -0
  170. package/dist/swap.js +387 -0
  171. package/dist/swap.js.map +1 -0
  172. package/dist/tokenProbe.d.ts +3 -0
  173. package/dist/tokenProbe.d.ts.map +1 -0
  174. package/dist/tokenProbe.js +4 -0
  175. package/dist/tokenProbe.js.map +1 -0
  176. package/dist/write/batch.d.ts +34 -2
  177. package/dist/write/batch.d.ts.map +1 -1
  178. package/dist/write/batch.js +22 -3
  179. package/dist/write/batch.js.map +1 -1
  180. package/dist/write/collect.d.ts +9 -0
  181. package/dist/write/collect.d.ts.map +1 -1
  182. package/dist/write/collect.js +41 -1
  183. package/dist/write/collect.js.map +1 -1
  184. package/dist/write/send.d.ts +4 -0
  185. package/dist/write/send.d.ts.map +1 -1
  186. package/dist/write/send.js.map +1 -1
  187. package/dist/write/signer.d.ts +5 -1
  188. package/dist/write/signer.d.ts.map +1 -1
  189. package/dist/write/signer.js.map +1 -1
  190. package/dist/write/transfer.d.ts +5 -3
  191. package/dist/write/transfer.d.ts.map +1 -1
  192. package/dist/write/transfer.js +12 -6
  193. package/dist/write/transfer.js.map +1 -1
  194. package/dist/write/tron.d.ts +25 -0
  195. package/dist/write/tron.d.ts.map +1 -1
  196. package/dist/write/tron.js +26 -0
  197. package/dist/write/tron.js.map +1 -1
  198. package/package.json +73 -16
  199. package/src/aggregate.ts +110 -3
  200. package/src/approvals.ts +462 -0
  201. package/src/chains.ts +4 -0
  202. package/src/dividends.ts +360 -0
  203. package/src/fees.ts +201 -0
  204. package/src/history.ts +367 -0
  205. package/src/historySources.ts +360 -0
  206. package/src/http.ts +34 -0
  207. package/src/liquidity/v3.ts +1 -1
  208. package/src/liquidity/v4.ts +4 -3
  209. package/src/lp.ts +21 -2
  210. package/src/lpV4.ts +32 -2
  211. package/src/multicall.ts +3 -0
  212. package/src/names.ts +141 -0
  213. package/src/owner.ts +7 -138
  214. package/src/permit.ts +457 -0
  215. package/src/portfolio.ts +98 -0
  216. package/src/price.ts +415 -0
  217. package/src/rewards/farms.ts +229 -0
  218. package/src/rewards/index.ts +431 -0
  219. package/src/rewards/types.ts +179 -0
  220. package/src/risk.ts +166 -0
  221. package/src/rpcNodes.ts +18 -0
  222. package/src/simulate.ts +727 -0
  223. package/src/simulateDecode.ts +352 -0
  224. package/src/smartAccount/account.ts +63 -0
  225. package/src/smartAccount/clients.ts +180 -0
  226. package/src/smartAccount/eip5792.ts +187 -0
  227. package/src/smartAccount/send.ts +341 -0
  228. package/src/smartAccount/userOp.ts +180 -0
  229. package/src/subpaths/approvals.ts +48 -0
  230. package/src/subpaths/dividends.ts +47 -0
  231. package/src/subpaths/fees.ts +43 -0
  232. package/src/subpaths/history.ts +41 -0
  233. package/src/subpaths/names.ts +43 -0
  234. package/src/subpaths/permit.ts +39 -0
  235. package/src/subpaths/price.ts +60 -0
  236. package/src/subpaths/rewards.ts +46 -0
  237. package/src/subpaths/risk.ts +23 -0
  238. package/src/subpaths/simulate.ts +51 -0
  239. package/src/subpaths/smart-account.ts +40 -0
  240. package/src/subpaths/swap.ts +36 -0
  241. package/src/swap.ts +534 -0
  242. package/src/tokenProbe.ts +5 -0
  243. package/src/write/batch.ts +44 -5
  244. package/src/write/collect.ts +60 -1
  245. package/src/write/send.ts +4 -0
  246. package/src/write/signer.ts +5 -1
  247. package/src/write/transfer.ts +17 -10
  248. package/src/write/tron.ts +43 -0
package/src/history.ts ADDED
@@ -0,0 +1,367 @@
1
+ /**
2
+ * 交易历史(子路径 `@w3lib/evm-batch-call/history`):一个地址的转账 / 兑换 / 授权 / 流动性 / 铸造记录,统一成 Activity。
3
+ * 数据来源是可替换的适配器(见 historySources.ts):Blockscout、Etherscan 系(需要 Key)、纯 RPC 日志、Tron 的 TronGrid;
4
+ * `source: [a, b]` 按顺序故障切换。适配器只返回原始记录(交易、转账),这里负责合并、解码和分类。
5
+ */
6
+ import { getAddress } from 'ethers'
7
+
8
+ import { getNativeCurrency } from './chains.js'
9
+ import { formatUnits } from './erc20.js'
10
+ import type { Provider } from './provider.js'
11
+ import { decodeCalldata, type DecodedCall } from './simulateDecode.js'
12
+ import { historySourcesFor, type HistorySource } from './historySources.js'
13
+ import { isTronChain, toEvmAddress, toTronAddress } from './tron.js'
14
+
15
+ export type ActivityType = 'transfer' | 'swap' | 'approve' | 'liquidity' | 'mint' | 'contract' | 'other'
16
+
17
+ export interface ActivityChange {
18
+ /** 代币 / NFT 合约地址;主币是 'native' */
19
+ asset: string
20
+ symbol: string | null
21
+ decimals: number
22
+ /** 带符号的最小单位数量 */
23
+ delta: string
24
+ formatted: string
25
+ /** NFT(721 / 1155):tokenId */
26
+ tokenId?: string
27
+ }
28
+
29
+ export interface Activity {
30
+ hash: string
31
+ /** 秒级时间戳 */
32
+ time: number
33
+ block?: number
34
+ status: 'success' | 'failed'
35
+ type: ActivityType
36
+ /** 转账 / 铸造的方向(自己转给自己是 self);兑换等没有 */
37
+ direction?: 'in' | 'out' | 'self'
38
+ /** 对方地址(转账的另一方;合约调用是合约地址) */
39
+ counterparty?: string
40
+ /** 一句话描述 */
41
+ label?: string
42
+ /** 这个地址的资产变化(失败的交易没有) */
43
+ changes: ActivityChange[]
44
+ /** 手续费(最小单位的主币,只有这个地址是发送者时)。Tron 为 sun */
45
+ fee?: string
46
+ protocol?: string
47
+ /** 调用的方法名(能解码时) */
48
+ method?: string
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // 适配器返回的原始记录
53
+ // ---------------------------------------------------------------------------
54
+
55
+ export interface RawTx {
56
+ hash: string
57
+ /** 位置:EVM 是区块号,Tron 是毫秒时间戳(分页用) */
58
+ pos: number
59
+ block?: number
60
+ time: number
61
+ from: string
62
+ to?: string | null
63
+ /** 附带的主币 */
64
+ value: bigint
65
+ /** calldata(0x 开头) */
66
+ input: string
67
+ ok: boolean
68
+ fee?: bigint
69
+ }
70
+
71
+ export interface RawMove {
72
+ hash: string
73
+ pos: number
74
+ block?: number
75
+ time: number
76
+ /** 'native' 或代币合约地址 */
77
+ asset: string
78
+ from: string
79
+ to: string
80
+ value: bigint
81
+ tokenId?: string
82
+ symbol?: string | null
83
+ decimals?: number | null
84
+ }
85
+
86
+ export interface RawPage {
87
+ txs: RawTx[]
88
+ moves: RawMove[]
89
+ /** 位置 ≥ 这个值的记录是完整的(列表按条数截断时,最后一个位置上的记录可能只拿到一部分);没有更多历史时为 0 */
90
+ completeFrom: number
91
+ /** 下一页查到这个位置(含)为止,往旧的方向;没有更多历史时没有 */
92
+ next?: number
93
+ }
94
+
95
+ export interface HistoryQuery {
96
+ /** 要查的地址:EVM 是 0x 小写,Tron 是 T 开头 */
97
+ address: string
98
+ chainId: number
99
+ /** 每次从各个列表取多少条 */
100
+ limit: number
101
+ /** 只查位置 ≤ 这个值的记录(分页用,不传从最新开始) */
102
+ endPos?: number
103
+ /** 是否包含 NFT 转账 */
104
+ nft: boolean
105
+ provider: Provider
106
+ fetch: typeof fetch
107
+ signal?: AbortSignal
108
+ }
109
+
110
+ /** 数据来源适配器:按查询返回原始记录 */
111
+ export interface HistoryAdapter {
112
+ name: string
113
+ /** 这个来源是否支持这条链(不支持的在故障切换里直接跳过) */
114
+ supports?(chainId: number): boolean
115
+ fetch(query: HistoryQuery): Promise<RawPage>
116
+ }
117
+
118
+ export interface HistoryOptions {
119
+ /** 数据来源:名字('blockscout' | 'etherscan' | 'rpc' | 'trongrid')、适配器对象,或它们的数组(按顺序故障切换)。默认:有 Blockscout 的链 [blockscout, rpc],否则 [rpc];传了 apiKey 时 etherscan 排最前;Tron 是 trongrid */
120
+ source?: HistorySource | readonly HistorySource[]
121
+ /** Etherscan 系(v2,一个 Key 覆盖多条链)/ TronGrid 的 API Key */
122
+ apiKey?: string
123
+ /** 至少返回多少条(按区块整块取,可能略多;历史到头时可能不足) */
124
+ limit?: number
125
+ /** 上一次返回的 nextCursor */
126
+ cursor?: string
127
+ /** 只要这些类型(过滤后不够 limit 会继续往前翻页,直到 maxPages) */
128
+ types?: readonly ActivityType[]
129
+ /** 时间范围(秒级时间戳):只要这个时间之前 / 之后的;往前翻到早于 after 就停 */
130
+ before?: number
131
+ after?: number
132
+ /** 是否包含 NFT 转账,默认 true */
133
+ nft?: boolean
134
+ /** 为凑够 limit 最多翻几页,默认 5 */
135
+ maxPages?: number
136
+ fetch?: typeof fetch
137
+ signal?: AbortSignal
138
+ }
139
+
140
+ export interface HistoryResult {
141
+ items: Activity[]
142
+ /** 还有更早的历史时给出,传给下一次的 cursor */
143
+ nextCursor?: string
144
+ /** 实际用的数据来源 */
145
+ source: string
146
+ }
147
+
148
+ export class HistorySourceError extends Error {
149
+ constructor(readonly errors: ReadonlyArray<{ source: string; error: unknown }>) {
150
+ super(`All history sources failed: ${errors.map((e) => `${e.source}: ${e.error instanceof Error ? e.error.message : String(e.error)}`).join(' | ')}`)
151
+ this.name = 'HistorySourceError'
152
+ }
153
+ }
154
+
155
+ const ZERO = '0x0000000000000000000000000000000000000000'
156
+ const CURSOR_RE = /^p:(\d+)$/
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // 合并 + 分类
160
+ // ---------------------------------------------------------------------------
161
+
162
+ interface BuildCtx {
163
+ chainId: number
164
+ tron: boolean
165
+ /** 比较用的 0x 小写地址 */
166
+ me: string
167
+ native: { symbol: string | null; decimals: number }
168
+ }
169
+
170
+ const short = (a: string) => `${a.slice(0, 6)}…${a.slice(-4)}`
171
+
172
+ /** 地址的比较键:EVM 小写 0x;Tron 的 T 地址 / 41 开头的 hex 地址转成 0x 小写 */
173
+ function key(ctx: BuildCtx, address: string): string {
174
+ if (!ctx.tron) return address.toLowerCase()
175
+ try {
176
+ return toEvmAddress(address).toLowerCase()
177
+ } catch {
178
+ return address.toLowerCase()
179
+ }
180
+ }
181
+
182
+ const show = (ctx: BuildCtx, address: string): string => {
183
+ try {
184
+ return ctx.tron ? toTronAddress(address) : getAddress(address)
185
+ } catch {
186
+ return address
187
+ }
188
+ }
189
+
190
+ function changesOf(ctx: BuildCtx, moves: readonly RawMove[]): { changes: ActivityChange[]; mints: boolean; partners: Map<string, bigint> } {
191
+ const net = new Map<string, ActivityChange & { raw: bigint }>()
192
+ const partners = new Map<string, bigint>()
193
+ let mints = false
194
+ for (const m of moves) {
195
+ const from = key(ctx, m.from)
196
+ const to = key(ctx, m.to)
197
+ if (from === to) continue
198
+ const sign = to === ctx.me ? 1n : from === ctx.me ? -1n : 0n
199
+ if (!sign) continue
200
+ const nft = m.tokenId !== undefined
201
+ const k = `${m.asset}|${m.tokenId ?? ''}`
202
+ const decimals = nft ? 0 : (m.decimals ?? (m.asset === 'native' ? ctx.native.decimals : 18))
203
+ const entry = net.get(k) ?? { asset: m.asset === 'native' ? 'native' : show(ctx, m.asset), symbol: m.asset === 'native' ? ctx.native.symbol : (m.symbol ?? null), decimals, delta: '0', formatted: '0', ...(nft ? { tokenId: m.tokenId } : {}), raw: 0n }
204
+ entry.raw += sign * m.value
205
+ net.set(k, entry)
206
+ const other = sign > 0n ? from : to
207
+ partners.set(other, (partners.get(other) ?? 0n) + m.value)
208
+ if (sign > 0n && from === ZERO && (nft || m.asset !== 'native')) mints = true
209
+ }
210
+ const changes = [...net.values()].filter((e) => e.raw !== 0n).sort((a, b) => Number(a.raw > 0n) - Number(b.raw > 0n)).map(({ raw, ...e }) => ({ ...e, delta: raw.toString(), formatted: formatUnits(raw, e.decimals) }))
211
+ return { changes, mints, partners }
212
+ }
213
+
214
+ const amount = (c: ActivityChange) => `${c.formatted.replace('-', '')} ${c.symbol ?? (c.tokenId === undefined ? short(c.asset) : `#${c.tokenId}`)}`
215
+
216
+ function classify(ctx: BuildCtx, tx: RawTx | undefined, decoded: DecodedCall | null, changes: ActivityChange[], mints: boolean): ActivityType {
217
+ const outs = changes.filter((c) => c.delta.startsWith('-'))
218
+ const ins = changes.filter((c) => !c.delta.startsWith('-'))
219
+ const sender = tx !== undefined && key(ctx, tx.from) === ctx.me
220
+ if (sender && decoded?.type === 'approve') return 'approve'
221
+ if (sender && decoded?.type === 'liquidity') return 'liquidity'
222
+ if (mints && ins.length) return 'mint'
223
+ if (outs.length && ins.length && new Set([...outs, ...ins].map((c) => `${c.asset}|${c.tokenId ?? ''}`)).size > 1) return 'swap'
224
+ if (decoded?.type === 'swap') return 'swap'
225
+ if (changes.length || (tx && tx.input === '0x') || decoded?.type === 'transfer') return 'transfer'
226
+ if (decoded) return 'contract'
227
+ return 'other'
228
+ }
229
+
230
+ export function buildActivities(page: Pick<RawPage, 'txs' | 'moves'>, ctx: BuildCtx, minPos = 0): Activity[] {
231
+ const groups = new Map<string, { tx?: RawTx; moves: RawMove[]; pos: number; time: number; block?: number }>()
232
+ const group = (hash: string, pos: number, time: number, block?: number) => {
233
+ let g = groups.get(hash)
234
+ if (!g) groups.set(hash, (g = { moves: [], pos, time, block }))
235
+ return g
236
+ }
237
+ for (const tx of page.txs) if (tx.pos >= minPos) group(tx.hash, tx.pos, tx.time, tx.block).tx = tx
238
+ for (const m of page.moves) if (m.pos >= minPos) group(m.hash, m.pos, m.time, m.block).moves.push(m)
239
+
240
+ const items: Activity[] = []
241
+ for (const [hash, g] of groups) {
242
+ const { tx } = g
243
+ const moves = [...g.moves]
244
+ // 顶层的主币转账(explorer 的转账列表里没有);失败的交易没有任何资产变化
245
+ if (tx?.ok && tx.value > 0n && tx.to) moves.push({ hash, pos: g.pos, time: g.time, asset: 'native', from: tx.from, to: tx.to, value: tx.value })
246
+ const { changes, mints, partners } = tx && !tx.ok ? { changes: [], mints: false, partners: new Map<string, bigint>() } : changesOf(ctx, moves)
247
+ const decoded = tx && tx.input !== '0x' && tx.to ? decodeCalldata(tx.input, { to: show(ctx, tx.to), chainId: ctx.chainId, value: tx.value }) : null
248
+ const type = classify(ctx, tx, decoded, changes, mints)
249
+ const sender = tx !== undefined && key(ctx, tx.from) === ctx.me
250
+ const outs = changes.filter((c) => c.delta.startsWith('-'))
251
+ const ins = changes.filter((c) => !c.delta.startsWith('-'))
252
+
253
+ let direction: Activity['direction']
254
+ if (type === 'transfer' || type === 'mint') {
255
+ if (tx?.to && key(ctx, tx.to) === ctx.me && sender) direction = 'self'
256
+ else if (outs.length && !ins.length) direction = 'out'
257
+ else if (ins.length && !outs.length) direction = 'in'
258
+ else if (!changes.length) direction = sender ? 'out' : 'in'
259
+ }
260
+ // 对方:转账是出入金额最大的那一方;其他是合约(自己发起时)或发起者
261
+ let counterparty: string | undefined
262
+ if (type === 'transfer' || type === 'mint') {
263
+ const top = [...partners.entries()].sort((a, b) => (a[1] > b[1] ? -1 : 1))[0]?.[0]
264
+ counterparty = top && top !== ZERO ? top : tx?.to && !sender ? tx.from : (tx?.to ?? undefined)
265
+ } else if (tx) {
266
+ counterparty = sender ? (tx.to ?? undefined) : tx.from
267
+ }
268
+
269
+ const protocol = decoded?.protocol
270
+ const failed = tx ? !tx.ok : false
271
+ let label: string
272
+ if (failed) label = `Failed ${decoded?.method ?? 'transaction'}${protocol ? ` on ${protocol}` : ''}`
273
+ else if (type === 'swap') label = `Swapped ${outs.map(amount).join(' + ')} for ${ins.map(amount).join(' + ')}${protocol ? ` on ${protocol}` : ''}`
274
+ else if (type === 'approve') label = `Approved ${decoded?.args?.spender ? short(String(decoded.args.spender)) : (decoded?.method ?? 'spending')}${decoded?.args?.amount === '0' || decoded?.args?.approved === false ? ' (revoked)' : ''}`
275
+ else if (type === 'liquidity') label = `${decoded?.method ?? 'Liquidity'}${protocol ? ` on ${protocol}` : ''}: ${changes.map((c) => `${c.delta.startsWith('-') ? '-' : '+'}${amount(c)}`).join(', ')}`
276
+ else if (type === 'mint') label = `Minted ${ins.map(amount).join(' + ')}`
277
+ else if (type === 'transfer') {
278
+ const cp = counterparty ? short(show(ctx, counterparty)) : ''
279
+ label = direction === 'in' ? `Received ${ins.map(amount).join(' + ')}${cp ? ` from ${cp}` : ''}` : direction === 'self' ? 'Sent to self' : `Sent ${outs.map(amount).join(' + ') || 'a transaction'}${cp ? ` to ${cp}` : ''}`
280
+ } else label = decoded?.summary ?? 'Transaction'
281
+
282
+ items.push({
283
+ hash,
284
+ time: g.time,
285
+ ...(g.block === undefined ? {} : { block: g.block }),
286
+ status: failed ? 'failed' : 'success',
287
+ type,
288
+ ...(direction ? { direction } : {}),
289
+ ...(counterparty ? { counterparty: show(ctx, counterparty) } : {}),
290
+ label,
291
+ changes,
292
+ ...(sender && tx?.fee !== undefined ? { fee: tx.fee.toString() } : {}),
293
+ ...(protocol ? { protocol } : {}),
294
+ ...(decoded?.method ? { method: decoded.method } : {}),
295
+ })
296
+ }
297
+ // 位置新的在前(同一个位置保持原来的顺序)
298
+ const posOf = new Map([...groups].map(([h, g]) => [h, g.pos]))
299
+ return items.sort((a, b) => (posOf.get(b.hash) as number) - (posOf.get(a.hash) as number))
300
+ }
301
+
302
+ // ---------------------------------------------------------------------------
303
+ // 入口
304
+ // ---------------------------------------------------------------------------
305
+
306
+ /**
307
+ * 查一个地址的历史活动(最新的在前),分页用 nextCursor。
308
+ *
309
+ * ```ts
310
+ * const page = await history(provider, user, { limit: 20 }) // Blockscout,失败换 RPC 日志
311
+ * const next = await history(provider, user, { cursor: page.nextCursor, types: ['swap'] })
312
+ * await history(provider, user, { source: ['etherscan', 'rpc'], apiKey }) // Etherscan v2 的 Key 覆盖多条链
313
+ * ```
314
+ */
315
+ export async function history(provider: Provider, address: string, options: HistoryOptions = {}): Promise<HistoryResult> {
316
+ const chainId = await provider.getChainId()
317
+ const tron = isTronChain(chainId)
318
+ const me = tron ? toTronAddress(address) : address.toLowerCase()
319
+ if (!tron && !/^0x[0-9a-f]{40}$/.test(me)) throw new Error(`Invalid address: ${address}`)
320
+ const limit = Math.max(1, Math.min(200, Math.floor(options.limit ?? 25)))
321
+ const cursor = options.cursor === undefined ? undefined : Number(CURSOR_RE.exec(options.cursor)?.[1] ?? Number.NaN)
322
+ if (cursor !== undefined && !Number.isFinite(cursor)) throw new Error(`Invalid cursor: ${options.cursor}`)
323
+ const adapters = historySourcesFor(chainId, options.source, options.apiKey)
324
+ if (!adapters.length) throw new Error(`No history source for chain ${chainId}: pass options.source`)
325
+ const nativeCurrency = getNativeCurrency(chainId)
326
+ const ctx: BuildCtx = { chainId, tron, me: tron ? toEvmAddress(me).toLowerCase() : me, native: { symbol: nativeCurrency?.symbol ?? null, decimals: nativeCurrency?.decimals ?? 18 } }
327
+ const query = (endPos?: number): HistoryQuery => ({ address: me, chainId, limit: Math.max(limit, 10), ...(endPos === undefined ? {} : { endPos }), nft: options.nft ?? true, provider, fetch: options.fetch ?? fetch, ...(options.signal ? { signal: options.signal } : {}) })
328
+
329
+ const items: Activity[] = []
330
+ const seen = new Set<string>()
331
+ let pos = cursor
332
+ let used = adapters[0]?.name ?? ''
333
+ let more = true
334
+ for (let pages = 0; pages < (options.maxPages ?? 5) && more; pages++) {
335
+ let page: RawPage | undefined
336
+ const errors: Array<{ source: string; error: unknown }> = []
337
+ for (const adapter of adapters) {
338
+ try {
339
+ page = await adapter.fetch(query(pos))
340
+ used = adapter.name
341
+ break
342
+ } catch (err) {
343
+ if (options.signal?.aborted) throw err
344
+ errors.push({ source: adapter.name, error: err })
345
+ }
346
+ }
347
+ if (!page) throw new HistorySourceError(errors)
348
+ // 整页都落在同一个位置、被条数截断时放不下完整记录:宁可收下这一页,也不能原地打转
349
+ const hasComplete = [...page.txs, ...page.moves].some((r) => r.pos >= page.completeFrom)
350
+ const batch = buildActivities(page, ctx, hasComplete ? page.completeFrom : 0)
351
+ for (const a of batch) {
352
+ if (seen.has(a.hash)) continue
353
+ seen.add(a.hash)
354
+ if (options.before !== undefined && a.time >= options.before) continue
355
+ if (options.after !== undefined && a.time < options.after) continue
356
+ if (options.types && !options.types.includes(a.type)) continue
357
+ items.push(a)
358
+ }
359
+ const oldest = batch.length ? Math.min(...batch.map((a) => a.time)) : Number.POSITIVE_INFINITY
360
+ pos = hasComplete ? page.next : page.next === undefined ? undefined : page.next - 1
361
+ more = pos !== undefined && pos >= 0 && !(options.after !== undefined && oldest < options.after)
362
+ if (items.length >= limit) break
363
+ }
364
+ return { items, ...(more && pos !== undefined ? { nextCursor: `p:${pos}` } : {}), source: used }
365
+ }
366
+
367
+ export type { HistorySource } from './historySources.js'