@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
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Flap(flap.sh,BNB Chain 发币平台)的分红:持有带分红的 Flap 税收代币(FlapTaxToken)的地址按持仓比例分到税收,
3
+ * 分红放在每个代币自己的 Dividend 合约里,持有人(或任何人替持有人)调用 withdrawDividendsFor 领取。
4
+ *
5
+ * 核实过的合约(BSC 主网,2026-10 在主网分叉上实测):
6
+ * - 代币:`dividendContract()` 返回它的 Dividend 合约(Tax Token V3 总是部署;V2 只在 dividendBps > 0 时部署)
7
+ * - Dividend 合约(EIP-1167 克隆,实现 0xd67447C2…5905,Sourcify / BscScan 上未验证源码,函数靠官方文档 + 链上调用核对):
8
+ * `dividendToken()`、`weth()`、`minimumShareBalance()`、`withdrawableDividends(address)`、`withdrawnDividends(address)`、
9
+ * `withdrawDividendsFor(address)`(任何人可调,款项直接到持有人)
10
+ * - 官方 TaxTokenHelper(0x53841c73…8346)的 `getDividendInfo(token, user) → (claimed, pending)` 与上面两个函数结果一致(测试里交叉核对)
11
+ *
12
+ * 只有一种合约形态,所以只有一个适配器(flap);识别不了的代币(没有 dividendContract() 或 Dividend 合约缺函数)放进 unsupported,
13
+ * 不猜函数。
14
+ */
15
+ import { Interface, getAddress } from 'ethers'
16
+
17
+ import { NATIVE_TOKEN, formatUnits } from './erc20.js'
18
+ import type { Provider } from './provider.js'
19
+ import { runLogScan, type LogScanProgress } from './scan.js'
20
+ import type { PriceSource } from './price.js'
21
+ import { isTronChain } from './tron.js'
22
+ import type { BatchCallOptions } from './write/batch.js'
23
+ import type { WriteResult } from './write/send.js'
24
+ import type { SignerInput } from './write/signer.js'
25
+
26
+ /** 支持的链:目前只有 BSC(Flap 在 Base、X Layer 等链上也有部署,但没有核实过,不开放) */
27
+ export const FLAP_DIVIDEND_CHAINS: Readonly<Record<number, { portal: string; taxTokenHelper: string; swapRegistry: string }>> = {
28
+ 56: {
29
+ portal: '0xe2cE6ab80874Fa9Fa2aAE65D277Dd6B8e65C9De0',
30
+ taxTokenHelper: '0x53841c73217735F37BC1775538b03b23feFD8346',
31
+ swapRegistry: '0x644A8f560138418bAD4EdEFC7c17878a3c2fBEB6',
32
+ },
33
+ }
34
+
35
+ /** Flap 代币地址的靓号后缀(Portal 创建时用):税收代币是 7777,普通代币是 8888(实测也有 8888 的代币带分红合约);发现持仓时只收这类地址 */
36
+ export const FLAP_TOKEN_SUFFIXES: readonly string[] = ['7777', '8888']
37
+
38
+ const TOKEN_ABI = [
39
+ 'function dividendContract() view returns (address)',
40
+ 'function balanceOf(address) view returns (uint256)',
41
+ 'function symbol() view returns (string)',
42
+ 'function decimals() view returns (uint8)',
43
+ ]
44
+ const DIVIDEND_ABI = [
45
+ 'function dividendToken() view returns (address)',
46
+ 'function weth() view returns (address)',
47
+ 'function minimumShareBalance() view returns (uint256)',
48
+ 'function withdrawableDividends(address) view returns (uint256)',
49
+ 'function withdrawnDividends(address) view returns (uint256)',
50
+ ]
51
+ const ERC20_META_ABI = ['function symbol() view returns (string)', 'function decimals() view returns (uint8)']
52
+ const claimInterface = new Interface(['function withdrawDividendsFor(address user)'])
53
+ const ZERO = '0x0000000000000000000000000000000000000000'
54
+ const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
55
+
56
+ /** 一次最多检查的代币数(防止传入或发现的列表无限大) */
57
+ const MAX_TOKENS = 200
58
+
59
+ export interface DividendItem {
60
+ /** 持有人 */
61
+ owner: string
62
+ /** 带分红的 Flap 代币 */
63
+ token: string
64
+ symbol: string | null
65
+ decimals: number
66
+ /** 持有人当前的代币余额(可以是 0:卖掉后之前累计的分红仍可领取) */
67
+ balance: string
68
+ /** 这个代币的 Dividend 合约(领取的目标) */
69
+ dividendContract: string
70
+ /** Dividend 合约里配置的分红代币地址(WBNB 也是这个地址) */
71
+ dividendToken: string
72
+ /** 实际收到的资产:'native'(分红代币是 WBNB 时领取会解包成 BNB)或分红代币地址 */
73
+ asset: 'native' | string
74
+ assetSymbol: string | null
75
+ assetDecimals: number
76
+ /** 分红资产的 decimals 读不到(节点出错等):assetDecimals 按 18 估,claimableFormatted 可能不准,usd 固定为 null */
77
+ decimalsUnknown?: boolean
78
+ /** 当前可领取的数量(asset 的最小单位),领取时就是收到的数量 */
79
+ claimable: string
80
+ claimableFormatted: string
81
+ /** 已经领取过的累计数量 */
82
+ claimed: string
83
+ claimedFormatted: string
84
+ /** 可领取部分的美元价值;没开价格或没有价格时为 null */
85
+ usd: number | null
86
+ /** 持有人要持有至少这么多代币才会继续累计新的分红(已累计的不受影响) */
87
+ minimumShareBalance: string
88
+ /** 当前余额是否达到 minimumShareBalance(false 时新的分红不再累计,但 claimable 仍可领取) */
89
+ accruing: boolean
90
+ /** 适配器名称 */
91
+ adapter: 'flap'
92
+ }
93
+
94
+ export interface DividendUnsupported {
95
+ token: string
96
+ reason: string
97
+ }
98
+
99
+ export interface DividendScanOptions {
100
+ /** 收哪些后缀的代币地址,默认 FLAP_TOKEN_SUFFIXES */
101
+ suffixes?: readonly string[]
102
+ /** 起点区块(含);不传时往回扫 lookbackBlocks 个区块 */
103
+ fromBlock?: number
104
+ /** 不传 fromBlock 时往回扫多少个区块,默认 5000 */
105
+ lookbackBlocks?: number
106
+ /** 上次的进度(结果的 scan.progress),传了就接着扫 */
107
+ progress?: LogScanProgress
108
+ /** 单次 getLogs 的最大区块范围,默认 1000;节点有上限时自动缩小 */
109
+ blockRange?: number
110
+ /** 这次最多发多少个 getLogs,默认 10 */
111
+ maxRequests?: number
112
+ /** 这次最多花多少毫秒,默认 10000 */
113
+ timeBudget?: number
114
+ signal?: AbortSignal
115
+ }
116
+
117
+ export interface DividendsOptions {
118
+ /** 要检查的代币(Flap 代币地址)。可以和 discover 一起用 */
119
+ tokens?: readonly string[]
120
+ /** 从持有人收到过的 Transfer 事件里发现 Flap 代币(只收靓号后缀 7777 / 8888 的地址,再逐个核对;发现的代币里没有分红合约的不报告),true 用默认值 */
121
+ discover?: boolean | DividendScanOptions
122
+ /** 查分红资产的美元价格:true(默认)用 DefiLlama + 链上池子,也可以传自己的 PriceSource,false 不查 */
123
+ prices?: boolean | PriceSource
124
+ fetch?: typeof fetch
125
+ /** 包含没有可领取分红的代币(默认 false,只返回 claimable > 0 的) */
126
+ includeZero?: boolean
127
+ }
128
+
129
+ export interface DividendsResult {
130
+ items: DividendItem[]
131
+ /** 检查过但识别不了的代币(不是税收代币、没有分红合约、合约函数不符) */
132
+ unsupported: DividendUnsupported[]
133
+ /** discover 的扫描状态:cursor 是已扫到的区块,progress 可以存下来下次接着扫 */
134
+ scan?: { cursor: number; progress: LogScanProgress | null; tokens: string[] }
135
+ /** 查价格出错时的信息(这时 usd 都是 null) */
136
+ priceError?: string
137
+ }
138
+
139
+ function assertChain(chainId: number): void {
140
+ if (isTronChain(chainId) || !FLAP_DIVIDEND_CHAINS[chainId]) {
141
+ throw new Error(`Flap dividends are only supported on BNB Chain (56), got chain ${chainId}`)
142
+ }
143
+ }
144
+
145
+ /** 发现持有人收到过的 Flap 税收代币(只发现,不核对有没有分红:把 tokens 交给 dividends) */
146
+ export async function scanDividendTokens(provider: Provider, owner: string, options: DividendScanOptions = {}): Promise<{ tokens: string[]; cursor: number; progress: LogScanProgress | null }> {
147
+ assertChain(await provider.getChainId())
148
+ const ownerTopic = `0x${getAddress(owner).slice(2).toLowerCase().padStart(64, '0')}`
149
+ const lookback = Math.max(0, Math.floor(options.lookbackBlocks ?? 5000))
150
+ const suffixes = (options.suffixes ?? FLAP_TOKEN_SUFFIXES).map((x) => x.toLowerCase())
151
+ const progress = await runLogScan({
152
+ provider,
153
+ saved: options.progress,
154
+ initialCursor: (target) => (options.fromBlock !== undefined ? options.fromBlock - 1 : target - lookback),
155
+ filter: { topics: [TRANSFER_TOPIC, null, ownerTopic] },
156
+ // 只收 ERC20 的 Transfer(3 个 topic)里靓号后缀的合约
157
+ collect: (log) => (log.topics.length === 3 && suffixes.some((s) => log.address.toLowerCase().endsWith(s)) ? log.address.toLowerCase() : null),
158
+ maxRange: Math.max(1, Math.floor(options.blockRange ?? 1000)),
159
+ maxRequests: Math.max(1, Math.floor(options.maxRequests ?? 10)),
160
+ timeBudget: options.timeBudget ?? 10_000,
161
+ confirmations: 0,
162
+ ...(options.signal ? { signal: options.signal } : {}),
163
+ })
164
+ return { tokens: (progress?.items ?? []).map((a) => getAddress(a)), cursor: progress?.cursor ?? -1, progress }
165
+ }
166
+
167
+ /**
168
+ * 查持有人在 Flap 代币上的分红:余额、可领取数量、已领取数量、分红资产和美元价值。
169
+ * 代币来自 tokens 和 discover(扫描收到过的 Transfer),每个代币都在链上核对(dividendContract() + Dividend 合约的函数),
170
+ * 核对不过的放进 unsupported。不发送交易
171
+ */
172
+ export async function dividends(provider: Provider, owner: string, options: DividendsOptions = {}): Promise<DividendsResult> {
173
+ const chainId = await provider.getChainId()
174
+ assertChain(chainId)
175
+ const holder = getAddress(owner)
176
+
177
+ let scan: DividendsResult['scan']
178
+ const found = new Map<string, string>()
179
+ const explicit = new Set<string>()
180
+ for (const t of options.tokens ?? []) {
181
+ found.set(getAddress(t).toLowerCase(), getAddress(t))
182
+ explicit.add(getAddress(t).toLowerCase())
183
+ }
184
+ if (options.discover) {
185
+ const r = await scanDividendTokens(provider, holder, options.discover === true ? {} : options.discover)
186
+ scan = { cursor: r.cursor, progress: r.progress, tokens: r.tokens }
187
+ for (const t of r.tokens) found.set(t.toLowerCase(), t)
188
+ }
189
+ const tokens = [...found.values()]
190
+ if (tokens.length > MAX_TOKENS) throw new Error(`Too many tokens (${tokens.length}), at most ${MAX_TOKENS} per call`)
191
+ const result: DividendsResult = { items: [], unsupported: [], ...(scan ? { scan } : {}) }
192
+ if (!tokens.length) return result
193
+
194
+ // 第一步:代币本身(余额、精度、Dividend 合约地址)
195
+ const stage1 = await provider.tryAll(tokens.flatMap((t) => [provider.contract(t, TOKEN_ABI).dividendContract(), provider.contract(t, TOKEN_ABI).balanceOf(holder), provider.contract(t, TOKEN_ABI).symbol(), provider.contract(t, TOKEN_ABI).decimals()]))
196
+ const valid: Array<{ token: string; dividend: string; balance: bigint; symbol: string | null; decimals: number }> = []
197
+ tokens.forEach((token, i) => {
198
+ const [dividend, balance, symbol, decimals] = stage1.slice(i * 4, i * 4 + 4) as [string | null, bigint | null, string | null, bigint | null]
199
+ if (dividend === null || dividend === ZERO) {
200
+ // 发现来的代币大多不是分红代币(普通 8888 代币等),只报告用户明确传入的
201
+ if (explicit.has(token.toLowerCase())) result.unsupported.push({ token, reason: dividend === null ? 'no dividendContract() (not a Flap tax token V2/V3)' : 'token has no dividend contract' })
202
+ } else if (balance === null || decimals === null) {
203
+ result.unsupported.push({ token, reason: 'not an ERC20 token' })
204
+ } else {
205
+ valid.push({ token, dividend, balance, symbol: typeof symbol === 'string' ? symbol : null, decimals: Number(decimals) })
206
+ }
207
+ })
208
+
209
+ // 第二步:Dividend 合约(分红代币、可领取、已领取、最低持仓)
210
+ const stage2 = valid.length
211
+ ? await provider.tryAll(valid.flatMap((v) => {
212
+ const d = provider.contract(v.dividend, DIVIDEND_ABI)
213
+ return [d.dividendToken(), d.weth(), d.withdrawableDividends(holder), d.withdrawnDividends(holder), d.minimumShareBalance()]
214
+ }))
215
+ : []
216
+ const rows: Array<{ v: (typeof valid)[number]; dividendToken: string; weth: string | null; claimable: bigint; claimed: bigint; min: bigint }> = []
217
+ valid.forEach((v, i) => {
218
+ const [dividendToken, weth, claimable, claimed, min] = stage2.slice(i * 5, i * 5 + 5) as [string | null, string | null, bigint | null, bigint | null, bigint | null]
219
+ if (dividendToken === null || claimable === null || claimed === null || min === null) {
220
+ result.unsupported.push({ token: v.token, reason: 'dividend contract does not expose the expected functions (unknown generation)' })
221
+ } else {
222
+ rows.push({ v, dividendToken, weth, claimable, claimed, min })
223
+ }
224
+ })
225
+
226
+ // 第三步:分红资产的 symbol / decimals(WBNB 作为 BNB 领取:按主币处理)
227
+ const isNative = (r: (typeof rows)[number]) => r.dividendToken === ZERO || (r.weth !== null && r.dividendToken.toLowerCase() === r.weth.toLowerCase())
228
+ const assetTokens = [...new Set(rows.filter((r) => !isNative(r)).map((r) => r.dividendToken))]
229
+ const meta = new Map<string, { symbol: string | null; decimals: number; known: boolean }>()
230
+ if (assetTokens.length) {
231
+ const res = await provider.tryAll(assetTokens.flatMap((a) => [provider.contract(a, ERC20_META_ABI).symbol(), provider.contract(a, ERC20_META_ABI).decimals()]))
232
+ assetTokens.forEach((a, i) => meta.set(a, { symbol: typeof res[i * 2] === 'string' ? (res[i * 2] as string) : null, decimals: res[i * 2 + 1] === null ? 18 : Number(res[i * 2 + 1]), known: res[i * 2 + 1] !== null }))
233
+ }
234
+ const nativeMeta = { symbol: 'BNB', decimals: 18, known: true }
235
+
236
+ const items: DividendItem[] = rows.map((r) => {
237
+ const native = isNative(r)
238
+ const m = native ? nativeMeta : (meta.get(r.dividendToken) ?? { symbol: null, decimals: 18, known: false })
239
+ return {
240
+ owner: holder,
241
+ token: r.v.token,
242
+ symbol: r.v.symbol,
243
+ decimals: r.v.decimals,
244
+ balance: r.v.balance.toString(),
245
+ dividendContract: getAddress(r.v.dividend),
246
+ dividendToken: getAddress(r.dividendToken),
247
+ asset: native ? 'native' : getAddress(r.dividendToken),
248
+ assetSymbol: m.symbol,
249
+ assetDecimals: m.decimals,
250
+ ...(m.known ? {} : { decimalsUnknown: true }),
251
+ claimable: r.claimable.toString(),
252
+ claimableFormatted: formatUnits(r.claimable, m.decimals),
253
+ claimed: r.claimed.toString(),
254
+ claimedFormatted: formatUnits(r.claimed, m.decimals),
255
+ usd: null,
256
+ minimumShareBalance: r.min.toString(),
257
+ accruing: r.v.balance >= r.min && r.v.balance > 0n,
258
+ adapter: 'flap',
259
+ }
260
+ })
261
+ const shown = options.includeZero ? items : items.filter((i) => BigInt(i.claimable) > 0n)
262
+
263
+ if (options.prices !== false && shown.length) {
264
+ try {
265
+ const { tokenPrices } = await import('./price.js')
266
+ const assets = [...new Set(shown.map((i) => (i.asset === 'native' ? NATIVE_TOKEN : i.asset)))]
267
+ const prices = await tokenPrices(provider, assets, { ...(typeof options.prices === 'object' ? { sources: [options.prices] } : {}), ...(options.fetch ? { fetch: options.fetch } : {}) })
268
+ const byAsset = new Map(prices.map((p) => [p.token.toLowerCase(), p.price]))
269
+ for (const i of shown) {
270
+ if (i.decimalsUnknown) continue // 精度未知,换算不出可信的美元价值
271
+ const price = byAsset.get((i.asset === 'native' ? NATIVE_TOKEN : i.asset).toLowerCase())
272
+ i.usd = price === null || price === undefined ? null : Number(i.claimableFormatted) * price
273
+ }
274
+ } catch (err) {
275
+ result.priceError = err instanceof Error ? err.message : String(err)
276
+ }
277
+ }
278
+ result.items = shown
279
+ return result
280
+ }
281
+
282
+ // ---------------------------------------------------------------------------
283
+ // 领取
284
+ // ---------------------------------------------------------------------------
285
+
286
+ export interface ClaimDividendTx {
287
+ to: string
288
+ data: string
289
+ value: '0'
290
+ item: DividendItem
291
+ }
292
+
293
+ /**
294
+ * 领取分红要发的交易(不签名):每个代币一笔 `withdrawDividendsFor(owner)`,发给它的 Dividend 合约,款项直接到 owner。
295
+ * 只包含 claimable > 0 的项。只校验 owner / dividendContract 是合法地址,不连链:items 来自调用方,
296
+ * 要确认 dividendContract 确实是代币自己的 Dividend 合约,用 claimDividends(发送前会在链上核对)
297
+ */
298
+ export function buildClaimDividends(items: readonly DividendItem[]): ClaimDividendTx[] {
299
+ return items
300
+ .filter((item) => BigInt(item.claimable) > 0n)
301
+ .map((item) => {
302
+ if (item.adapter !== 'flap') throw new Error(`Unsupported dividend adapter for ${item.token}`)
303
+ let to: string
304
+ let owner: string
305
+ try {
306
+ to = getAddress(item.dividendContract)
307
+ owner = getAddress(item.owner)
308
+ } catch {
309
+ throw new Error(`Invalid dividendContract / owner address for ${item.token}`)
310
+ }
311
+ return { to, data: claimInterface.encodeFunctionData('withdrawDividendsFor', [owner]), value: '0' as const, item }
312
+ })
313
+ }
314
+
315
+ export interface ClaimDividendsOptions extends BatchCallOptions {
316
+ /** 发送前先用 /simulate 模拟每一笔(claimable 只是查询时的快照,可能已经变了),会 revert 的不发(结果 status 为 'skipped',error 是原因)。默认 true,false 不模拟;dryRun 只预估 gas */
317
+ simulate?: boolean
318
+ }
319
+
320
+ export interface ClaimDividendResult extends WriteResult {
321
+ item: DividendItem
322
+ }
323
+
324
+ /**
325
+ * 领取分红:构建(同 buildClaimDividends)后用 from 签名发送,钱包和选项同 batchCall
326
+ * (mode: '7702' / '4337' / '5792' 时合成一笔原子交易)。任何人都可以替持有人领取,款项直接到 item.owner,所以 from 不必是持有人(如代付 gas)。
327
+ *
328
+ * ```ts
329
+ * const { items } = await getDividends(user, { chainId: 56, discover: true })
330
+ * await claimDividends(provider, key, items, { mode: '7702' })
331
+ * ```
332
+ */
333
+ export async function claimDividends(provider: Provider, from: SignerInput, items: readonly DividendItem[], options: ClaimDividendsOptions = {}): Promise<ClaimDividendResult[]> {
334
+ assertChain(await provider.getChainId())
335
+ const { simulate: preCheck = true, ...sendOptions } = options
336
+ const txs = buildClaimDividends(items)
337
+ if (!txs.length) return []
338
+ const skipped = new Map<number, string>()
339
+ // 核对 items 里的 dividendContract 确实是代币自己的 Dividend 合约(items 来自调用方,不能直接信)
340
+ const onChain = await provider.tryAll<string>(txs.map((tx) => provider.contract(tx.item.token, TOKEN_ABI).dividendContract()))
341
+ onChain.forEach((d, i) => {
342
+ if (d === null || (txs[i] as ClaimDividendTx).to.toLowerCase() !== d.toLowerCase()) {
343
+ skipped.set(i, `dividendContract mismatch: ${(txs[i] as ClaimDividendTx).to} is not the dividend contract of ${(txs[i] as ClaimDividendTx).item.token}`)
344
+ }
345
+ })
346
+ if (preCheck) {
347
+ const { simulate } = await import('./simulate.js')
348
+ const sims = await Promise.all(txs.map((tx, i): Promise<{ success: boolean; error?: string }> => skipped.has(i) ? Promise.resolve({ success: true }) : simulate(provider, { to: tx.to, data: tx.data, from: tx.item.owner }, { usd: false, risk: false }).catch((err: unknown) => ({ success: false, error: err instanceof Error ? err.message : String(err) }))))
349
+ sims.forEach((s, i) => !s.success && skipped.set(i, s.error ?? 'simulation failed'))
350
+ }
351
+ const send = txs.map((tx, i) => ({ tx, i })).filter(({ i }) => !skipped.has(i))
352
+ const { batchCall } = await import('./write/batch.js')
353
+ const sent = send.length ? await batchCall(provider, send.map(({ tx }) => ({ from, to: tx.to, data: tx.data })), { continueOnError: true, ...sendOptions }) : []
354
+ const sentBy = new Map(send.map(({ i }, k) => [i, sent[k] as WriteResult]))
355
+ return txs.map((tx, i) => {
356
+ const r = sentBy.get(i)
357
+ if (r) return { ...r, index: i, item: tx.item }
358
+ return { index: i, from: tx.item.owner, to: tx.to, status: 'skipped', hash: null, nonce: null, gasLimit: null, gasUsed: null, fee: null, blockNumber: null, error: skipped.get(i) ?? null, item: tx.item }
359
+ })
360
+ }
package/src/fees.ts ADDED
@@ -0,0 +1,201 @@
1
+ /**
2
+ * 手续费估算(子路径 `@w3lib/evm-batch-call/fees`):EIP-1559 的 baseFee 和三档优先费(按最近区块的 eth_feeHistory),
3
+ * 一笔交易的手续费(含 OP Stack / Scroll 的 L1 数据费);Tron 按能量 / 带宽和账户剩余的资源算要烧的 TRX
4
+ */
5
+ import type { BigNumberish } from 'ethers'
6
+
7
+ import { getNativeCurrency } from './chains.js'
8
+ import { formatUnits } from './erc20.js'
9
+ import type { Provider } from './provider.js'
10
+ import { isTronChain } from './tron.js'
11
+
12
+ export type FeeLevel = 'slow' | 'normal' | 'fast'
13
+
14
+ /** 一档费用(wei 的十进制字符串):EIP-1559 链是 maxFeePerGas / maxPriorityFeePerGas,legacy 链是 gasPrice */
15
+ export type FeeLevelEstimate = { maxFeePerGas: string; maxPriorityFeePerGas: string } | { gasPrice: string }
16
+
17
+ export interface FeeEstimate {
18
+ type: 'eip1559' | 'legacy'
19
+ /** 下一个区块的 baseFee(wei);legacy 链为 null */
20
+ baseFee: string | null
21
+ slow: FeeLevelEstimate
22
+ normal: FeeLevelEstimate
23
+ fast: FeeLevelEstimate
24
+ }
25
+
26
+ export interface EstimateFeesOptions {
27
+ /** 看最近多少个区块,默认 20 */
28
+ blocks?: number
29
+ /** 三档优先费取各区块交易优先费的百分位,默认 [10, 50, 90] */
30
+ percentiles?: readonly [number, number, number]
31
+ /** maxFeePerGas = baseFee × baseFeeMultiplier + 优先费,默认 2(baseFee 连涨几个区块也够) */
32
+ baseFeeMultiplier?: number
33
+ }
34
+
35
+ interface FeeHistory {
36
+ baseFeePerGas?: string[]
37
+ reward?: string[][]
38
+ }
39
+
40
+ /** 节点的 JSON-RPC send(ethers 的 JsonRpcProvider / BrowserProvider) */
41
+ type Sender = { send(method: string, params: unknown[]): Promise<unknown> }
42
+
43
+ async function send<T>(provider: Provider, method: string, params: unknown[]): Promise<T> {
44
+ const { runOnNodes } = await import('./write/rpc.js')
45
+ return runOnNodes(provider.rpc, method, async (node) => {
46
+ if (typeof (node as Partial<Sender>).send !== 'function') throw new Error(`${method} is not supported by this node; pass an RPC URL or an ethers Provider`)
47
+ return (await (node as unknown as Sender).send(method, params)) as T
48
+ })
49
+ }
50
+
51
+ const median = (values: bigint[]): bigint => {
52
+ if (!values.length) return 0n
53
+ const sorted = [...values].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
54
+ return sorted[Math.floor(sorted.length / 2)] as bigint
55
+ }
56
+
57
+ /**
58
+ * 当前的手续费建议(EVM 链):baseFee 取下一个区块的,三档优先费是最近区块里交易优先费的百分位的中位数;
59
+ * 节点不支持 eth_feeHistory 或链上没有 baseFee 时按 eth_gasPrice(三档为 1、1、1.2 倍)。Tron 用 estimateCost
60
+ *
61
+ * ```ts
62
+ * const fees = await estimateFees(provider)
63
+ * // { type: 'eip1559', baseFee: '1200000000', slow: { maxFeePerGas, maxPriorityFeePerGas }, normal: {…}, fast: {…} }
64
+ * ```
65
+ */
66
+ export async function estimateFees(provider: Provider, options: EstimateFeesOptions = {}): Promise<FeeEstimate> {
67
+ if (isTronChain(await provider.getChainId())) throw new Error('estimateFees is for EVM chains; use estimateCost on Tron')
68
+ const blocks = Math.max(1, Math.min(1024, Math.floor(options.blocks ?? 20)))
69
+ const percentiles = options.percentiles ?? [10, 50, 90]
70
+ if (!percentiles.every((p, i) => p >= 0 && p <= 100 && (i === 0 || p >= (percentiles[i - 1] as number)))) throw new RangeError('percentiles must be ascending numbers in [0, 100]')
71
+ const multiplier = options.baseFeeMultiplier ?? 2
72
+ let history: FeeHistory | null = null
73
+ try {
74
+ history = await send<FeeHistory>(provider, 'eth_feeHistory', [`0x${blocks.toString(16)}`, 'latest', [...percentiles]])
75
+ } catch (err) {
76
+ // 只有节点 / 链不支持 feeHistory 时才按 gasPrice;网络等其他错误照常抛出(不要在 EIP-1559 链上悄悄退回 legacy)
77
+ if (!/not (supported|available|found)|unsupported|does not exist|unknown method|-32601|method.*not/i.test(err instanceof Error ? err.message : String(err))) throw err
78
+ history = null
79
+ }
80
+ const baseFees = history?.baseFeePerGas ?? []
81
+ const next = baseFees.length ? BigInt(baseFees[baseFees.length - 1] as string) : 0n
82
+ if (!history || !baseFees.length || next === 0n) {
83
+ const gasPrice = BigInt(await send<string>(provider, 'eth_gasPrice', []))
84
+ const level = (n: bigint, d: bigint) => ({ gasPrice: ((gasPrice * n) / d).toString() })
85
+ return { type: 'legacy', baseFee: null, slow: level(1n, 1n), normal: level(1n, 1n), fast: level(6n, 5n) }
86
+ }
87
+ const rewards = history.reward ?? []
88
+ // 每档:各区块这个百分位的优先费取中位数(空区块的 0 不算)
89
+ const tip = (k: number) => median(rewards.map((r) => BigInt(r[k] ?? '0')).filter((v) => v > 0n))
90
+ const base = (next * BigInt(Math.round(multiplier * 1000))) / 1000n
91
+ const level = (priority: bigint) => ({ maxFeePerGas: (base + priority).toString(), maxPriorityFeePerGas: priority.toString() })
92
+ const [slow, normal, fast] = [tip(0), tip(1), tip(2)]
93
+ // 优先费不递减(区块很空时三档可能相同)
94
+ return { type: 'eip1559', baseFee: next.toString(), slow: level(slow), normal: level(normal > slow ? normal : slow), fast: level(fast > normal ? fast : normal > slow ? normal : slow) }
95
+ }
96
+
97
+ export interface CostCall {
98
+ /** 发送者(预估 gas、Tron 上按它剩余的资源算) */
99
+ from: string
100
+ to: string
101
+ data?: string
102
+ value?: BigNumberish
103
+ }
104
+
105
+ export interface EvmCostEstimate {
106
+ type: 'evm'
107
+ gasLimit: string
108
+ /** 这一档的每 gas 价格:EIP-1559 是 baseFee + 优先费(预计实际付的),legacy 是 gasPrice */
109
+ gasPrice: string
110
+ /** gasLimit × gasPrice(wei) */
111
+ fee: string
112
+ /** 最多付多少:gasLimit × maxFeePerGas(legacy 同 fee) */
113
+ maxFee: string
114
+ /** OP Stack / Scroll 的 L1 数据费(wei,含 25% 余量),其他链为 0 */
115
+ l1Fee: string
116
+ /** fee + l1Fee(wei)和按主币精度换算的值 */
117
+ total: string
118
+ totalFormatted: string
119
+ symbol: string | null
120
+ }
121
+
122
+ export interface TronCost {
123
+ type: 'tron'
124
+ energy: number
125
+ bandwidth: number
126
+ /** 剩余能量 / 带宽不够时要烧的 TRX(sun) */
127
+ energyBurn: string
128
+ bandwidthBurn: string
129
+ /** 转 TRX 给没激活的地址(激活费算在 bandwidthBurn 里) */
130
+ activates: boolean
131
+ energyPrice: string
132
+ bandwidthPrice: string
133
+ /** energyBurn + bandwidthBurn(sun)和按 TRX 换算的值 */
134
+ total: string
135
+ totalFormatted: string
136
+ symbol: 'TRX'
137
+ }
138
+
139
+ export type CostEstimate = EvmCostEstimate | TronCost
140
+
141
+ export interface EstimateCostOptions extends EstimateFeesOptions {
142
+ /** 用哪一档,默认 'normal' */
143
+ level?: FeeLevel
144
+ /** 已经查好的费用建议(多笔交易共用,不再查一次) */
145
+ fees?: FeeEstimate
146
+ }
147
+
148
+ /**
149
+ * 估算一笔交易的手续费:EVM 预估 gas × 这一档的价格(+ L1 数据费);Tron 模拟能量,按发送者剩余的资源算要烧的 TRX。
150
+ * 预估 gas 会执行一遍交易,revert 时抛出
151
+ *
152
+ * ```ts
153
+ * await estimateCost(provider, { from: user, to: USDT, data }) // { type: 'evm', gasLimit, fee, total, totalFormatted: '0.00042', symbol: 'BNB', … }
154
+ * await estimateCost(tronProvider, { from, to: USDT, data }) // { type: 'tron', energy: 64285, energyBurn: '6750000', totalFormatted: '7.0', … }
155
+ * ```
156
+ */
157
+ export async function estimateCost(provider: Provider, call: CostCall, options: EstimateCostOptions = {}): Promise<CostEstimate> {
158
+ const chainId = await provider.getChainId()
159
+ const value = BigInt(call.value ?? 0)
160
+ if (isTronChain(chainId)) {
161
+ const { tronEstimateCost } = await import('./write/tron.js')
162
+ const c = await tronEstimateCost(provider, { from: call.from, to: call.to, ...(call.data ? { data: call.data } : {}), value })
163
+ const total = c.energyBurn + c.bandwidthBurn
164
+ return {
165
+ type: 'tron',
166
+ energy: c.energy,
167
+ bandwidth: c.bandwidth,
168
+ energyBurn: c.energyBurn.toString(),
169
+ bandwidthBurn: c.bandwidthBurn.toString(),
170
+ activates: c.activates,
171
+ energyPrice: c.energyPrice.toString(),
172
+ bandwidthPrice: c.bandwidthPrice.toString(),
173
+ total: total.toString(),
174
+ totalFormatted: formatUnits(total, 6),
175
+ symbol: 'TRX',
176
+ }
177
+ }
178
+ const { WriteRpc, l1Fee } = await import('./write/rpc.js')
179
+ const rpc = await WriteRpc.from(provider)
180
+ const tx = { from: call.from, to: call.to, data: call.data ?? '0x', value }
181
+ const [gasLimit, fees] = await Promise.all([rpc.estimateGas(tx), options.fees ? Promise.resolve(options.fees) : estimateFees(provider, options)])
182
+ const level = fees[options.level ?? 'normal']
183
+ const perGas = 'gasPrice' in level ? BigInt(level.gasPrice) : BigInt(fees.baseFee ?? '0') + BigInt(level.maxPriorityFeePerGas)
184
+ const maxPerGas = 'gasPrice' in level ? perGas : BigInt(level.maxFeePerGas)
185
+ const feeFields = 'gasPrice' in level ? { type: 0, gasPrice: perGas } : { type: 2, maxFeePerGas: maxPerGas, maxPriorityFeePerGas: BigInt(level.maxPriorityFeePerGas) }
186
+ const l1 = await l1Fee(rpc, { ...tx, from: undefined, gasLimit, ...feeFields })
187
+ const fee = gasLimit * perGas
188
+ const total = fee + l1
189
+ const native = getNativeCurrency(chainId)
190
+ return {
191
+ type: 'evm',
192
+ gasLimit: gasLimit.toString(),
193
+ gasPrice: perGas.toString(),
194
+ fee: fee.toString(),
195
+ maxFee: (gasLimit * maxPerGas).toString(),
196
+ l1Fee: l1.toString(),
197
+ total: total.toString(),
198
+ totalFormatted: formatUnits(total, native?.decimals ?? 18),
199
+ symbol: native?.symbol ?? null,
200
+ }
201
+ }