@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.
Files changed (159) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +206 -1
  3. package/dist/dividends.d.ts +135 -0
  4. package/dist/dividends.d.ts.map +1 -0
  5. package/dist/dividends.js +253 -0
  6. package/dist/dividends.js.map +1 -0
  7. package/dist/history.d.ts +154 -0
  8. package/dist/history.d.ts.map +1 -0
  9. package/dist/history.js +247 -0
  10. package/dist/history.js.map +1 -0
  11. package/dist/historySources.d.ts +32 -0
  12. package/dist/historySources.d.ts.map +1 -0
  13. package/dist/historySources.js +319 -0
  14. package/dist/historySources.js.map +1 -0
  15. package/dist/liquidity/v3.d.ts +2 -0
  16. package/dist/liquidity/v3.d.ts.map +1 -1
  17. package/dist/liquidity/v3.js +1 -1
  18. package/dist/liquidity/v3.js.map +1 -1
  19. package/dist/liquidity/v4.d.ts +21 -0
  20. package/dist/liquidity/v4.d.ts.map +1 -1
  21. package/dist/liquidity/v4.js +4 -3
  22. package/dist/liquidity/v4.js.map +1 -1
  23. package/dist/lp.d.ts +8 -0
  24. package/dist/lp.d.ts.map +1 -1
  25. package/dist/lp.js +17 -1
  26. package/dist/lp.js.map +1 -1
  27. package/dist/lpV4.d.ts +4 -0
  28. package/dist/lpV4.d.ts.map +1 -1
  29. package/dist/lpV4.js +27 -3
  30. package/dist/lpV4.js.map +1 -1
  31. package/dist/permit.d.ts +137 -0
  32. package/dist/permit.d.ts.map +1 -0
  33. package/dist/permit.js +355 -0
  34. package/dist/permit.js.map +1 -0
  35. package/dist/portfolio.d.ts +2 -0
  36. package/dist/portfolio.d.ts.map +1 -1
  37. package/dist/portfolio.js +9 -4
  38. package/dist/portfolio.js.map +1 -1
  39. package/dist/rewards/farms.d.ts +26 -0
  40. package/dist/rewards/farms.d.ts.map +1 -0
  41. package/dist/rewards/farms.js +210 -0
  42. package/dist/rewards/farms.js.map +1 -0
  43. package/dist/rewards/index.d.ts +42 -0
  44. package/dist/rewards/index.d.ts.map +1 -0
  45. package/dist/rewards/index.js +382 -0
  46. package/dist/rewards/index.js.map +1 -0
  47. package/dist/rewards/types.d.ts +170 -0
  48. package/dist/rewards/types.d.ts.map +1 -0
  49. package/dist/rewards/types.js +2 -0
  50. package/dist/rewards/types.js.map +1 -0
  51. package/dist/simulate.d.ts +186 -0
  52. package/dist/simulate.d.ts.map +1 -0
  53. package/dist/simulate.js +559 -0
  54. package/dist/simulate.js.map +1 -0
  55. package/dist/simulateDecode.d.ts +33 -0
  56. package/dist/simulateDecode.d.ts.map +1 -0
  57. package/dist/simulateDecode.js +321 -0
  58. package/dist/simulateDecode.js.map +1 -0
  59. package/dist/smartAccount/account.d.ts +31 -0
  60. package/dist/smartAccount/account.d.ts.map +1 -0
  61. package/dist/smartAccount/account.js +33 -0
  62. package/dist/smartAccount/account.js.map +1 -0
  63. package/dist/smartAccount/clients.d.ts +77 -0
  64. package/dist/smartAccount/clients.d.ts.map +1 -0
  65. package/dist/smartAccount/clients.js +122 -0
  66. package/dist/smartAccount/clients.js.map +1 -0
  67. package/dist/smartAccount/eip5792.d.ts +77 -0
  68. package/dist/smartAccount/eip5792.d.ts.map +1 -0
  69. package/dist/smartAccount/eip5792.js +152 -0
  70. package/dist/smartAccount/eip5792.js.map +1 -0
  71. package/dist/smartAccount/send.d.ts +40 -0
  72. package/dist/smartAccount/send.d.ts.map +1 -0
  73. package/dist/smartAccount/send.js +271 -0
  74. package/dist/smartAccount/send.js.map +1 -0
  75. package/dist/smartAccount/userOp.d.ts +91 -0
  76. package/dist/smartAccount/userOp.d.ts.map +1 -0
  77. package/dist/smartAccount/userOp.js +124 -0
  78. package/dist/smartAccount/userOp.js.map +1 -0
  79. package/dist/subpaths/dividends.d.ts +28 -0
  80. package/dist/subpaths/dividends.d.ts.map +1 -0
  81. package/dist/subpaths/dividends.js +29 -0
  82. package/dist/subpaths/dividends.js.map +1 -0
  83. package/dist/subpaths/history.d.ts +22 -0
  84. package/dist/subpaths/history.d.ts.map +1 -0
  85. package/dist/subpaths/history.js +23 -0
  86. package/dist/subpaths/history.js.map +1 -0
  87. package/dist/subpaths/permit.d.ts +14 -0
  88. package/dist/subpaths/permit.d.ts.map +1 -0
  89. package/dist/subpaths/permit.js +14 -0
  90. package/dist/subpaths/permit.js.map +1 -0
  91. package/dist/subpaths/price.js +2 -2
  92. package/dist/subpaths/price.js.map +1 -1
  93. package/dist/subpaths/rewards.d.ts +27 -0
  94. package/dist/subpaths/rewards.d.ts.map +1 -0
  95. package/dist/subpaths/rewards.js +25 -0
  96. package/dist/subpaths/rewards.js.map +1 -0
  97. package/dist/subpaths/simulate.d.ts +25 -0
  98. package/dist/subpaths/simulate.d.ts.map +1 -0
  99. package/dist/subpaths/simulate.js +28 -0
  100. package/dist/subpaths/simulate.js.map +1 -0
  101. package/dist/subpaths/smart-account.d.ts +24 -0
  102. package/dist/subpaths/smart-account.d.ts.map +1 -0
  103. package/dist/subpaths/smart-account.js +24 -0
  104. package/dist/subpaths/smart-account.js.map +1 -0
  105. package/dist/swap.d.ts +45 -5
  106. package/dist/swap.d.ts.map +1 -1
  107. package/dist/swap.js +108 -12
  108. package/dist/swap.js.map +1 -1
  109. package/dist/write/batch.d.ts +34 -2
  110. package/dist/write/batch.d.ts.map +1 -1
  111. package/dist/write/batch.js +22 -3
  112. package/dist/write/batch.js.map +1 -1
  113. package/dist/write/collect.d.ts +9 -0
  114. package/dist/write/collect.d.ts.map +1 -1
  115. package/dist/write/collect.js +41 -1
  116. package/dist/write/collect.js.map +1 -1
  117. package/dist/write/send.d.ts +4 -0
  118. package/dist/write/send.d.ts.map +1 -1
  119. package/dist/write/send.js.map +1 -1
  120. package/dist/write/signer.d.ts +5 -1
  121. package/dist/write/signer.d.ts.map +1 -1
  122. package/dist/write/signer.js.map +1 -1
  123. package/dist/write/transfer.d.ts +5 -3
  124. package/dist/write/transfer.d.ts.map +1 -1
  125. package/dist/write/transfer.js +12 -6
  126. package/dist/write/transfer.js.map +1 -1
  127. package/package.json +35 -1
  128. package/src/dividends.ts +360 -0
  129. package/src/history.ts +367 -0
  130. package/src/historySources.ts +360 -0
  131. package/src/liquidity/v3.ts +1 -1
  132. package/src/liquidity/v4.ts +4 -3
  133. package/src/lp.ts +18 -1
  134. package/src/lpV4.ts +32 -2
  135. package/src/permit.ts +457 -0
  136. package/src/portfolio.ts +11 -4
  137. package/src/rewards/farms.ts +229 -0
  138. package/src/rewards/index.ts +431 -0
  139. package/src/rewards/types.ts +179 -0
  140. package/src/simulate.ts +727 -0
  141. package/src/simulateDecode.ts +352 -0
  142. package/src/smartAccount/account.ts +63 -0
  143. package/src/smartAccount/clients.ts +180 -0
  144. package/src/smartAccount/eip5792.ts +187 -0
  145. package/src/smartAccount/send.ts +341 -0
  146. package/src/smartAccount/userOp.ts +180 -0
  147. package/src/subpaths/dividends.ts +47 -0
  148. package/src/subpaths/history.ts +41 -0
  149. package/src/subpaths/permit.ts +39 -0
  150. package/src/subpaths/price.ts +2 -2
  151. package/src/subpaths/rewards.ts +46 -0
  152. package/src/subpaths/simulate.ts +51 -0
  153. package/src/subpaths/smart-account.ts +40 -0
  154. package/src/swap.ts +139 -18
  155. package/src/write/batch.ts +44 -5
  156. package/src/write/collect.ts +60 -1
  157. package/src/write/send.ts +4 -0
  158. package/src/write/signer.ts +5 -1
  159. package/src/write/transfer.ts +17 -10
@@ -0,0 +1,229 @@
1
+ import { Interface } from 'ethers'
2
+
3
+ import type { CallOverrides } from '../aggregate.js'
4
+ import type { Call } from '../call.js'
5
+ import { evm, fmt, same, tokenInfo, type Ctx } from '../liquidity/common.js'
6
+ import { lpPositionsByIds, type LpConcentratedPosition, type LpProtocol } from '../lp.js'
7
+ import type { Provider, StaticCallItem, StaticCallResult } from '../provider.js'
8
+ import { withSignal } from '../util.js'
9
+ import { mapPool } from '../write/send.js'
10
+ import type { ClaimAmount, ClaimItem, FarmProtocol } from './types.js'
11
+
12
+ const masterChef = (chainId: number, address: string): FarmProtocol => ({
13
+ id: 'pancakeswap-v3',
14
+ name: 'PancakeSwap V3 Farm',
15
+ kind: 'masterchef-v3',
16
+ chainId,
17
+ positionManager: '0x46A15B0b27311cedF172AB29E4f4766fbE7F4364',
18
+ masterChef: address,
19
+ })
20
+
21
+ /**
22
+ * 内置的挖矿合约(2026-10 在链上逐个核对:合约有代码,MasterChefV3 的 CAKE / nonfungiblePositionManager / poolLength、
23
+ * Voter 的 length / pools / gauges 能读,CLGauge 的 stakedValues / earned 能读)。
24
+ * Uniswap V3 的激励(UniswapV3Staker)按 IncentiveKey 领取,key 不能从链上按用户列出,不在表里
25
+ */
26
+ export const FARM_PROTOCOLS: readonly FarmProtocol[] = [
27
+ masterChef(56, '0x556B9306565093C855AEA9AE92A594704c2Cd59e'),
28
+ masterChef(1, '0x556B9306565093C855AEA9AE92A594704c2Cd59e'),
29
+ masterChef(8453, '0xC6A2Db661D5a5690172d8eB0a7DEA2d3008665A3'),
30
+ masterChef(42161, '0x5e09ACf80C0296740eC5d6F643005a4ef8DaA694'),
31
+ {
32
+ id: 'aerodrome-slipstream',
33
+ name: 'Aerodrome Slipstream',
34
+ kind: 'gauge',
35
+ chainId: 8453,
36
+ positionManager: '0x827922686190790b37229fd06084350E74485b72',
37
+ voter: '0x16613524e02ad97eDfeF371bC883F2F5d6C480A5',
38
+ },
39
+ {
40
+ id: 'velodrome-slipstream',
41
+ name: 'Velodrome Slipstream',
42
+ kind: 'gauge',
43
+ chainId: 10,
44
+ positionManager: '0x416b433906b1B72FA758e166e239c43d68dC6F29',
45
+ voter: '0x41C914ee0c7E1A5edCD0295623e6dC557B5aBf3C',
46
+ },
47
+ ]
48
+
49
+ export const masterChefInterface = new Interface([
50
+ 'function CAKE() view returns (address)',
51
+ 'function balanceOf(address user) view returns (uint256)',
52
+ 'function tokenOfOwnerByIndex(address user, uint256 index) view returns (uint256)',
53
+ 'function pendingCake(uint256 tokenId) view returns (uint256)',
54
+ 'function harvest(uint256 tokenId, address to) returns (uint256 reward)',
55
+ 'function collectTo((uint256 tokenId, address recipient, uint128 amount0Max, uint128 amount1Max) params, address to) returns (uint256 amount0, uint256 amount1)',
56
+ 'function multicall(bytes[] data) payable returns (bytes[] results)',
57
+ ])
58
+ export const gaugeInterface = new Interface(['function getReward(uint256 tokenId)'])
59
+
60
+ const voterAbi = ['function length() view returns (uint256)', 'function pools(uint256 index) view returns (address)', 'function gauges(address pool) view returns (address)']
61
+ const gaugeAbi = ['function stakedValues(address depositor) view returns (uint256[])', 'function earned(address account, uint256 tokenId) view returns (uint256)', 'function rewardToken() view returns (address)']
62
+
63
+ const CHUNK = 400
64
+ /** 每个 Voter 最多枚举多少个池子(Aerodrome 目前两千多个) */
65
+ const MAX_POOLS = 6000
66
+
67
+ /** harvest 预执行:每条一个 eth_call,分批并发(受限节点上一次几百个会被限流) */
68
+ const HARVEST_CHUNK = 25
69
+
70
+ const chunks = <T>(list: readonly T[]): T[][] => Array.from({ length: Math.ceil(list.length / CHUNK) }, (_, i) => list.slice(i * CHUNK, (i + 1) * CHUNK))
71
+
72
+ async function staticCallChunked(provider: Provider, items: readonly StaticCallItem[]): Promise<StaticCallResult<bigint>[]> {
73
+ const parts = Array.from({ length: Math.ceil(items.length / HARVEST_CHUNK) }, (_, i) => items.slice(i * HARVEST_CHUNK, (i + 1) * HARVEST_CHUNK))
74
+ return (await mapPool(parts, 2, (part) => provider.staticCallAll<bigint>(part))).flat()
75
+ }
76
+
77
+ /** 分批 tryAll:一次 multicall 不塞太多调用 */
78
+ async function tryAllChunked<T>(provider: Provider, calls: readonly Call[], overrides: CallOverrides): Promise<(T | null)[]> {
79
+ return (await Promise.all(chunks(calls).map((part) => provider.tryAll<T>(part, overrides)))).flat()
80
+ }
81
+
82
+ // Voter 枚举出的 gauge 列表(几乎不变):按 Provider(即按链)+ Voter 缓存 1 小时,只缓存最新状态的结果。
83
+ // 共享的查询不带调用方的 signal / minBlock(否则第一个调用方取消会让其他等着的调用方一起失败),调用方各自按自己的 signal 取消等待
84
+ const gaugeCache = new WeakMap<Provider, Map<string, { at: number; gauges: Promise<string[]> }>>()
85
+ const GAUGE_TTL = 3_600_000
86
+
87
+ async function enumerateGauges(provider: Provider, voter: string, overrides: CallOverrides): Promise<string[]> {
88
+ const v = provider.contract(voter, voterAbi)
89
+ const length = Math.min(Number(await provider.call<bigint>(v.length(), overrides)), MAX_POOLS)
90
+ const pools = await tryAllChunked<string>(provider, Array.from({ length }, (_, i) => v.pools(i)), overrides)
91
+ const list = pools.filter((p): p is string => p !== null)
92
+ const found = await tryAllChunked<string>(provider, list.map((p) => v.gauges(p)), overrides)
93
+ return [...new Set(found.filter((g): g is string => g !== null && !/^0x0{40}$/.test(g)))]
94
+ }
95
+
96
+ function discoverGauges(provider: Provider, voter: string, overrides: CallOverrides): Promise<string[]> {
97
+ if (overrides.blockTag !== undefined && overrides.blockTag !== 'latest') {
98
+ return enumerateGauges(provider, voter, overrides) // 历史区块:不缓存
99
+ }
100
+ const cache = gaugeCache.get(provider) ?? new Map()
101
+ gaugeCache.set(provider, cache)
102
+ const key = voter.toLowerCase()
103
+ const hit = cache.get(key)
104
+ let gauges: Promise<string[]>
105
+ if (hit && Date.now() - hit.at < GAUGE_TTL) {
106
+ gauges = hit.gauges
107
+ } else {
108
+ gauges = enumerateGauges(provider, voter, {})
109
+ const entry = { at: Date.now(), gauges }
110
+ cache.set(key, entry)
111
+ gauges.catch(() => {
112
+ if (cache.get(key) === entry) cache.delete(key)
113
+ })
114
+ }
115
+ return withSignal(overrides.signal, () => gauges)
116
+ }
117
+
118
+ type Source = { ctx: Ctx; owner: string; overrides: CallOverrides; maxPositions: number; lpProtocols: readonly LpProtocol[] }
119
+
120
+ const amountOf = (token: ClaimAmount['token'], value: bigint): ClaimAmount => ({ token, amount: value.toString(), formatted: fmt(value, token) })
121
+
122
+ /** MasterChefV3:用户质押的仓位。返回挖矿奖励(CAKE)和质押仓位未领取的手续费 */
123
+ export async function readMasterChef(source: Source, farm: FarmProtocol, wantFees: boolean, wantRewards: boolean): Promise<ClaimItem[]> {
124
+ const { ctx, owner, overrides, maxPositions } = source
125
+ const { provider } = ctx
126
+ const mcAddress = evm(farm.masterChef as string)
127
+ const mc = provider.contract(mcAddress, masterChefInterface)
128
+ const count = Number(await provider.call<bigint>(mc.balanceOf(owner), overrides))
129
+ if (!count) {
130
+ return []
131
+ }
132
+ // 质押多的地址只看最近的 maxPositions 个(新质押的排在最后)
133
+ const indexes = Array.from({ length: Math.min(count, maxPositions) }, (_, i) => count - Math.min(count, maxPositions) + i)
134
+ const ids = (await tryAllChunked<bigint>(provider, indexes.map((i) => mc.tokenOfOwnerByIndex(owner, i)), overrides)).filter((id): id is bigint => id !== null)
135
+ const items: ClaimItem[] = []
136
+ if (wantRewards) {
137
+ // pendingCake 不含奖励池自上次更新以来新增的部分(比实际领到的少,差额随时间变大):
138
+ // 以所有者的身份预执行 harvest 拿到合约真正会发的数量,预执行不了(钱包节点不支持 from 等)时退回 pendingCake
139
+ const [pending, exact, cake] = await Promise.all([
140
+ tryAllChunked<bigint>(provider, ids.map((id) => mc.pendingCake(id)), overrides),
141
+ staticCallChunked(provider, ids.map((id) => ({ call: mc.harvest(id, owner), overrides: { ...overrides, from: owner } }))),
142
+ provider.call<string>(mc.CAKE(), overrides),
143
+ ])
144
+ const [token] = await tokenInfo(ctx, [evm(cake)])
145
+ ids.forEach((id, i) => {
146
+ const run = exact[i]
147
+ // 预执行失败(限流 / 节点不支持 …)时退回 pendingCake,数量可能偏少,标 approximate
148
+ const value = run?.success ? BigInt(run.data) : (pending[i] ?? 0n)
149
+ if (value > 0n && token) {
150
+ items.push({
151
+ id: `farm:${farm.id}:${id}`,
152
+ kind: 'farm',
153
+ protocol: farm.id,
154
+ protocolName: farm.name,
155
+ tokenId: id.toString(),
156
+ pool: null,
157
+ staked: true,
158
+ target: mcAddress,
159
+ amounts: [amountOf(token, value)],
160
+ ...(run?.success ? {} : { approximate: true }),
161
+ })
162
+ }
163
+ })
164
+ }
165
+ const lp = source.lpProtocols.find((p) => p.version === 'v3' && p.chainId === ctx.chainId && p.positionManager && same(p.positionManager, farm.positionManager))
166
+ if (wantFees && lp && ids.length) {
167
+ const positions = (await lpPositionsByIds(provider, ids.map((tokenId) => ({ protocol: lp, tokenId })), { ...overrides })) as LpConcentratedPosition[]
168
+ for (const p of positions) {
169
+ if (BigInt(p.fees0) + BigInt(p.fees1) > 0n) {
170
+ items.push({
171
+ id: `fees:${lp.id}:${p.tokenId}`,
172
+ kind: 'fees',
173
+ protocol: lp.id,
174
+ protocolName: lp.name,
175
+ version: 'v3',
176
+ tokenId: p.tokenId,
177
+ pool: p.pool,
178
+ staked: true,
179
+ target: mcAddress,
180
+ amounts: [
181
+ { token: p.token0, amount: p.fees0, formatted: p.fees0Formatted },
182
+ { token: p.token1, amount: p.fees1, formatted: p.fees1Formatted },
183
+ ],
184
+ })
185
+ }
186
+ }
187
+ }
188
+ return items
189
+ }
190
+
191
+ /** Slipstream gauge:用户质押的仓位和待领取的奖励 */
192
+ export async function readGauges(source: Source, farm: FarmProtocol, gauges: readonly string[] | undefined): Promise<ClaimItem[]> {
193
+ const { ctx, owner, overrides } = source
194
+ const { provider } = ctx
195
+ const list = gauges ? gauges.map(evm) : await discoverGauges(provider, evm(farm.voter as string), overrides)
196
+ const staked = await tryAllChunked<bigint[]>(provider, list.map((g) => provider.contract(g, gaugeAbi).stakedValues(owner)), overrides)
197
+ const pairs = list.flatMap((gauge, i) => (staked[i] ?? []).slice(0, source.maxPositions).map((tokenId) => ({ gauge, tokenId })))
198
+ if (!pairs.length) {
199
+ return []
200
+ }
201
+ const [earned, rewardTokens] = await Promise.all([
202
+ tryAllChunked<bigint>(provider, pairs.map((x) => provider.contract(x.gauge, gaugeAbi).earned(owner, x.tokenId)), overrides),
203
+ tryAllChunked<string>(provider, [...new Set(pairs.map((x) => x.gauge))].map((g) => provider.contract(g, gaugeAbi).rewardToken()), overrides),
204
+ ])
205
+ const gaugeList = [...new Set(pairs.map((x) => x.gauge))]
206
+ const rewardOf = new Map(gaugeList.map((g, i) => [g, rewardTokens[i] ?? null]))
207
+ const tokens = await tokenInfo(ctx, [...new Set([...rewardOf.values()].filter((t): t is string => t !== null).map(evm))])
208
+ const infoOf = new Map(tokens.map((t) => [evm(t.address), t]))
209
+ return pairs.flatMap(({ gauge, tokenId }, i): ClaimItem[] => {
210
+ const value = earned[i] ?? 0n
211
+ const reward = rewardOf.get(gauge)
212
+ const token = reward ? infoOf.get(evm(reward)) : undefined
213
+ return value > 0n && token
214
+ ? [
215
+ {
216
+ id: `gauge:${farm.id}:${tokenId}`,
217
+ kind: 'gauge',
218
+ protocol: farm.id,
219
+ protocolName: farm.name,
220
+ tokenId: tokenId.toString(),
221
+ pool: null,
222
+ staked: true,
223
+ target: gauge,
224
+ amounts: [amountOf(token, value)],
225
+ },
226
+ ]
227
+ : []
228
+ })
229
+ }
@@ -0,0 +1,431 @@
1
+ import type { CallOverrides } from '../aggregate.js'
2
+ import { MAX_UINT128, createCtx, evm, fmt, out, type Ctx } from '../liquidity/common.js'
3
+ import { npmInterface } from '../liquidity/v3.js'
4
+ import { Actions, Planner, pmInterface } from '../liquidity/v4.js'
5
+ import { LP_PROTOCOLS, lpPositions, type LpConcentratedPosition, type LpProtocol } from '../lp.js'
6
+ import { tokenPrices } from '../price.js'
7
+ import type { Provider } from '../provider.js'
8
+ import { withSignal } from '../util.js'
9
+ import { batchCall, tronModule } from '../write/batch.js'
10
+ import { resolveSigner } from '../write/signer.js'
11
+ import { mapPool } from '../write/send.js'
12
+ import { FARM_PROTOCOLS, gaugeInterface, masterChefInterface, readGauges, readMasterChef } from './farms.js'
13
+ import type {
14
+ ClaimAllOptions,
15
+ ClaimAllSendOptions,
16
+ ClaimAmount,
17
+ ClaimItem,
18
+ ClaimPlan,
19
+ ClaimResult,
20
+ ClaimSkipped,
21
+ ClaimTx,
22
+ ClaimTxResult,
23
+ Claimable,
24
+ ClaimableOptions,
25
+ FarmProtocol,
26
+ } from './types.js'
27
+ import type { SignerInput } from '../write/signer.js'
28
+
29
+ export { FARM_PROTOCOLS } from './farms.js'
30
+
31
+ const lower = (s: string) => s.toLowerCase()
32
+
33
+ /** 同一个代币的数量合并 */
34
+ function sumAmounts(items: readonly ClaimItem[]): ClaimAmount[] {
35
+ const sum = new Map<string, { amount: ClaimAmount; raw: bigint; usd: number | null | undefined }>()
36
+ for (const { amounts } of items) {
37
+ for (const a of amounts) {
38
+ const key = lower(a.token.address)
39
+ const entry = sum.get(key) ?? { amount: a, raw: 0n, usd: undefined }
40
+ entry.raw += BigInt(a.amount)
41
+ if (a.usd !== undefined) {
42
+ entry.usd = a.usd === null ? (entry.usd ?? null) : (entry.usd ?? 0) + a.usd
43
+ }
44
+ sum.set(key, entry)
45
+ }
46
+ }
47
+ return [...sum.values()]
48
+ .filter((e) => e.raw > 0n)
49
+ .map(({ amount, raw, usd }): ClaimAmount => ({ token: amount.token, amount: raw.toString(), formatted: fmt(raw, amount.token), ...(usd === undefined ? {} : { usd }) }))
50
+ }
51
+
52
+ const usdOf = (amounts: readonly ClaimAmount[]): number | null | undefined => {
53
+ if (amounts.every((a) => a.usd === undefined)) {
54
+ return undefined
55
+ }
56
+ const known = amounts.filter((a) => a.usd != null && BigInt(a.amount) > 0n)
57
+ return known.length ? known.reduce((s, a) => s + (a.usd as number), 0) : null
58
+ }
59
+
60
+ /** 给每一项的数量加美元价值(价格源出错时全部为 null) */
61
+ async function applyPrices(provider: Provider, items: ClaimItem[], options: ClaimableOptions): Promise<void> {
62
+ const tokens = [...new Set(items.flatMap((i) => i.amounts).filter((a) => BigInt(a.amount) > 0n).map((a) => a.token.address))]
63
+ if (!tokens.length) {
64
+ return
65
+ }
66
+ let prices: Array<number | null> = tokens.map(() => null)
67
+ try {
68
+ const found = await tokenPrices(provider, tokens, {
69
+ ...(options.prices === true ? {} : { sources: options.prices as never }),
70
+ ...(options.fetch ? { fetch: options.fetch } : {}),
71
+ })
72
+ prices = found.map((p) => p.price)
73
+ } catch (err) {
74
+ try {
75
+ options.onPriceError?.(err)
76
+ } catch {
77
+ // 回调出错不影响查询结果
78
+ }
79
+ }
80
+ const priceOf = new Map(tokens.map((t, i) => [lower(t), prices[i] as number | null]))
81
+ for (const item of items) {
82
+ for (const a of item.amounts) {
83
+ const price = priceOf.get(lower(a.token.address)) ?? null
84
+ a.usd = a.formatted !== null && price !== null ? Number(a.formatted) * price : BigInt(a.amount) === 0n ? 0 : null
85
+ }
86
+ item.usd = usdOf(item.amounts) ?? null
87
+ }
88
+ }
89
+
90
+ /** 选出要查的 LP 协议 / 挖矿合约 */
91
+ function selectProtocols(chainId: number, options: ClaimableOptions): { lp: Array<string | LpProtocol> | undefined; farms: FarmProtocol[]; lpAll: LpProtocol[] } {
92
+ const farmTable = [...(options.farms ?? []), ...FARM_PROTOCOLS].filter((f) => f.chainId === chainId)
93
+ const custom = (options.protocols ?? []).filter((p): p is LpProtocol => typeof p !== 'string' && p.chainId === chainId)
94
+ const lpAll = [...custom, ...LP_PROTOCOLS.filter((p) => p.chainId === chainId)]
95
+ if (!options.protocols) {
96
+ return { lp: undefined, farms: farmTable, lpAll }
97
+ }
98
+ const lp: Array<string | LpProtocol> = []
99
+ const farms: FarmProtocol[] = []
100
+ for (const p of options.protocols) {
101
+ if (typeof p !== 'string') {
102
+ if (p.chainId === chainId && p.version !== 'v2') lp.push(p)
103
+ continue
104
+ }
105
+ const inLp = lpAll.filter((x) => x.id === p)
106
+ const inFarm = farmTable.filter((f) => f.id === p)
107
+ if (!inLp.length && !inFarm.length) {
108
+ throw new Error(`Unknown protocol ${p} on chain ${chainId}; pass an LpProtocol / FarmProtocol object for protocols that are not built in`)
109
+ }
110
+ // V2 的手续费计入 LP 份额,没有单独领取的
111
+ if (inLp.some((x) => x.version !== 'v2')) lp.push(p)
112
+ farms.push(...inFarm)
113
+ }
114
+ return { lp, farms, lpAll }
115
+ }
116
+
117
+ /**
118
+ * 查一个地址能领取的全部内容(只读):V3 / V4 仓位未领取的手续费、质押在 MasterChef / gauge 里的仓位的挖矿奖励,
119
+ * 数量按精度换算,可选美元价值。每个来源单独读取,某个来源出错只记进 `errors`
120
+ *
121
+ * ```ts
122
+ * const c = await claimable(multi, user, { prices: true })
123
+ * c.items // [{ kind: 'fees', protocol: 'uniswap-v3', tokenId: '123', amounts: [{ token: USDC, formatted: '12.3', usd: 12.3 }, …], usd: 20.1 }, { kind: 'farm', amounts: [CAKE …] }, …]
124
+ * c.totals // 按代币汇总
125
+ * ```
126
+ */
127
+ export function claimable(provider: Provider, owner: string, options: ClaimableOptions = {}): Promise<Claimable> {
128
+ return withSignal(options.signal, () => queryClaimable(provider, owner, options))
129
+ }
130
+
131
+ async function queryClaimable(provider: Provider, owner: string, options: ClaimableOptions): Promise<Claimable> {
132
+ const ctx = await createCtx(provider, owner, {})
133
+ const { chainId } = ctx
134
+ const wantFees = options.fees ?? true
135
+ const wantRewards = options.rewards ?? true
136
+ const maxPositions = Math.max(1, Math.floor(options.maxPositions ?? 500))
137
+ const overrides: CallOverrides = {
138
+ ...(options.blockTag !== undefined ? { blockTag: options.blockTag } : {}),
139
+ ...(options.minBlock !== undefined ? { minBlock: options.minBlock } : {}),
140
+ ...(options.signal ? { signal: options.signal } : {}),
141
+ }
142
+ const { lp, farms, lpAll } = selectProtocols(chainId, options)
143
+ const errors: Claimable['errors'] = []
144
+ const items: ClaimItem[] = []
145
+ const task = async (source: string, run: () => Promise<ClaimItem[]>) => {
146
+ try {
147
+ items.push(...(await run()))
148
+ } catch (err) {
149
+ errors.push({ source, error: err instanceof Error ? err.message : String(err) })
150
+ }
151
+ }
152
+ const source = { ctx, owner: ctx.owner, overrides, maxPositions, lpProtocols: lpAll }
153
+ await Promise.all([
154
+ wantFees && (lp === undefined || lp.length)
155
+ ? task('positions', async () => {
156
+ const positions = await lpPositions(provider, owner, { ...overrides, ...(lp ? { protocols: lp } : {}), versions: ['v3', 'v4'], v4: options.v4, maxPositions })
157
+ return (positions as LpConcentratedPosition[])
158
+ .filter((p) => BigInt(p.fees0) + BigInt(p.fees1) > 0n)
159
+ .map(
160
+ (p): ClaimItem => ({
161
+ id: `fees:${p.protocol}:${p.tokenId}`,
162
+ kind: 'fees',
163
+ protocol: p.protocol,
164
+ protocolName: p.protocolName,
165
+ version: p.version,
166
+ tokenId: p.tokenId,
167
+ pool: p.pool,
168
+ staked: false,
169
+ target: p.positionManager,
170
+ amounts: [
171
+ { token: p.token0, amount: p.fees0, formatted: p.fees0Formatted },
172
+ { token: p.token1, amount: p.fees1, formatted: p.fees1Formatted },
173
+ ],
174
+ }),
175
+ )
176
+ })
177
+ : null,
178
+ ...farms.map((farm) =>
179
+ farm.kind === 'masterchef-v3'
180
+ ? wantFees || wantRewards
181
+ ? task(farm.id, () => readMasterChef(source, farm, wantFees, wantRewards))
182
+ : null
183
+ : wantRewards
184
+ ? task(farm.id, () => readGauges(source, farm, options.gauges))
185
+ : null,
186
+ ),
187
+ ])
188
+ if (options.prices) {
189
+ await applyPrices(provider, items, options)
190
+ }
191
+ if (options.prices) {
192
+ items.sort((a, b) => (b.usd ?? -1) - (a.usd ?? -1))
193
+ }
194
+ const totals = sumAmounts(items)
195
+ const usd = usdOf(totals)
196
+ return { chainId, owner: out(ctx, ctx.owner), items, totals, ...(usd === undefined ? {} : { usd }), errors }
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // 构建交易
201
+ // ---------------------------------------------------------------------------
202
+
203
+ interface Group {
204
+ kind: ClaimTx['kind']
205
+ type: 'v3' | 'v4' | 'masterchef' | 'gauge'
206
+ target: string
207
+ items: ClaimItem[]
208
+ }
209
+
210
+ function groupOf(items: readonly ClaimItem[]): Group[] {
211
+ const groups = new Map<string, Group>()
212
+ for (const item of items) {
213
+ const [type, kind, key]: [Group['type'], Group['kind'], string] =
214
+ item.kind === 'gauge'
215
+ ? ['gauge', 'gauge', item.id]
216
+ : item.staked
217
+ ? ['masterchef', 'farm', item.target]
218
+ : item.version === 'v4'
219
+ ? ['v4', 'fees', item.target]
220
+ : ['v3', 'fees', item.target]
221
+ const k = `${type}:${key}`
222
+ const group = groups.get(k) ?? { kind, type, target: item.target, items: [] }
223
+ group.items.push(item)
224
+ groups.set(k, group)
225
+ }
226
+ return [...groups.values()]
227
+ }
228
+
229
+ const wrap = (iface: typeof npmInterface, calls: string[]) => (calls.length === 1 ? (calls[0] as string) : iface.encodeFunctionData('multicall', [calls]))
230
+
231
+ /** 一组(同一个合约)里的若干项 → 一笔交易的 calldata */
232
+ function encodeGroup(ctx: Ctx, type: Group['type'], items: readonly ClaimItem[]): string {
233
+ const recipient = ctx.recipient
234
+ if (type === 'v3') {
235
+ return wrap(
236
+ npmInterface,
237
+ items.map((i) => npmInterface.encodeFunctionData('collect', [{ tokenId: i.tokenId, recipient, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128 }])),
238
+ )
239
+ }
240
+ if (type === 'masterchef') {
241
+ return wrap(
242
+ masterChefInterface,
243
+ items.map((i) =>
244
+ i.kind === 'farm'
245
+ ? masterChefInterface.encodeFunctionData('harvest', [i.tokenId, recipient])
246
+ : masterChefInterface.encodeFunctionData('collectTo', [{ tokenId: i.tokenId, recipient, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128 }, recipient]),
247
+ ),
248
+ )
249
+ }
250
+ if (type === 'gauge') {
251
+ return gaugeInterface.encodeFunctionData('getReward', [(items[0] as ClaimItem).tokenId])
252
+ }
253
+ // V4:每个仓位 DECREASE_LIQUIDITY(0) 结算手续费,再把涉及的每个币整个取出(TAKE 的数量 0 = 全部)
254
+ const planner = new Planner()
255
+ const hookData = ctx.options.hookData ?? '0x'
256
+ for (const i of items) {
257
+ planner.add(Actions.DECREASE_LIQUIDITY, ['uint256', 'uint256', 'uint128', 'uint128', 'bytes'], [i.tokenId, 0n, 0n, 0n, hookData])
258
+ }
259
+ const currencies = new Set(items.flatMap((i) => i.amounts.map((a) => evm(a.token.address))))
260
+ for (const currency of currencies) {
261
+ planner.add(Actions.TAKE, ['address', 'address', 'uint256'], [currency, recipient, 0n])
262
+ }
263
+ return pmInterface.encodeFunctionData('modifyLiquidities', [planner.encode(), ctx.deadline])
264
+ }
265
+
266
+ function buildTxs(ctx: Ctx, items: readonly ClaimItem[], batchSize: number): ClaimTx[] {
267
+ return groupOf(items).flatMap((group) => {
268
+ // gauge 的 getReward 一次只能领一个仓位
269
+ const size = group.type === 'gauge' ? 1 : batchSize
270
+ const txs: ClaimTx[] = []
271
+ for (let i = 0; i < group.items.length; i += size) {
272
+ const part = group.items.slice(i, i + size)
273
+ txs.push({ kind: group.kind, to: out(ctx, group.target), data: encodeGroup(ctx, group.type, part), value: '0', itemIds: part.map((x) => x.id) })
274
+ }
275
+ return txs
276
+ })
277
+ }
278
+
279
+ /**
280
+ * 领取计划(不签名、不发送):把要领取的项合并成尽量少的交易——同一个仓位合约的 V3 仓位用它的 `multicall`、
281
+ * V4 仓位合并成一次 `modifyLiquidities`、MasterChef 的挖矿奖励和质押仓位手续费用它的 `multicall`、每个 gauge 的仓位各一笔。
282
+ * 交易之间互不依赖,可以交给 `batchCall` 的 '7702' / '4337' / '5792' 模式合成一笔原子交易,或者逐笔发送。
283
+ *
284
+ * 默认先模拟(/simulate):某一笔失败时逐项再试,会失败的项放进 `skipped`,不拖累其他仓位。
285
+ *
286
+ * ```ts
287
+ * const plan = await buildClaimAll(multi, user, { prices: true, minUsd: 1 })
288
+ * plan.transactions // [{ kind: 'fees', to: npm, data, itemIds: [...] }, { kind: 'farm', … }]
289
+ * plan.skipped // [{ item, reason: 'below minUsd' }]
290
+ * ```
291
+ */
292
+ export async function buildClaimAll(provider: Provider, owner: string, options: ClaimAllOptions = {}): Promise<ClaimPlan> {
293
+ const ctx = await createCtx(provider, owner, { recipient: options.recipient, hookData: options.hookData })
294
+ const batchSize = Math.floor(options.batchSize ?? 20)
295
+ if (!(batchSize >= 1)) {
296
+ throw new RangeError(`batchSize must be at least 1: ${options.batchSize}`)
297
+ }
298
+ if (options.minUsd !== undefined && !(options.minUsd >= 0)) {
299
+ throw new RangeError(`minUsd must not be negative: ${options.minUsd}`)
300
+ }
301
+ const found = options.items ? undefined : await claimable(provider, owner, options)
302
+ const candidates = options.items ? [...options.items] : (found as Claimable).items
303
+ const skipped: ClaimSkipped[] = []
304
+ const seen = new Set<string>()
305
+ let included = candidates.filter((item) => {
306
+ const skip = (reason: string) => {
307
+ skipped.push({ item, reason })
308
+ return false
309
+ }
310
+ if (seen.has(item.id)) return false
311
+ seen.add(item.id)
312
+ if (item.amounts.every((a) => BigInt(a.amount) === 0n)) return skip('nothing to claim')
313
+ // 有一个要领的币没有价格就当价值未知(usd 只是已知部分的合计),不跳过
314
+ const unknownValue = item.amounts.some((a) => BigInt(a.amount) > 0n && a.usd === null)
315
+ if (options.minUsd !== undefined && item.usd != null && !unknownValue && item.usd < options.minUsd) return skip(`below minUsd (${item.usd})`)
316
+ if (item.kind === 'gauge' && ctx.recipient !== ctx.owner) return skip('gauge rewards are always paid to the owner, recipient is not supported')
317
+ return true
318
+ })
319
+ let transactions = buildTxs(ctx, included, batchSize)
320
+ if (options.simulate !== false && transactions.length) {
321
+ const checked = await preflight(provider, ctx, included, transactions, skipped, options.signal)
322
+ included = checked.items
323
+ transactions = checked.transactions
324
+ }
325
+ const totals = sumAmounts(included)
326
+ const usd = usdOf(totals)
327
+ return { chainId: ctx.chainId, owner: out(ctx, ctx.owner), recipient: out(ctx, ctx.recipient), items: included, skipped, totals, ...(usd === undefined ? {} : { usd }), transactions }
328
+ }
329
+
330
+ /**
331
+ * 模拟每一笔;失败的逐项再试,去掉会失败的项后重新构建。逐项都通过、只有合并后的那一笔失败时(gas / multicall 的相互影响)
332
+ * 对半拆开再模拟,直到通过或拆成单项。返回的交易模拟都没有失败;模拟本身出错(节点不支持等)时不拦,照常发送;取消(signal)时抛出
333
+ */
334
+ async function preflight(provider: Provider, ctx: Ctx, items: ClaimItem[], transactions: ClaimTx[], skipped: ClaimSkipped[], signal?: AbortSignal): Promise<{ items: ClaimItem[]; transactions: ClaimTx[] }> {
335
+ const { simulate } = await import('../simulate.js')
336
+ const run = async (tx: ClaimTx) => {
337
+ signal?.throwIfAborted()
338
+ try {
339
+ tx.simulation = await simulate(provider, { from: ctx.owner, to: tx.to, data: tx.data, value: 0n }, { from: ctx.owner, fee: false })
340
+ return tx.simulation.success ? null : (tx.simulation.error ?? 'execution reverted')
341
+ } catch (err) {
342
+ if (signal?.aborted || (err instanceof Error && err.name === 'AbortError')) throw err
343
+ return null
344
+ }
345
+ }
346
+ const byId = new Map(items.map((i) => [i.id, i]))
347
+ const failed = new Map<string, string>()
348
+ const settle = async (tx: ClaimTx): Promise<ClaimTx[]> => {
349
+ const error = await run(tx)
350
+ if (error === null) return [tx]
351
+ const list = tx.itemIds.map((id) => byId.get(id) as ClaimItem)
352
+ if (list.length === 1) {
353
+ failed.set(tx.itemIds[0] as string, error)
354
+ return []
355
+ }
356
+ // 逐项再试,找出是哪几个仓位失败
357
+ const singles = await mapPool(list, 4, async (item) => {
358
+ const single = buildTxs(ctx, [item], 1)[0] as ClaimTx
359
+ return { item, single, error: await run(single) }
360
+ })
361
+ const good = singles.filter((s) => s.error === null)
362
+ for (const s of singles) {
363
+ if (s.error !== null) failed.set(s.item.id, s.error)
364
+ }
365
+ if (good.length === singles.length) {
366
+ // 单项都能过、合并的失败:对半拆开
367
+ const half = Math.ceil(list.length / 2)
368
+ const parts = [list.slice(0, half), list.slice(half)].flatMap((part) => buildTxs(ctx, part, part.length))
369
+ return (await Promise.all(parts.map(settle))).flat()
370
+ }
371
+ if (good.length === 1) return [(good[0] as (typeof good)[number]).single]
372
+ return (await Promise.all(buildTxs(ctx, good.map((s) => s.item), list.length).map(settle))).flat()
373
+ }
374
+ const settled: ClaimTx[][] = []
375
+ await mapPool(transactions, 4, async (tx, i) => {
376
+ settled[i] = await settle(tx)
377
+ })
378
+ if (!failed.size) {
379
+ return { items, transactions: settled.flat() }
380
+ }
381
+ for (const item of items) {
382
+ if (failed.has(item.id)) skipped.push({ item, reason: `simulation failed: ${failed.get(item.id)}` })
383
+ }
384
+ return { items: items.filter((i) => !failed.has(i.id)), transactions: settled.flat() }
385
+ }
386
+
387
+ /**
388
+ * 领取全部手续费和挖矿奖励:构建计划(同 buildClaimAll)后用 from 签名发送,每笔独立,某一笔失败不影响后面的。
389
+ * from 与 /write 的 batchCall 相同;`mode: '7702' | '4337' | '5792'` 时合成一笔原子交易(一个仓位失败整笔回滚,所以先模拟、剔除会失败的项)。
390
+ *
391
+ * ```ts
392
+ * const { status, plan, claimed } = await claimAll(multi, privateKey, { minUsd: 1 })
393
+ * await claimAll(multi, privateKey, { mode: '7702' }) // 一笔交易领完
394
+ * await claimAll(multi, privateKey, { items: picked }) // 只领 getClaimable / claimable 结果里挑出来的
395
+ * ```
396
+ */
397
+ export async function claimAll(provider: Provider, from: SignerInput, options: ClaimAllSendOptions = {}): Promise<ClaimResult> {
398
+ const tron = await tronModule(provider)
399
+ const owner = tron ? await tron.tronSignerAddress(from) : (await resolveSigner(from)).address
400
+ const plan = await buildClaimAll(provider, owner, options)
401
+ if (!plan.transactions.length) {
402
+ return { plan, results: [], status: 'skipped', claimed: [], failed: [] }
403
+ }
404
+ const {
405
+ items: _items, protocols: _protocols, fees: _fees, rewards: _rewards, v4: _v4, farms: _farms, gauges: _gauges, maxPositions: _max, prices: _prices, onPriceError: _onPriceError,
406
+ fetch: _fetch, recipient: _recipient, hookData: _hookData, minUsd: _minUsd, batchSize: _batchSize, simulate: _simulate, blockTag: _blockTag, from: _from, minBlock: _minBlock, ...send
407
+ } = options
408
+ const written = await batchCall(
409
+ provider,
410
+ plan.transactions.map((tx) => ({ from, to: tx.to, data: tx.data, value: 0n })),
411
+ { ...send, continueOnError: options.continueOnError ?? true },
412
+ )
413
+ const results = written.map((r, i): ClaimTxResult => ({ ...r, kind: (plan.transactions[i] as ClaimTx).kind, itemIds: (plan.transactions[i] as ClaimTx).itemIds }))
414
+ if (options.dryRun) {
415
+ // 只预估:没有领取任何东西。预估通过的项放进 simulated,预估失败的才算 failed
416
+ const ok = new Set(results.filter((r) => r.status === 'simulated').flatMap((r) => r.itemIds))
417
+ const badRun = results.find((r) => r.status !== 'simulated')
418
+ return { plan, results, status: badRun ? badRun.status : 'simulated', claimed: [], failed: plan.items.filter((i) => !ok.has(i.id)), simulated: plan.items.filter((i) => ok.has(i.id)) }
419
+ }
420
+ const confirmed = new Set(results.filter((r) => r.status === 'confirmed').flatMap((r) => r.itemIds))
421
+ const bad = results.find((r) => r.status !== 'confirmed')
422
+ return {
423
+ plan,
424
+ results,
425
+ status: bad ? bad.status : 'confirmed',
426
+ claimed: plan.items.filter((i) => confirmed.has(i.id)),
427
+ failed: plan.items.filter((i) => !confirmed.has(i.id)),
428
+ }
429
+ }
430
+
431
+ export type { ClaimAllOptions, ClaimAllSendOptions, ClaimAmount, ClaimItem, ClaimKind, ClaimPlan, ClaimResult, ClaimSkipped, ClaimTx, ClaimTxResult, Claimable, ClaimableOptions, FarmProtocol } from './types.js'