@w3lib/solana-batch-call 0.4.0 → 0.6.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 (223) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +343 -3
  3. package/dist/approvals.d.ts +129 -0
  4. package/dist/approvals.d.ts.map +1 -0
  5. package/dist/approvals.js +221 -0
  6. package/dist/approvals.js.map +1 -0
  7. package/dist/batchPlan.d.ts +52 -0
  8. package/dist/batchPlan.d.ts.map +1 -0
  9. package/dist/batchPlan.js +85 -0
  10. package/dist/batchPlan.js.map +1 -0
  11. package/dist/claim.d.ts +161 -0
  12. package/dist/claim.d.ts.map +1 -0
  13. package/dist/claim.js +197 -0
  14. package/dist/claim.js.map +1 -0
  15. package/dist/decode.d.ts +22 -0
  16. package/dist/decode.d.ts.map +1 -0
  17. package/dist/decode.js +187 -0
  18. package/dist/decode.js.map +1 -0
  19. package/dist/describe.d.ts +26 -0
  20. package/dist/describe.d.ts.map +1 -0
  21. package/dist/describe.js +58 -0
  22. package/dist/describe.js.map +1 -0
  23. package/dist/fees.d.ts +89 -0
  24. package/dist/fees.d.ts.map +1 -0
  25. package/dist/fees.js +166 -0
  26. package/dist/fees.js.map +1 -0
  27. package/dist/history.d.ts +125 -0
  28. package/dist/history.d.ts.map +1 -0
  29. package/dist/history.js +231 -0
  30. package/dist/history.js.map +1 -0
  31. package/dist/historyHelius.d.ts +52 -0
  32. package/dist/historyHelius.d.ts.map +1 -0
  33. package/dist/historyHelius.js +125 -0
  34. package/dist/historyHelius.js.map +1 -0
  35. package/dist/layout.d.ts +2 -0
  36. package/dist/layout.d.ts.map +1 -1
  37. package/dist/layout.js +2 -2
  38. package/dist/layout.js.map +1 -1
  39. package/dist/liquidity/orca.d.ts +6 -0
  40. package/dist/liquidity/orca.d.ts.map +1 -1
  41. package/dist/liquidity/orca.js +14 -3
  42. package/dist/liquidity/orca.js.map +1 -1
  43. package/dist/liquidity/raydiumClmm.d.ts +6 -0
  44. package/dist/liquidity/raydiumClmm.d.ts.map +1 -1
  45. package/dist/liquidity/raydiumClmm.js +12 -5
  46. package/dist/liquidity/raydiumClmm.js.map +1 -1
  47. package/dist/lp.d.ts +55 -15
  48. package/dist/lp.d.ts.map +1 -1
  49. package/dist/lp.js +98 -46
  50. package/dist/lp.js.map +1 -1
  51. package/dist/lpMath.d.ts +22 -0
  52. package/dist/lpMath.d.ts.map +1 -1
  53. package/dist/lpMath.js +21 -0
  54. package/dist/lpMath.js.map +1 -1
  55. package/dist/names.d.ts +68 -0
  56. package/dist/names.d.ts.map +1 -0
  57. package/dist/names.js +257 -0
  58. package/dist/names.js.map +1 -0
  59. package/dist/pools.d.ts +9 -0
  60. package/dist/pools.d.ts.map +1 -0
  61. package/dist/pools.js +45 -0
  62. package/dist/pools.js.map +1 -0
  63. package/dist/portfolio.d.ts +64 -0
  64. package/dist/portfolio.d.ts.map +1 -0
  65. package/dist/portfolio.js +57 -0
  66. package/dist/portfolio.js.map +1 -0
  67. package/dist/price.d.ts +90 -0
  68. package/dist/price.d.ts.map +1 -0
  69. package/dist/price.js +287 -0
  70. package/dist/price.js.map +1 -0
  71. package/dist/programs.d.ts +19 -0
  72. package/dist/programs.d.ts.map +1 -0
  73. package/dist/programs.js +175 -0
  74. package/dist/programs.js.map +1 -0
  75. package/dist/reclaim.d.ts +190 -0
  76. package/dist/reclaim.d.ts.map +1 -0
  77. package/dist/reclaim.js +399 -0
  78. package/dist/reclaim.js.map +1 -0
  79. package/dist/risk.d.ts +84 -0
  80. package/dist/risk.d.ts.map +1 -0
  81. package/dist/risk.js +211 -0
  82. package/dist/risk.js.map +1 -0
  83. package/dist/rpc.d.ts +2 -0
  84. package/dist/rpc.d.ts.map +1 -1
  85. package/dist/rpc.js +36 -10
  86. package/dist/rpc.js.map +1 -1
  87. package/dist/simulate.d.ts +106 -0
  88. package/dist/simulate.d.ts.map +1 -0
  89. package/dist/simulate.js +418 -0
  90. package/dist/simulate.js.map +1 -0
  91. package/dist/subpaths/approvals.d.ts +29 -0
  92. package/dist/subpaths/approvals.d.ts.map +1 -0
  93. package/dist/subpaths/approvals.js +30 -0
  94. package/dist/subpaths/approvals.js.map +1 -0
  95. package/dist/subpaths/claim.d.ts +31 -0
  96. package/dist/subpaths/claim.d.ts.map +1 -0
  97. package/dist/subpaths/claim.js +31 -0
  98. package/dist/subpaths/claim.js.map +1 -0
  99. package/dist/subpaths/fees.d.ts +23 -0
  100. package/dist/subpaths/fees.d.ts.map +1 -0
  101. package/dist/subpaths/fees.js +25 -0
  102. package/dist/subpaths/fees.js.map +1 -0
  103. package/dist/subpaths/history.d.ts +21 -0
  104. package/dist/subpaths/history.d.ts.map +1 -0
  105. package/dist/subpaths/history.js +22 -0
  106. package/dist/subpaths/history.js.map +1 -0
  107. package/dist/subpaths/lp.d.ts +2 -2
  108. package/dist/subpaths/lp.d.ts.map +1 -1
  109. package/dist/subpaths/lp.js +2 -2
  110. package/dist/subpaths/lp.js.map +1 -1
  111. package/dist/subpaths/names.d.ts +37 -0
  112. package/dist/subpaths/names.d.ts.map +1 -0
  113. package/dist/subpaths/names.js +27 -0
  114. package/dist/subpaths/names.js.map +1 -0
  115. package/dist/subpaths/price.d.ts +40 -0
  116. package/dist/subpaths/price.d.ts.map +1 -0
  117. package/dist/subpaths/price.js +43 -0
  118. package/dist/subpaths/price.js.map +1 -0
  119. package/dist/subpaths/reclaim.d.ts +34 -0
  120. package/dist/subpaths/reclaim.d.ts.map +1 -0
  121. package/dist/subpaths/reclaim.js +34 -0
  122. package/dist/subpaths/reclaim.js.map +1 -0
  123. package/dist/subpaths/risk.d.ts +28 -0
  124. package/dist/subpaths/risk.d.ts.map +1 -0
  125. package/dist/subpaths/risk.js +30 -0
  126. package/dist/subpaths/risk.js.map +1 -0
  127. package/dist/subpaths/simulate.d.ts +22 -0
  128. package/dist/subpaths/simulate.d.ts.map +1 -0
  129. package/dist/subpaths/simulate.js +23 -0
  130. package/dist/subpaths/simulate.js.map +1 -0
  131. package/dist/subpaths/swap.d.ts +22 -0
  132. package/dist/subpaths/swap.d.ts.map +1 -0
  133. package/dist/subpaths/swap.js +21 -0
  134. package/dist/subpaths/swap.js.map +1 -0
  135. package/dist/subpaths/write.d.ts +1 -1
  136. package/dist/subpaths/write.d.ts.map +1 -1
  137. package/dist/subpaths/write.js +1 -1
  138. package/dist/subpaths/write.js.map +1 -1
  139. package/dist/swap/amm.d.ts +20 -0
  140. package/dist/swap/amm.d.ts.map +1 -0
  141. package/dist/swap/amm.js +77 -0
  142. package/dist/swap/amm.js.map +1 -0
  143. package/dist/swap/clmmMath.d.ts +24 -0
  144. package/dist/swap/clmmMath.d.ts.map +1 -0
  145. package/dist/swap/clmmMath.js +82 -0
  146. package/dist/swap/clmmMath.js.map +1 -0
  147. package/dist/swap/common.d.ts +68 -0
  148. package/dist/swap/common.d.ts.map +1 -0
  149. package/dist/swap/common.js +65 -0
  150. package/dist/swap/common.js.map +1 -0
  151. package/dist/swap/cpmm.d.ts +28 -0
  152. package/dist/swap/cpmm.d.ts.map +1 -0
  153. package/dist/swap/cpmm.js +113 -0
  154. package/dist/swap/cpmm.js.map +1 -0
  155. package/dist/swap/index.d.ts +145 -0
  156. package/dist/swap/index.d.ts.map +1 -0
  157. package/dist/swap/index.js +349 -0
  158. package/dist/swap/index.js.map +1 -0
  159. package/dist/swap/orca.d.ts +50 -0
  160. package/dist/swap/orca.d.ts.map +1 -0
  161. package/dist/swap/orca.js +382 -0
  162. package/dist/swap/orca.js.map +1 -0
  163. package/dist/swap/raydiumClmm.d.ts +62 -0
  164. package/dist/swap/raydiumClmm.d.ts.map +1 -0
  165. package/dist/swap/raydiumClmm.js +447 -0
  166. package/dist/swap/raydiumClmm.js.map +1 -0
  167. package/dist/tradeCheck.d.ts +45 -0
  168. package/dist/tradeCheck.d.ts.map +1 -0
  169. package/dist/tradeCheck.js +131 -0
  170. package/dist/tradeCheck.js.map +1 -0
  171. package/dist/write/instructions.d.ts +9 -0
  172. package/dist/write/instructions.d.ts.map +1 -1
  173. package/dist/write/instructions.js +40 -1
  174. package/dist/write/instructions.js.map +1 -1
  175. package/dist/write/transaction.d.ts +23 -0
  176. package/dist/write/transaction.d.ts.map +1 -1
  177. package/dist/write/transaction.js +50 -0
  178. package/dist/write/transaction.js.map +1 -1
  179. package/package.json +66 -14
  180. package/src/approvals.ts +358 -0
  181. package/src/batchPlan.ts +137 -0
  182. package/src/claim.ts +323 -0
  183. package/src/decode.ts +202 -0
  184. package/src/describe.ts +77 -0
  185. package/src/fees.ts +259 -0
  186. package/src/history.ts +336 -0
  187. package/src/historyHelius.ts +136 -0
  188. package/src/layout.ts +2 -2
  189. package/src/liquidity/orca.ts +15 -5
  190. package/src/liquidity/raydiumClmm.ts +13 -5
  191. package/src/lp.ts +148 -59
  192. package/src/lpMath.ts +38 -0
  193. package/src/names.ts +329 -0
  194. package/src/pools.ts +43 -0
  195. package/src/portfolio.ts +124 -0
  196. package/src/price.ts +366 -0
  197. package/src/programs.ts +216 -0
  198. package/src/reclaim.ts +547 -0
  199. package/src/risk.ts +243 -0
  200. package/src/rpc.ts +39 -10
  201. package/src/simulate.ts +535 -0
  202. package/src/subpaths/approvals.ts +47 -0
  203. package/src/subpaths/claim.ts +53 -0
  204. package/src/subpaths/fees.ts +41 -0
  205. package/src/subpaths/history.ts +38 -0
  206. package/src/subpaths/lp.ts +5 -1
  207. package/src/subpaths/names.ts +45 -0
  208. package/src/subpaths/price.ts +62 -0
  209. package/src/subpaths/reclaim.ts +57 -0
  210. package/src/subpaths/risk.ts +36 -0
  211. package/src/subpaths/simulate.ts +37 -0
  212. package/src/subpaths/swap.ts +41 -0
  213. package/src/subpaths/write.ts +2 -0
  214. package/src/swap/amm.ts +88 -0
  215. package/src/swap/clmmMath.ts +96 -0
  216. package/src/swap/common.ts +113 -0
  217. package/src/swap/cpmm.ts +132 -0
  218. package/src/swap/index.ts +467 -0
  219. package/src/swap/orca.ts +397 -0
  220. package/src/swap/raydiumClmm.ts +464 -0
  221. package/src/tradeCheck.ts +166 -0
  222. package/src/write/instructions.ts +43 -1
  223. package/src/write/transaction.ts +62 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### 新功能
6
+
7
+ - 交易模拟,子路径 `@w3lib/solana-batch-call/simulate`:`getSimulation` / `simulate` 在主网模拟一笔交易(`simulateTransaction`,不验签名、不上链),输入是 base64 / 字节的交易(legacy、v0 含地址查找表)或本库构建器的输出(`buildSwap`、流动性计划、`{ owner, instructions }`、`buildRevokes` / `buildCloses`)。给出余额变化(SOL 不含手续费、Wrapped SOL 并入 SOL、可选美元价值)、授权变化(delegate 设置 / 变更 / 撤销、账户 owner / 关闭权限 / mint 权限被改、关闭账户,按模拟前后的账户状态比较)、指令解码(System、Token / Token-2022、ATA、计算预算、Memo,Jupiter / Raydium / Orca / Meteora / Pump.fun 的协议和指令名)、程序错误解码、计算单元、手续费和警告(无限额度授权、SetAuthority、改 owner、关闭账户退给别人、不认识的程序、新收款地址、多余的签名者、手续费过高、花掉大部分余额;可选 `/risk` 检查变化的代币)。publicnode 的 `simulateTransaction` 最多返回 10 个账户,自动只看和钱包有关的账户并分批
8
+ - 交易历史,子路径 `@w3lib/solana-batch-call/history`:`getHistory` 把地址的交易整理成统一的 `Activity`(兑换、转账、授权、流动性、铸造、合约调用、失败,带方向、对手方、资产变化、手续费、协议),`cursor` 是最后一条签名,支持 `limit`、`before` / `after`(区块时间)、`types` 过滤。数据源可换:默认 `rpcHistory()`(`getSignaturesForAddress` + `getTransaction`,限并发),可选 `heliusHistory({ apiKey })`;传数组时按顺序尝试
9
+ - 账户回收,子路径 `@w3lib/solana-batch-call/reclaim`:`getReclaimable` / `reclaimable` 扫描钱包里关闭后能退回租金的账户并给出每项和合计的 lamports:空代币账户(SPL Token / Token-2022,有暂存转账手续费的先收到 mint 上)、垃圾币(销毁后关闭;保守的默认判断:有价格且价值高的、NFT、仓位 NFT 一律不当垃圾币,只勾选有价格的尘埃 / `/risk` 标记的,可疑名字只作提示不单独触发勾选,可传 `isJunk`、`prices`、`maxJunkUsd`)、Wrapped SOL(解包)、已清空的 Orca Whirlpool / Raydium CLMM 仓位(关闭仓位、销毁 NFT,Token-2022 的 NFT 连 mint 一起退回);冻结 / 关闭权限是别人 / 仓位还有没领的手续费的列在 `blocked`。`buildReclaim` 发送前重新读取账户并核对(余额变了、冻结、仓位不再为空的跳过并给出原因),`simulateReclaim` 用 `/simulate` 逐笔模拟并给出预计退回的 SOL,`reclaim` 构建 → 模拟 → 签名发送(打包同 `batchCall`,`destination`、`feePayer`、`dryRun`)。`/write` 新增指令工具 `burnChecked`
10
+ - 批量领取,子路径 `@w3lib/solana-batch-call/claim`:`getClaimable` / `claimable` 只读预览 Orca Whirlpool、Raydium CLMM 仓位的未领取手续费和挖矿奖励(Orca 奖励 slot、Raydium 农场奖励,数量按程序的公式结算),带美元价值和按代币汇总;`buildClaimAll` 复用 `/liquidity` 的 `collectFees` 为每个仓位构建领取(手续费 + 全部奖励),`simulateClaim` 用 `/simulate` 模拟并汇总收到的代币,`claimAll` 构建 → 模拟 → 签名发送(`minUsd`、`dryRun`)
11
+ - `/lp`:`getLpPositions` 新增 `rewards: true`(仓位的 `rewards`、`rewardsUsd`,`now` 指定结算时刻),`/lp` 导出 `rewardGrowthGlobal`、`getRewardGrowthInside`;`/liquidity` 内部抽出 `orcaClosePosition` / `raydiumClosePosition`(关闭已清空的仓位)
12
+
13
+ ### 修复
14
+
15
+ - publicnode 对 `getMultipleAccounts` 超过 10 个账户返回 “A maximum of 10 accounts is allowed”(之前只认 “too many accounts” 和 403 “Request blocked”),会被当成节点故障。现在识别这条报错,直接按它给出的上限拆分
16
+
17
+ ## 0.5.0
18
+
19
+ ### 新功能
20
+
21
+ - 价格,子路径 `@w3lib/solana-batch-call/price`:
22
+ - `getTokenPrices` / `tokenPrices`:代币(含原生 SOL)的美元价格,依次使用多个价格源,结果带给出价格的源
23
+ - `onchainPrices()`:从 Raydium CLMM、Orca Whirlpool、Raydium CPMM 的池子算价格(按 PDA 推导池子地址,不需要 `getProgramAccounts`;对 Wrapped SOL、USDC、USDT,用最深的池子,流动性下限 `minLiquidityUsd`)
24
+ - `defillamaPrices({ at })`:历史价格
25
+ - `combinePrices([...])`:组合多个价格源,可以传给 `getLpPositions` 的 `prices`
26
+ - `getPortfolio` / `portfolio`:地址的资产总值(指定代币按 ATA 读余额,或列出全部持仓;可选加上 LP 仓位)
27
+ - 手续费估算,子路径 `@w3lib/solana-batch-call/fees`:`getFeeEstimate` / `estimateFees`(按 `getRecentPrioritizationFees` 的三档优先费,可以只看交易会写的账户)、`getCostEstimate` / `estimateCost`(模拟一次算一笔交易的花费:每个签名 5000 lamports + 计算单元上限 × 单价 + 新建账户的免租押金;支持指令和构建好的 legacy / v0 交易)
28
+ - 域名解析,子路径 `@w3lib/solana-batch-call/names`:SNS(Bonfida,`.sns`)的 `getNameAddresses` / `resolveNames`(名字 → 地址,解析顺序同官方 SDK:代币化域名的 NFT 持有人、V2 / V1 的 SOL 记录、owner)、`getAddressNames` / `lookupAddresses`(地址 → 主域名,核对名字和归属)。`.sol` 已迁到 SRS、SNS 官方 SDK 不再解析,默认不认,`legacySol: true` 时按同名 `.sns` 解析
29
+ - 授权管理,子路径 `@w3lib/solana-batch-call/approvals`:
30
+ - `getApprovals` / `approvals`:代币账户的 delegate 授权、别人持有的关闭权限、余额为 0 可以关闭的账户(指定 mints 时读 ATA,免费公共节点也能用;不传时列出全部代币账户),带能否撤销 / 关闭和原因、Token-2022 暂存的转账手续费、mint 的永久授权
31
+ - `buildRevokes` / `revokeApprovals`:批量 Revoke;`buildCloses` / `closeTokenAccounts`:批量关闭回收租金(有暂存转账手续费时先 HarvestWithheldTokensToMint),发送同 `batchCall`
32
+ - `/write` 新增指令工具 `revoke`、`harvestWithheldTokensToMint`
33
+ - 价格源的参数(`SolanaPriceSourceContext`)新增 `client`(链上价格源用);`defillamaPrices`、`SolanaPriceSource` 仍可以从 `/lp` 导入
34
+ - 兑换,子路径 `@w3lib/solana-batch-call/swap`(不依赖各 DEX 的 SDK、不调用聚合器):
35
+ - `buildSwap` / `getSwapQuote` 只构建(报价、最少收到、所有候选路线、按顺序执行的指令),`swap` 构建后签名发送(钱包同 `/write`)
36
+ - Raydium CLMM、Orca Whirlpool、Raydium CPMM 按 PDA 推导池子,Raydium AMM v4 和其他池子用 `pools` 传入;直接兑换和经过 SOL / USDC / USDT 的两跳,选输出最多的
37
+ - 卖出 SOL 时余额检查给手续费和租金留 `SWAP_SOL_RESERVE`(0.01 SOL);Raydium AMM v4 只支持 status 6(SwapOnly)的池子
38
+ - 本地报价照搬各程序的计算和取整:集中流动性逐个 tick 区间计算(Raydium 的 `fee_on`、动态费率、tick 上的限价单,Orca 的自适应费率),CPMM 的创建者手续费,AMM v4 的待提取收益,Token-2022 转账手续费;主网同一 slot 读账户 + 模拟交易核对 23 个场景(四个协议两个方向、大额穿过上百个 tick、限价单、两跳、转账手续费)全部逐位一致
39
+ - SOL 自动包装 / 解包、缺少的代币账户自动创建、按滑点设每一跳的最少收到、放不进一笔交易的路线跳过
40
+ - 代币风险检查,子路径 `@w3lib/solana-batch-call/risk`:
41
+ - `getTokenRisks` / `tokenRisks`:增发 / 冻结权限,Token-2022 的转账手续费(当前和即将生效的费率、能不能改)、永久授权、转账 hook、不可转让、默认冻结、可暂停、保密转账、mint 关闭权限、显示数量会变、销毁需批准,汇总成 `flags`;可选持币集中度(`concentration: true`)
42
+ - `getTradeChecks` / `tradeChecks`:用 `/swap` 模拟“买入 → 卖出”,判断能不能买、能不能卖(`honeypot`)和来回损失
43
+
44
+ ### 修复
45
+
46
+ - publicnode(默认节点)对单次超过 10 个账户的 `getMultipleAccounts` 返回 HTTP 403 “Request blocked”(2026-10 起),之前会被当成节点故障、换到限频很严的官方节点,`getBalances` 等较大的读取经常失败。现在按 10 个一组重新拆分;从报错里学到的节点限制按节点地址共享,新建的客户端不用再试一遍
47
+
3
48
  ## 0.4.0
4
49
 
5
50
  ### 新功能
package/README.md CHANGED
@@ -1,17 +1,23 @@
1
1
  # @w3lib/solana-batch-call
2
2
 
3
- Solana 批量读取库:余额(SOL / SPL Token / Token-2022)、代币详情、NFT、[LP 仓位](#lp-仓位)(子路径 `/lp`);[添加 / 移除流动性](#添加--移除流动性)(子路径 `/liquidity`);以及[批量写操作](#批量写操作)(一对多转账、多钱包归集、批量调用程序,子路径 `/write`)。不依赖 Solana SDK,直接发 JSON-RPC。
3
+ Solana 批量读取库:余额(SOL / SPL Token / Token-2022)、代币详情、NFT、[LP 仓位](#lp-仓位)(子路径 `/lp`)、[价格和资产总值](#价格)(子路径 `/price`)、[手续费估算](#手续费估算)(子路径 `/fees`)、[SNS 域名解析](#域名解析)(子路径 `/names`)、[授权管理](#授权管理)(子路径 `/approvals`)、[代币风险检查](#代币风险)(子路径 `/risk`);[交易模拟](#交易模拟)(子路径 `/simulate`)、[交易历史](#交易历史)(子路径 `/history`);[添加 / 移除流动性](#添加--移除流动性)(子路径 `/liquidity`)、[兑换](#兑换)(子路径 `/swap`);以及[批量写操作](#批量写操作)(一对多转账、多钱包归集、批量调用程序,子路径 `/write`)。不依赖 Solana SDK,直接发 JSON-RPC。
4
4
 
5
5
  ```bash
6
6
  pnpm add @w3lib/solana-batch-call
7
7
  ```
8
8
 
9
- **在线 demo**:<https://web3-lib.github.io/libs/solana/>(余额、代币信息、资产列表、NFT、LP 仓位,显示每次查询实际发出的 RPC 调用数)。本地运行:`pnpm --filter @w3lib/solana-batch-call demo`(http://localhost:5175)
9
+ **在线 demo**:<https://web3-lib.github.io/libs/solana/>(余额、代币信息、资产列表、NFT、LP 仓位、添加 / 移除流动性、代币价格和资产总值、授权查询和撤销 / 关闭空代币账户、SNS 域名解析、优先费估算、兑换、代币风险检查;写操作先报价 / 构建,连接浏览器钱包后可以发送;显示每次查询实际发出的 RPC 调用数)。本地运行:`pnpm --filter @w3lib/solana-batch-call demo`(http://localhost:5175)
10
10
 
11
11
  - **一次请求查一批**:账户读取用 `getMultipleAccounts`(自动去重、按 100 个一组拆分、最多同时 3 个请求);同一 tick 内发起的 RPC 调用合并成一个 JSON-RPC 批量请求
12
12
  - **免费节点也能查余额**:指定代币时在本地推导关联代币账户(ATA)地址,和 mint 账户一起用 `getMultipleAccounts` 读完,不需要 `getTokenAccountsByOwner` 这类索引方法
13
13
  - **只查主币不读账户数据**:SOL 余额(`getSolBalances`、单币模式里的主币)用带 `dataSlice` 的 `getMultipleAccounts` 只取 lamports,与代币账户分开两组、同一个批量请求发出——钱包是数据很大的程序账户时也不会拖慢或超时
14
14
  - **Token-2022**:余额、decimals、TokenMetadata 扩展里的 name / symbol / uri 都支持
15
+ - **价格**(子路径 `/price`):DefiLlama 当前 / 历史价格、链上 Raydium / Orca 池子价格(按 PDA 推导池子,不需要 `getProgramAccounts`),组合价格源,地址的资产总值(代币 + LP 仓位)
16
+ - **手续费估算**(子路径 `/fees`):三档优先费建议(可以只看交易会写的账户),一笔交易的花费(基础费 + 优先费 + 新建账户的免租押金),支持指令和构建好的 legacy / v0 交易
17
+ - **域名解析**(子路径 `/names`):SNS(Bonfida,`.sns`),批量正查(SOL 记录 V1 / V2、代币化域名)、反查主域名(核对属于这个地址)
18
+ - **授权管理**(子路径 `/approvals`):代币账户的 delegate 授权、可以关闭回收租金的空账户,批量撤销 / 关闭(Token-2022 暂存的转账手续费自动先收走)
19
+ - **兑换**(子路径 `/swap`):Raydium CLMM / CPMM / AMM v4、Orca Whirlpool 的池子里找最好的路线(直接或经过 SOL / USDC / USDT 两跳),按链上状态本地报价(与程序逐位一致,主网模拟核对),SOL 自动包装 / 解包
20
+ - **代币风险**(子路径 `/risk`):增发 / 冻结权限、Token-2022 的转账手续费、永久授权、转账 hook、不可转让、默认冻结、可暂停等扩展;可选模拟“买入 → 卖出”判断能不能卖
15
21
  - **多节点故障切换**:限频、403、需要 API Key、不支持某方法等节点问题自动换下一个节点;节点不支持批量请求或限制批量大小时自动降级
16
22
  - **网络校验**:按创世区块哈希确认节点所在网络,不会把 devnet 的数据当成 mainnet 的返回
17
23
  - 结果字段都是字符串 / 数字 / 布尔 / null,可以直接 `JSON.stringify`
@@ -206,6 +212,336 @@ await collectFees(client, secretKey, { position })
206
212
 
207
213
  打包:在已经用到 `SolanaClient` 的基础上,只用 `build*` 约增加 43KB(minify,gzip 约 14KB,含 `/lp` 的仓位查询),加上发送约 66KB(gzip 约 22KB,含 `/write` 的发送代码)。
208
214
 
215
+ ## 兑换
216
+
217
+ 子路径 `@w3lib/solana-batch-call/swap`:在 **Raydium CLMM**、**Orca Whirlpool**、**Raydium CPMM**、**Raydium AMM v4** 的池子里兑换(按输入数量),与 [添加 / 移除流动性](#添加--移除流动性) 相同的四个协议。不依赖各 DEX 的 SDK,也不调用聚合器的 API:报价按链上状态在本地计算,照搬各程序的计算步骤和取整,结果与程序逐位一致。
218
+
219
+ - `buildSwap` / `getSwapQuote`:只读取、不签名,返回报价、最少收到、所有候选路线和按顺序执行的指令(`steps`)
220
+ - `swap`:构建后签名发送,钱包和发送参数与 `/write` 的 `batchCall` 相同;创建代币账户和兑换能放进同一笔交易时合成一笔
221
+
222
+ ```ts
223
+ import { NATIVE_MINT, SolanaClient } from '@w3lib/solana-batch-call'
224
+ import { buildSwap, getSwapQuote, swap } from '@w3lib/solana-batch-call/swap'
225
+
226
+ const client = new SolanaClient(rpcUrl)
227
+ const plan = await buildSwap(client, owner, { tokenIn: NATIVE_MINT, tokenOut: USDC, amountInFormatted: '1' })
228
+ // { amountOut: '104338470', amountOutFormatted: '104.33847', amountOutMin: '103816777', price: 104.338, priceImpact: 0.00003,
229
+ // route: { path: [SOL, USDC], hops: [{ protocol: 'raydium-clmm', pool: '3ucNos4N…', amountIn, amountOut, amountOutMin }], amountOut, value },
230
+ // routes: [...], // 所有能报价的路线,按 value 从高到低
231
+ // leftover: null, // 两跳时留在账户里的中间代币
232
+ // native: 'tokenIn', // 用 SOL 支付(自动包装)
233
+ // skipped: [{ protocol, pool, reason }], // 没用上的池子和原因
234
+ // steps: [{ kind: 'setup', instructions }, { kind: 'swap', instructions }] }
235
+
236
+ await swap(client, secretKey, { tokenIn: USDC, tokenOut: NATIVE_MINT, amountInFormatted: '100' }, { slippage: 0.01 })
237
+ await getSwapQuote(owner, { tokenIn: JUP, tokenOut: USDC, amountIn: 10_000_000n }, { provider: rpcUrl }) // 节点参数同 getBalances
238
+ ```
239
+
240
+ 找池子:
241
+
242
+ - Raydium CLMM、CPMM、Orca Whirlpool 按 PDA 推导池子地址(与 `/price` 的链上价格相同:21 个 AmmConfig、22 个 Orca 费率档位),不需要 `getProgramAccounts`。直接兑换和经过中间代币(`intermediates`,默认 Wrapped SOL、USDC、USDT)的两跳一起找,候选池子和报价要用的账户(配置、金库、tick 数组、Orca 的 Oracle、Clock)用合并的 `getMultipleAccounts` 读完,一般两到三轮
243
+ - Raydium AMM v4 的池子地址要用 OpenBook 市场推导,不在自动查找范围内:放进 `pools` 才会用(如 SOL / USDC 的 `58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2`);`pools` 也可以放用 seed_index 建的 Raydium CLMM 池子等其他地址。`discover: false` 时只用 `pools` 里的池子
244
+ - 每条路线的报价都在本地算,选 `value` 最大的;第一条放不进一笔交易(1232 字节、64 个账户)的路线跳过,用下一条
245
+
246
+ 报价(与程序一致的部分):
247
+
248
+ | | 计算 | 一起算进去的 |
249
+ | --- | --- | --- |
250
+ | Raydium CLMM | `swap_v2`:按 tick 数组位图找下一个已初始化的 tick,逐个区间计算(Q64.64,取整同程序) | 手续费从输入或输出扣(`fee_on`)、动态费率、tick 上的限价单(先按 tick 价格成交)、Token-2022 转账手续费 |
251
+ | Orca Whirlpool | `swap_v2`:当前价格起的 3 个 tick 数组(不存在的按全空),逐个区间计算 | 自适应费率(Oracle 里的波动累计,按 Clock 的时间衰减)、Token-2022 转账手续费 |
252
+ | Raydium CPMM | `swap_base_input`:恒定乘积,手续费向上取整 | 创建者手续费(从输入或输出扣)、储备扣掉协议 / 基金 / 创建者手续费、Token-2022 转账手续费 |
253
+ | Raydium AMM v4 | `swap_base_in_v2`(不需要 OpenBook 账户,只支持 status 6 SwapOnly 的池子,其他状态不报价):恒定乘积,手续费向上取整 | 储备扣掉待提取的收益(need_take_pnl) |
254
+
255
+ - **两跳**:第二跳卖出的是第一跳的**最少收到**(交易里每一跳的数量要事先定好,第一跳实际多收到的不会卖掉);多出来的中间代币留在账户里(`leftover`,按当前状态是第一跳报价 − 最少收到),中间代币是 Wrapped SOL 且原来没有 wSOL 账户时随账户关闭换回 SOL。`amountOut` 是这样实际能收到的数量;比较路线用 `value`(第二跳按第一跳的全部报价卖出能拿到的)
256
+ - **滑点**:每一跳的最少收到 = 报价 × (1 − `slippage`),默认 0.005;价格影响 `priceImpact` 按同一路线卖出 1 / 10000 的数量的价格比
257
+ - **SOL**:`tokenIn` / `tokenOut` 传 `NATIVE_MINT`(或客户端的 `nativeMints`)时自动包装 / 解包,规则同添加流动性(`native` 选项,默认 true):wSOL 账户原来没有时创建、用完关闭;原来就有时卖出先用已有的 wSOL,买入的留在 wSOL 账户里
258
+ - **代币账户**:卖出、买入、中间代币都用关联代币账户(ATA),没有时创建;卖出的代币不在 ATA 里时不支持
259
+ - **Token-2022**:转账手续费按当前 epoch 的费率算进报价(`amountOut` 是扣完手续费实际收到的);带转账 hook 的代币不支持(池子跳过)
260
+ - `swap` 发送前检查卖出的余额(`checkBalance`,默认 true);交易发送前照常模拟,失败时不发送
261
+
262
+ 核对方式:在同一个 JSON-RPC 批量请求里读账户(池子、配置、tick 数组、Oracle、Clock、用户的代币账户)并模拟兑换交易(`simulateTransaction`,不验签名),两者 `context.slot` 相同时用读到的账户在本地重新报价,和模拟后代币账户的余额变化比较。2026-10-20 主网,23 个场景全部逐位一致(`test/swap-live.test.ts`;快照存为 `test/fixtures/swap-mainnet.json.gz`,单元测试离线重放):
263
+
264
+ | 场景 | 本地报价 | 模拟收到 |
265
+ | --- | --- | --- |
266
+ | Raydium CLMM SOL → USDC 3 SOL / USDC → SOL 250 USDC | 311,282,691 / 2,406,607,223 | 一致 |
267
+ | Orca SOL → USDC 3 SOL(自适应费率池)/ USDC → SOL 250 USDC | 311,525,883 / 2,404,623,893 | 一致 |
268
+ | Raydium CPMM SOL → USDC / USDC → SOL | 149,994,662 / 1,287,814,267 | 一致 |
269
+ | Raydium AMM v4 SOL → USDC / USDC → SOL | 311,213,424 / 2,397,749,721 | 一致 |
270
+ | Raydium CLMM 3000 SOL → USDC(穿过 109 个 tick + 1 个限价单) | 309,164,899,689 | 一致 |
271
+ | Raydium CLMM 400000 USDC → SOL(穿过 98 个 tick) | 3,827,084,863,504 | 一致 |
272
+ | Raydium CLMM USDC → JUP / SOL → JUP(成交 tick 上的限价单) | 927,247,953 / 17,710,692,783 | 一致 |
273
+ | Orca 自适应费率池 25 SOL → USDC(穿过 14 个 tick) | 2,570,899,908 | 一致 |
274
+ | Raydium CPMM SOL → BERN / BERN → SOL(Token-2022,转账手续费 2.69%) | 657,602 / 97,156 | 一致 |
275
+ | 两跳 SOL → USDC → USDT、USDC → SOL → USDT(Orca + Raydium)、BERN → SOL → USDC | 收到的和留下的中间代币 | 都一致 |
276
+
277
+ **局限**:
278
+
279
+ - 只做按输入数量兑换(exact in);不使用地址查找表(ALT),一条路线要能放进一笔 legacy 交易,放不下的路线跳过
280
+ - Raydium CLMM:最多读 10 个已初始化的 tick 数组(先读 2 个,不够再补读);需要 tick 数组位图扩展账户的池子(当前价格或兑换路径超出池子自带位图覆盖的范围,只有 tick spacing < 15 的池子会碰到)不支持。一次穿过太多已初始化 tick 的大额兑换可能超过 140 万计算单元,报价能算出来但交易会失败(如在 SOL / USDC 的 spacing 1 池子一次卖 6000 SOL;3000 SOL、穿过 109 个 tick 的没问题)
281
+ - Raydium 的动态费率和 `fee_on`(手续费从输出扣)照搬了程序,但 2026-10 主网的 PDA 池子里还没有用到它们的,只用改过字段的快照检查了方向,没有和模拟核对过
282
+ - Orca:程序只接受当前价格起的 3 个 tick 数组,走不完的数量报价不可用(池子跳过)
283
+ - 报价是读取时的链上状态,发送时价格可能已经变了;保护靠最少收到。Raydium 的动态费率、Orca 的自适应费率按 Clock 的时间衰减,与上链时的时间不同会差一点
284
+ - 两跳时第一跳多收到的中间代币留在账户里(见上),不会一起卖掉
285
+ - 不查 Meteora、PumpSwap、Phoenix 等其他 DEX;要更好的价格可以用聚合器(如 Jupiter)
286
+
287
+ 打包:在已经用到 `SolanaClient` 的基础上,`buildSwap` / `getSwapQuote` 约增加 35KB(minify,gzip 约 13KB),加上 `swap` 发送约 55KB(gzip 约 20KB,含 `/write` 的发送代码)。
288
+
289
+ ## 价格
290
+
291
+ 子路径 `@w3lib/solana-batch-call/price`:代币的美元价格和地址的资产总值。
292
+
293
+ ```ts
294
+ import { NATIVE_MINT } from '@w3lib/solana-batch-call'
295
+ import { getPortfolio, getTokenPrices } from '@w3lib/solana-batch-call/price'
296
+
297
+ await getTokenPrices([NATIVE_MINT, USDC, newToken])
298
+ // [{ token: 'So111…112', native: true, price: 187.4, source: 'defillama' }, …, { token: newToken, price: 0.0012, source: 'onchain' }]
299
+
300
+ const p = await getPortfolio(owner, { tokens: [NATIVE_MINT, USDC, JUP] }) // 指定代币:按 ATA 读余额,免费公共节点也能用
301
+ const all = await getPortfolio(owner, { provider: 'https://my-rpc…', lp: true }) // 全部持仓 + LP 仓位:需要支持 getTokenAccountsByOwner 的节点
302
+ p.totalUsd // 代币 + LP 仓位的合计;p.tokens 按价值排序,p.lp 是 LP 仓位
303
+ ```
304
+
305
+ - 价格源依次使用,前面没有价格的代币交给后面的(`sources`,默认 `[defillamaPrices(), onchainPrices()]`),结果的 `source` 是给出价格的源;原生 SOL(客户端的 `nativeMints`)按 Wrapped SOL 的 mint 查
306
+ - `defillamaPrices({ at })`:历史价格(unix 秒或 `Date`);`minConfidence` 过滤置信度低的价格(默认 0.9)
307
+ - `onchainPrices()`:按 PDA 推导代币对 Wrapped SOL、USDC、USDT 的池子——Raydium CLMM(`["pool", ammConfig, mint0, mint1]`,21 个 AmmConfig)、Orca Whirlpool(`["whirlpool", config, mintA, mintB, 费率档位]`,22 个档位)、Raydium CPMM(同 CLMM 的种子,21 个 AmmConfig),用计价币一侧最深的池子的价格;稳定币按 1 美元,Wrapped SOL 先对稳定币定价。`minLiquidityUsd`(默认 10000)以下的池子不用,防止小池子的价格被操纵;集中流动性的深度取金库余额和当前活跃流动性折算的储备中较小的,流动性都在区间外的池子不会被选中。不需要 `getProgramAccounts`,每个代币约推导 190 个地址,读取分两轮(池子和 mint、金库),每轮都是合并的 `getMultipleAccounts`。2026-10 主网实测 SOL、JUP、BONK、RAY、JTO、WIF 和 DefiLlama 相差在 0.5% 以内
308
+ - Raydium AMM v4、Meteora、PumpSwap 等池子的地址不能只靠两个 mint 推导,`onchainPrices` 不查;这些代币一般 DefiLlama 有价格
309
+ - `combinePrices([…])` 把多个源组合成一个,可以传给 `getLpPositions` 的 `prices`(价格源的参数里有 `client`,链上价格源用它读池子)
310
+ - 资产总值:传 `tokens` 时按 ATA 读这些代币的余额(免费公共节点也能用);不传时列出地址的全部持仓(同 `getOwnerTokens`,需要支持 `getTokenAccountsByOwner` 的节点,不支持时报错并提示传 `tokens`),这时 LP 代币按仓位计入、不再作为代币重复计算。`lp: true` 时加上 LP 仓位(参数同 `getLpPositions`;它不传 `mints` 时也要扫描代币账户,免费公共节点上传 `lp: { mints, pools }`)
311
+
312
+ 价格只用于展示和估值:链上价格是池子当前的价格,可能被单个区块内的交易影响,不要用它算交易的最小数量。
313
+
314
+ 打包:在已经用到 `SolanaClient` 的基础上,`getTokenPrices` 约增加 7KB(minify,gzip 约 3KB),`getPortfolio`(含 LP 仓位)约 22KB(gzip 约 9KB)。
315
+
316
+ ## 手续费估算
317
+
318
+ 子路径 `@w3lib/solana-batch-call/fees`:当前的优先费建议,和一笔交易大概要花多少 SOL。
319
+
320
+ ```ts
321
+ import { getCostEstimate, getFeeEstimate } from '@w3lib/solana-batch-call/fees'
322
+
323
+ await getFeeEstimate({ accounts: [pool] })
324
+ // { slow: '0', normal: '1166', fast: '10000', slots: 150 } 每个计算单元多少 micro-lamports
325
+
326
+ await getCostEstimate({ feePayer: owner, instructions: [ix1, ix2] })
327
+ // { computeUnits: 15318, computeUnitLimit: 17850, priorityFee: '1166', signatures: 1, baseFee: '5000', priorityFeeLamports: '21', fee: '5021',
328
+ // rent: '1488440', createdAccounts: [{ address, owner: 'Tokenkeg…', space: 165, rent: '1488440' }], total: '1493461', totalFormatted: '0.001493461', symbol: 'SOL' }
329
+
330
+ await getCostEstimate({ transaction: base64Tx }) // 构建好的交易(如聚合器 API 返回的),按交易自带的计算预算算
331
+ ```
332
+
333
+ - **费用建议**:`getRecentPrioritizationFees` 返回最近 150 个 slot 里(会写 `accounts` 的)落块交易的**最低**单价,三档取第 50 / 75 / 90 百分位(`percentiles`),`normal` 与 `/write` 的 `priorityFee: 'auto'` 取法相同(不含 `maxPriorityFee` 上限)。不传 `accounts` 时看全网,大多是 0(每个区块总有不加优先费的交易);估某笔交易时传它的可写账户(`estimateCost` 自动这样做)
334
+ - **一笔交易**:模拟一次(`sigVerify: false`、`replaceRecentBlockhash`,不需要签名、不会发送),模拟失败时抛出(带程序日志里的原因)。传指令时按 `/write` 的发送方式算:去掉自带的计算预算指令,计算单元上限 = 模拟消耗 × 1.1 + 1000,单价按这一档(`level`,或 `priorityFee` 直接指定,或传查好的 `fees` 共用);传构建好的交易时按它自带的 SetComputeUnitLimit / SetComputeUnitPrice(没有单价时优先费为 0,没有上限时按每条指令 20 万),v0 交易会读地址查找表找可写账户
335
+ - **手续费** = 每个签名 5000 lamports + 单价 × 上限(向上取整)。**押金**:交易前不存在、模拟后存在、有数据或不归 System Program 的可写账户(新建的代币账户、仓位等),按 `getMinimumBalanceForRentExemption` 算(不写死:2026-10 主网 165 字节是 1,488,440 lamports),关闭账户时退回;普通转账给新地址不算押金
336
+ - 只支持 legacy 和 v0 交易:主网已经有 v1 格式的交易(2026-10 约三成),它们的布局不同,传入时报错(改传指令)
337
+
338
+ 主网核对:Raydium SOL-USDC 池子的三档费用、新建 USDC ATA 的押金(和 `getMinimumBalanceForRentExemption(165)` 一致);从 Jupiter 最近的真实 v0 交易(带地址查找表)算出的手续费和链上实际扣的相同(如 5803 / 5027 lamports)。
339
+
340
+ 打包:在已经用到 `SolanaClient` 的基础上,`getFeeEstimate` 约增加 2KB(minify,gzip 约 1KB),`getCostEstimate` 约 5KB(gzip 约 2KB),另外按需加载 `/write` 的交易构建和错误说明(约 19KB,gzip 约 7KB)。
341
+
342
+ ## 域名解析
343
+
344
+ 子路径 `@w3lib/solana-batch-call/names`:SNS(Solana Name Service,Bonfida)的名字 → 地址、地址 → 主域名。
345
+
346
+ ```ts
347
+ import { getAddressNames, getNameAddresses } from '@w3lib/solana-batch-call/names'
348
+
349
+ await getNameAddresses(['bonfida.sns', 'Toly.SNS', 'pay.alice.sns'])
350
+ // [{ name: 'bonfida.sns', normalized: 'bonfida.sns', address: 'Fw1E…Lb1v', owner: 'Fw1E…Lb1v', source: 'owner', service: 'sns' }, …]
351
+ await getAddressNames([wallet])
352
+ // [{ address: wallet, name: 'couponvault.sns', service: 'sns' }]
353
+ ```
354
+
355
+ - **`.sol` 和 `.sns`**:SNS 的域名 2026-10 起改用 `.sns` 后缀(链上还是同一批账户,`bonfida.sol` 和 `bonfida.sns` 是同一个域名);`.sol` 正在迁到另一个注册服务 SRS,SNS 官方 SDK 从 slot 452,825,395(约 2026-10-15)起不再解析 `.sol`,SRS 的解析还没有公开。所以默认只认 `.sns`,`.sol` 返回 null;`legacySol: true` 时 `.sol` 按同名的 `.sns` 解析(迁移前的含义,以后同一个 `.sol` 名字可能属于别人,自己判断)。反查得到的名字都是 `.sns`
356
+ - **正查**:名字小写、去掉空格,支持 `name.sns` 和一级子域名 `sub.name.sns`。账户都在本地按 PDA 推导(`sha256('SPL Name Service' + 名字)`、class、parent),一次 `getMultipleAccounts` 读域名、NFT 记录和两版 SOL 记录。解析顺序同官方 SDK:域名代币化成了 NFT → NFT 的持有人(`source: 'nft'`);V2 的 SOL 记录(owner 验证过、没有过期,`'record-v2'`);V1 的 SOL 记录(owner 的 Ed25519 签名,用 WebCrypto 验签,`'record-v1'`);否则是域名的 owner(`'owner'`)。`owner` 字段总是域名的 owner(代币化的是 NFT 持有人)
357
+ - **不解析(`address` 为 null)的情况**:没有注册(`source` 也为 null);V2 SOL 记录没有验证 / 格式不对(与官方 SDK 一样不往下解析);owner 是 PDA(程序账户,转过去可能取不出来,`allowPda: true` 时返回);代币化的域名要用 `getTokenLargestAccounts` 找 NFT 持有人,免费公共节点(publicnode、官方节点)都不支持,这时 `source` 为 `'nft'`、`address` 为 null,需要自己的节点
358
+ - **反查**:读地址设置的主域名(favourite domain,`85iDfUvr…` 程序的 PDA),再读域名和反查记录拼出名字(子域名再读一次父域名的反查记录),然后**核对**:名字推导出的账户就是主域名指向的账户,并且域名属于这个地址(owner 是它,或代币化后它的 ATA 里有这个域名的 NFT)。域名转给别人后旧的主域名设置不算。通常两次 `getMultipleAccounts`,免费公共节点能用
359
+
360
+ 主网核对:`bonfida.sns`、`toly.sns` 的 owner(`legacySol` 时 `bonfida.sol`、`toly.sol` 相同),`crypto.sns` 识别为代币化;`Fw1E…Lb1v` 的主域名是 `couponvault.sns`,`bonfida.sns` 的旧 owner 设置的主域名已经失效(返回 null)。
361
+
362
+ 打包:在已经用到 `SolanaClient` 的基础上约增加 6KB(minify,gzip 约 2.5KB)。
363
+
364
+ ## 授权管理
365
+
366
+ 子路径 `@w3lib/solana-batch-call/approvals`:Solana 上的“授权”是代币账户的 delegate(Approve 设置,被授权方可以转走 `delegatedAmount` 以内的代币)。查一个地址的代币账户给出的授权、可以关闭回收租金的空账户,批量撤销 / 关闭。
367
+
368
+ ```ts
369
+ import { closeTokenAccounts, getApprovals, revokeApprovals } from '@w3lib/solana-batch-call/approvals'
370
+
371
+ const list = await getApprovals(owner, { mints: [USDC, BONK, NATIVE_MINT] }) // 指定代币:读 ATA,免费公共节点也能用
372
+ // [{ account, mint: USDC, symbol: 'USDC', amount: '1500000', formatted: '1.5', delegate: 'GYFk…', delegatedAmount: '1000000',
373
+ // revocable: true, closable: false, closeBlocker: 'balance', lamports: '2039280', withheldAmount: '0', permanentDelegate: null, … }, …]
374
+ await getApprovals(owner, { provider: 'https://my-rpc…' }) // 全部代币账户:需要支持 getTokenAccountsByOwner 的节点
375
+
376
+ await revokeApprovals(client, key, list) // 有 delegate 的都撤销
377
+ await closeTokenAccounts(client, key, list.filter((a) => a.amount === '0')) // 关闭空账户,租金退回 owner(destination 可改)
378
+ ```
379
+
380
+ - **结果按代币账户**:默认返回有授权的(`delegate`,或关闭权限 `closeAuthority` 是别人)和余额为 0 的账户;`all: true` 时返回全部。传 `mints` 时读这些代币在 SPL Token 和 Token-2022 下的 ATA(一次 `getMultipleAccounts`,再一次读 mint 和元数据),非 ATA 的代币账户查不到;不传时用 `getTokenAccountsByOwner` 列出全部代币账户,节点不支持时报错并提示传 `mints`
381
+ - **能不能撤销 / 关闭**:`revocable`(有 delegate、没有冻结——冻结的账户不能 Revoke);`closable` / `closeBlocker`:`'balance'`(有余额,Wrapped SOL 除外)、`'close-authority'`(关闭权限是别的地址,owner 关不掉,它可以在余额为 0 时关闭并拿走租金,也只有它能撤销这个权限)、`'confidential-balance'`(Token-2022 机密转账的余额不为 0)。冻结的空账户可以关闭
382
+ - **Wrapped SOL**:`native: true`,`amount` 是包装的 SOL;有余额也能关闭(`lamports` 全部退回,相当于 unwrap),默认只列出空的,`buildCloses` 不会替你过滤有余额的 wSOL 账户
383
+ - **Token-2022**:账户里暂存的转账手续费(`withheldAmount`,不属于 owner)不为 0 时直接关闭会失败,`buildCloses` 先加一条 HarvestWithheldTokensToMint(任何人都能调用,不需要签名)把它收到 mint 上;`permanentDelegate` 是 mint 的永久授权(可以转走 / 销毁任何账户里的这种代币,撤销不了,只做提示)
384
+ - **发送**:`buildRevokes` / `buildCloses` 只构建(每个账户一个调用:Revoke / [Harvest +] CloseAccount);`revokeApprovals` / `closeTokenAccounts` 构建后用 owner 签名发送,钱包和选项同 `batchCall`(多个账户打包进同一笔交易,`feePayer` 可以替 owner 付手续费,`dryRun` 只模拟),默认一个失败不影响后面的;列表里有别人的账户时报错。发送的代码按需加载
385
+
386
+ 主网核对(只模拟,不发送):Star Atlas 游戏代币(AMMO / FOOD / FUEL)上真实的 delegate,Revoke 模拟通过(同一 owner 的两个账户打包进一笔,504 计算单元);清空的 USDC、Wrapped SOL、Token-2022(xStocks 的 SPYx,带 PermanentDelegate)账户 CloseAccount 模拟通过;有余额的账户强行关闭时链上报 “Non-native account can only be closed if its balance is zero”,和 `closeBlocker: 'balance'` 一致;清空了、但还有暂存转账手续费的 Token-2022 账户,Harvest + CloseAccount 模拟通过(约 3700 计算单元),不 Harvest 直接关闭时链上报 “An account can only be closed if its withheld fee balance is zero”;`getTokenAccountsByOwner` 列出的结果和按 ATA 读的一致。
387
+
388
+ 打包:在已经用到 `SolanaClient` 的基础上,`getApprovals` 约增加 6KB(minify,gzip 约 2KB),加上撤销 / 关闭约 7KB(gzip 约 3KB);发送的代码(`/write`,约 21KB,gzip 约 8KB)按需加载,只查授权时不会加载。
389
+
390
+ ## 账户回收
391
+
392
+ 子路径 `@w3lib/solana-batch-call/reclaim`:扫描钱包里关闭后能退回租金(SOL)的账户,给出每项和合计能拿回多少 lamports,批量关闭。
393
+
394
+ ```ts
395
+ import { getReclaimable, reclaim } from '@w3lib/solana-batch-call/reclaim'
396
+
397
+ const r = await getReclaimable(owner, { provider: 'https://my-rpc…' }) // 全部代币账户:需要支持 getTokenAccountsByOwner 的节点;免费节点传 mints 只看指定代币的 ATA
398
+ r.totalLamports // 全部能拿回的 lamports;r.selectedLamports 是建议勾选的
399
+ // r.items: [{ kind: 'empty-token', account, mint, lamports: '2039280', selected: true, reasons: ['empty'] },
400
+ // { kind: 'junk-token', symbol: 'claim-rewards.xyz', amount: '777', lamports: '2039280', selected: false, reasons: ['no-price', 'spam-name'] },
401
+ // { kind: 'wrapped-sol', amount: '5000000', lamports: '7039280', rentLamports: '2039280' },
402
+ // { kind: 'orca-position', position: { protocol: 'orca-whirlpool', positionMint, pool }, lamports: '4433520' }, …]
403
+ // r.blocked: [{ kind: 'empty-token', blocker: 'frozen', … }, { kind: 'raydium-position', blocker: 'owed', … }]
404
+
405
+ const { simulation, results } = await reclaim(client, key, r.items.filter((i) => i.selected), { destination: coldWallet, dryRun: true })
406
+ simulation.lamports // 预计退回的 lamports(模拟里的 SOL 变化,不含手续费)
407
+ await reclaim(client, key, r.items.filter((i) => i.selected)) // 构建 → 模拟 → 签名发送
408
+ ```
409
+
410
+ - **种类**(`kind`):`empty-token`(空的 SPL Token / Token-2022 账户,有暂存转账手续费的先收到 mint 上)、`junk-token`(垃圾币:销毁后关闭)、`wrapped-sol`(关闭即解包,`lamports` 含包装的 SOL,`rentLamports` 只是租金)、`orca-position` / `raydium-position`(流动性、手续费、奖励都为 0 的集中流动性仓位:关闭仓位账户、销毁 NFT;Token-2022 的 NFT 连 mint 一起退回)。`blocked` 里是暂时关不掉的:`frozen`(冻结的账户,保守起见不尝试)、`close-authority`、`confidential-balance`、`paused` / `permissioned-burn`(销毁不了)、`owed`(仓位还有没领的手续费 / 奖励,先用 `/claim` 领取)
411
+ - **垃圾币的判断很保守**:有价格且价值超过 `maxJunkUsd`(默认 0.01 美元)的、NFT(精度 0 余额 1)、仓位 NFT 一律不列。列出的垃圾币只有“有把握”的 `selected: true`:有价格但价值不超过 `maxJunkUsd` 的尘埃(`dust`)、`/risk` 标记为不可转让 / 转账手续费很高 / 已暂停(`risk:*`)。名字可疑(链接 / 域名 / claim / airdrop,`spam-name`)只作提示、不会单独触发勾选(名字谁都能起);没有价格也没有其他依据的只列出(`reasons: ['no-price']` / `['no-price', 'spam-name']`),由应用决定(可用 `isJunk` 勾选)。价格服务出错或 `prices: false` 时不勾选任何垃圾币(`warnings` 里有说明)。`isJunk({ account, price, usd, risk })` 返回 `true` 一定算垃圾币并勾选,`false` 一定排除,`undefined` 用默认判断(NFT 和仓位 NFT 不受它影响);`prices` 可以传自己的价格源(如 `combinePrices([defillamaPrices(), onchainPrices()])`,默认只用 DefiLlama:链上价格每个代币要读约 190 个池子地址)
412
+ - **安全**:`buildReclaim` 发送前重新读取每个账户并核对 owner、mint、余额、冻结、关闭权限、仓位是否仍是空的,变了的不构建、列在 `plan.skipped`(带原因)。不会关闭有余额的账户:空账户余额不为 0 时跳过;垃圾币只销毁和扫描时一致的余额(有人又转入了就跳过);只有 Wrapped SOL 带余额关闭。账户的 owner 必须是签名者,每个账户一个调用(垃圾币:销毁 → 关闭,原子执行),多个账户打包进同一笔交易,`feePayer` 可以替 owner 付手续费
413
+ - **模拟**:`simulateReclaim(client, plan)` 用 `/simulate` 逐笔模拟,`failed` 列出会失败的账户(整笔失败时逐个重新模拟找出来);`lamports` 是预计退回的 SOL:收款地址就是付款人时取模拟里的真实变化(`lamportsSource: 'simulation'`),否则按账户余额估算。`reclaim` 默认先模拟、失败的账户不发送;`dryRun: true` 只构建和模拟
414
+ - **destination**:退回的 SOL 给谁,默认 owner。Raydium CLMM 的仓位账户租金程序固定退给 owner,不能指定(`plan.warnings` 有提示)
415
+ - 限制:只处理上面这些;质押 / 锁仓的仓位、Orca 的仓位组合(Position Bundle)不在 owner 名下;冻结的账户不关闭(`/approvals` 的 `closable` 不检查冻结);价格只用于判断,不保证没有误判,发送前请展示列表给用户确认
416
+
417
+ 主网核对(只模拟,不发送):找到有大量空仓位的真实钱包(Orca SPL NFT、Raydium SPL NFT、Raydium Token-2022 NFT 各有),其中一个 249 个账户(空代币账户、尘埃币销毁 + 关闭、136 个 Raydium 仓位、69 个 Orca 仓位)分 32 笔交易模拟全部通过,模拟的 SOL 变化与预计的 1068283440 lamports 逐位一致;Token-2022 NFT 的仓位关闭时 mint 一起退回(每个约 0.0042 SOL),预计值据此修正。
418
+
419
+ ## 批量领取
420
+
421
+ 子路径 `@w3lib/solana-batch-call/claim`:Orca Whirlpool、Raydium CLMM 仓位的未领取手续费和挖矿奖励(Orca 的奖励 slot、Raydium 的农场奖励)。先只读预览,再把全部(或选中的)仓位的领取打包成交易。指令来自 `/liquidity` 的 `collectFees`(手续费 + 全部奖励,SOL 自动解包,缺少的代币账户自动创建),这里只做批量和打包。
422
+
423
+ ```ts
424
+ import { claimAll, getClaimable } from '@w3lib/solana-batch-call/claim'
425
+
426
+ const c = await getClaimable(owner, { provider: 'https://my-rpc…' }) // 只读预览,不发交易;免费节点传 mints(仓位 NFT)只看指定仓位
427
+ c.totalUsd // 手续费 + 奖励的美元价值;c.tokens 按代币汇总;c.incomplete 为 true 时有仓位的数量读不到(tick 数据),该代币行带 incomplete: true,数量偏低、usd 为 null
428
+ // c.positions: [{ protocol: 'raydium-clmm', positionMint, pool, token0, token1, inRange, totalUsd: 11.54,
429
+ // fees: [{ symbol: 'SOL', formatted: '0.0606', usd: 6.45 }, { symbol: 'USDC', formatted: '5.011', usd: 5.01 }],
430
+ // rewards: [{ index: 0, mint: RAY, symbol: 'RAY', formatted: '0.042698', usd: 0.083 }] }]
431
+
432
+ const plan = await buildClaimAll(client, owner, c.positions) // 只构建:每个仓位一个调用
433
+ const sim = await simulateClaim(client, plan) // 模拟:sim.received 是收到的各代币合计(usd 是合计数量的美元价值),sim.failed 是会失败的仓位
434
+ await claimAll(client, key, c.positions, { minUsd: 0.5 }) // 构建 → 模拟 → 签名发送(positions 不传时领取全部);dryRun: true 只模拟
435
+ ```
436
+
437
+ - **奖励的数量照搬程序结算**:池子的奖励增长按时间摊给当前流动性(Δt × 每秒排放 / 流动性,Raydium 到 `end_time` 为止),再按仓位区间内的增长(全局 − 下方 − 上方,u128 回绕)计算,加上已记账的。`getLpPositions(owner, { rewards: true })` 也可以直接拿到每个仓位的 `rewards`(`/lp` 的新选项,`now` 指定结算时刻);读不到两端的 tick 数据时数量为 `null`
438
+ - 没有可领的东西(手续费和奖励都是 0)的仓位不列;流动性为 0 但还有没领的手续费 / 奖励的仓位照常列出。领完的空仓位可以用 `/reclaim` 关闭回收租金
439
+ - `claimAll` 默认先模拟,失败的仓位不发送(`results[i].status: 'failed'`),一个仓位失败不影响其他的;钱包和选项同 `batchCall`(`feePayer`、`priorityFee` 等)。新建代币账户(如还没有的奖励代币的账户)的租金由 owner 付,owner 没有 SOL 时模拟会报 createIdempotent 失败
440
+ - 限制:手续费和奖励都领到 owner 自己的代币账户(不能指定收款地址);带转账手续费的 Token-2022 池子不支持(构建失败,列在 `plan.failed`);预览是读取时刻的数量,发送时会继续累积
441
+
442
+ 主网核对(只模拟,不发送):Raydium CLMM SOL-USDC 池(奖励 RAY)里真实仓位的预览与模拟时程序实际转出的数量一致(手续费相差几秒的累积,RAY 奖励 0.042698 逐位一致)。Orca 的奖励结算只有单元测试(主网上没有找到有奖励排放的 Orca 仓位),公式与 Raydium 相同;Orca 的手续费沿用 `/lp` 已核对的结果。
443
+
444
+ 打包(minify,含 `SolanaClient`,发送和模拟的代码按需加载):`/reclaim` 约 124KB,`/claim` 约 143KB(含 `/liquidity` 的指令构建);主入口不变。
445
+
446
+ ## 代币风险
447
+
448
+ 子路径 `@w3lib/solana-batch-call/risk`:买入前检查代币的权限和 Token-2022 扩展里会影响持有人的设置。
449
+
450
+ ```ts
451
+ import { getTokenRisks, getTradeChecks } from '@w3lib/solana-batch-call/risk'
452
+
453
+ const [risk] = await getTokenRisks([PYUSD])
454
+ // { program: 'token-2022', decimals: 6, supply: '…', mintAuthority: '22mK…', freezeAuthority: '2apB…',
455
+ // transferFee: { basisPoints: 0, maximumFee: '0', authority: '…', next: null }, permanentDelegate: '2apB…',
456
+ // transferHook: { program: null, authority: '…' }, confidentialTransfer: { authority, autoApprove }, mintCloseAuthority: '…',
457
+ // flags: ['mintable', 'freezable', 'fee-changeable', 'permanent-delegate', 'transfer-hook', 'confidential-transfer', 'closable'] }
458
+
459
+ const [trade] = await getTradeChecks([mint], { owner: myAddress, amountFormatted: '0.05' })
460
+ // { stage: 'ok', honeypot: false, roundTripLoss: 0.0002, bought: '20828496', sold: '198960224', buyRoute: [{ protocol: 'orca-whirlpool', pool }] }
461
+ ```
462
+
463
+ `tokenRisks` / `getTokenRisks`:mint 和 Clock(选转账手续费的新旧配置)用一次 `getMultipleAccounts` 读完,免费公共节点也能用。标记(`flags`):
464
+
465
+ | 标记 | 含义 |
466
+ | --- | --- |
467
+ | `mintable` / `freezable` | 增发权限、冻结权限还在(可以继续增发;可以冻结任何持有人的代币账户) |
468
+ | `transfer-fee` / `high-transfer-fee` | Token-2022 转账手续费 > 0 / > 10%(当前或已经设好、即将生效的费率) |
469
+ | `fee-changeable` | 转账手续费的配置权限还在(费率可以改,两个 epoch 后生效) |
470
+ | `permanent-delegate` | 永久授权:这个地址可以转走 / 销毁任何持有人的代币 |
471
+ | `transfer-hook` | 每次转账都调用一个程序(可以拒绝转账;程序为空但权限还在时也算,随时可以设上) |
472
+ | `non-transferable` / `default-frozen` | 不可转让;新建的代币账户默认冻结 |
473
+ | `pausable` / `paused` | 发行方可以暂停全部转账;当前已暂停 |
474
+ | `confidential-transfer` | 支持保密转账(余额和转账数量加密) |
475
+ | `closable` | mint 关闭权限(总量为 0 时可以关闭 mint) |
476
+ | `ui-amount-changes` | 显示数量会随利率 / 系数变化,且有权限可以改(InterestBearing、ScaledUiAmount) |
477
+ | `permissioned-burn` | 销毁要指定地址批准 |
478
+ | `concentrated` | `concentration: true` 时:最大的 10 个代币账户超过总量的 50%(含池子金库、交易所地址,只作参考)。用 `getTokenLargestAccounts`,免费公共节点大多不支持,不支持时 `topHolders` 为 null |
479
+ | `not-found` | mint 不存在或不是代币 mint |
480
+
481
+ `tradeChecks` / `getTradeChecks`:用 `/swap` 构建“SOL 买入 → 全部卖回 SOL”(卖出走买入的同一条路线),放进一笔交易在主网模拟(不验签名、不上链),看哪一步失败、来回损失多少:
482
+
483
+ - `owner`:模拟用的钱包,要有足够的 SOL(可以是任何有 SOL 的系统账户,如自己的地址);`amount` / `amountFormatted`:买入花的 SOL,默认 0.05
484
+ - `stage`:`ok`、`no-pool`(内置协议里找不到路线)、`buy-failed`(买不进,标记 `cannot-buy`)、`sell-failed`(买得进、卖不出,`honeypot: true`,标记 `honeypot`)
485
+ - `roundTripLoss`:1 − 卖回的 SOL / 花的 SOL(含两次池子手续费、价格影响和转账手续费),超过 10% 标记 `high-round-trip-loss`
486
+ - 先找直接兑换的路线(买入和卖出更容易放进一笔交易),没有时再找两跳;放不下一笔交易时 `stage` 为 null,`error` 说明原因
487
+ - 2026-10-20 主网:USDC 来回损失 0.02%、JUP 0.10%,BERN(转账手续费 2.69%)能买能卖
488
+
489
+ 打包:在已经用到 `SolanaClient` 的基础上,`getTokenRisks` 约增加 5KB(minify,gzip 约 1.5KB);`tradeChecks` 用到 `/swap`,一起约 43KB(gzip 约 16KB)。
490
+
491
+ ## 交易模拟
492
+
493
+ 子路径 `@w3lib/solana-batch-call/simulate`:发送前在主网模拟一笔交易(`simulateTransaction`,不验签名、换最新 blockhash,不需要私钥、不会上链),给出余额变化、授权变化、指令说明、程序错误、手续费和警告。
494
+
495
+ ```ts
496
+ import { getSwapQuote } from '@w3lib/solana-batch-call/swap'
497
+ import { getSimulation } from '@w3lib/solana-batch-call/simulate'
498
+
499
+ const plan = await getSwapQuote(owner, { tokenIn: NATIVE_MINT, tokenOut: USDC, amountInFormatted: '0.1' })
500
+ const sim = await getSimulation(plan, { prices: true }) // 也可以是 base64 的交易:getSimulation(base64Tx, { owner })
501
+ // { success: true, gasUsed: '112394', fee: '5000',
502
+ // balanceChanges: [{ address: owner, asset: 'native', symbol: 'SOL', delta: '-100000000', formatted: '-0.1', usd: -10.68 }, { asset: USDC, symbol: 'USDC', delta: '10677005', … }],
503
+ // approvalChanges: [], decoded: { type: 'swap', protocol: 'Orca', summary: 'Swap 0.1 SOL for 10.677005 USDC on Orca', instructions: [...] }, warnings: [], logs, signers, version: 'legacy' }
504
+ if (!sim.success || sim.warnings.some((w) => w.severity === 'danger')) throw new Error(sim.error ?? sim.warnings[0].message)
505
+ ```
506
+
507
+ - **输入**:base64 / 字节的交易(legacy 和 v0,v0 的地址查找表自动读取;v1 格式报错);本库构建器的输出:`buildSwap` / `buildAddLiquidity` 等的计划(多步合并成一笔,放不下时用 `step` 选一步)、`{ owner, instructions }`(如 `/write` 的指令工具)、`buildRevokes` / `buildCloses` 的结果
508
+ - **余额变化**:站在 `owner`(默认第一个签名者)的角度;SOL 不含手续费(手续费在 `fee`),Wrapped SOL 账户的变化并入 SOL(`asset: 'native'`),代币按模拟前后的代币账户算;`watch` 可以加看别的地址;`prices: true` 加美元价值
509
+ - **授权变化**(`approvalChanges`):按模拟前后的账户状态比较,CPI 里发生的也看得到:`delegate-set` / `delegate-changed` / `delegate-revoked`(带额度,接近 u64 上限为 `unlimited`)、`authority-changed`(账户 owner、关闭权限、mint 的增发 / 冻结权限被改)、`account-closed`。模拟失败时只能按指令判断
510
+ - **指令解码**:System、SPL Token / Token-2022、关联代币账户、计算预算、Memo、升级程序加载器,Jupiter、Raydium(AMM v4 / CLMM / CPMM)、Orca、Meteora、Pump.fun 等程序的协议和指令名(Anchor 程序按 discriminator),`decoded.type` 为 `transfer | swap | approve | liquidity | mint | contract | unknown`;程序不认识时,按钱包“一个资产减少、另一个增加”推断为 `swap`(`decoded.inferred: true`)
511
+ - **错误**:`error` 带失败的指令、程序名和错误名(Anchor 日志里的错误名优先;System、Token、Jupiter v6 等按错误码查表),如 `instruction 2 (SPL Token transferChecked) failed: InsufficientFunds (0x1)`
512
+ - **警告**(`{ code, severity: 'info' | 'warn' | 'danger', message, address? }`):`unlimited-delegate`(danger)/ `delegate-approved`、`authority-changed`(账户 owner / 关闭权限为 danger)、`assign-owner`、`program-upgrade`、`close-to-other`、`unknown-program`、`new-recipient`(收款地址从没有过 SOL / 没有这个代币的账户)、`unexpected-signer`(交易还需要别人签名,`signers` 可以列出预期的)、`high-fee`、`drains-balance`、`simulation-failed`;`risk: true` 时按 `/risk` 检查变化的代币(懒加载),永久授权、转账 hook、高转账手续费等加进 `warnings`,`risks` 里是全部标记
513
+ - 公共节点:publicnode 的 `simulateTransaction` 一次最多返回 10 个账户,先读前状态只留和钱包有关的账户(钱包、新建的账户、钱包的代币账户、mint),多于上限时自动分批模拟
514
+
515
+ 主网核对(publicnode):热钱包转 SOL / USDC(新地址警告、自动建 ATA)、余额不够的错误说明、`buildSwap` 的兑换(SOL → USDC,Orca,美元价值)、无限额度 Approve、Jupiter 最近的一笔真实 v0 交易(地址查找表,余额变化和协议都读对)。
516
+
517
+ 打包:在已经用到 `SolanaClient` 的基础上,`/simulate` 约增加 45KB(minify,gzip 约 16KB;价格、风险按需加载)。
518
+
519
+ ## 交易历史
520
+
521
+ 子路径 `@w3lib/solana-batch-call/history`:地址的交易整理成统一的 `Activity`,数据源可换,游标是最后一条签名。
522
+
523
+ ```ts
524
+ import { getHistory, heliusHistory, rpcHistory } from '@w3lib/solana-batch-call/history'
525
+
526
+ const page = await getHistory(address, { limit: 20 })
527
+ // { items: [{ hash, time, block, status: 'success', type: 'swap', label: 'Swap 2.57 SOL for 274.47 USDC on Jupiter', protocol: 'Jupiter',
528
+ // changes: [{ asset: 'native', symbol: 'SOL', delta: '-2569401878', formatted: '-2.569401878' }, { asset: USDC, … }], fee: '5000' }, …],
529
+ // nextCursor: '4jNtY56m…' }
530
+ const next = await getHistory(address, { cursor: page.nextCursor, types: ['swap', 'transfer'] })
531
+ await getHistory(address, { source: [heliusHistory({ apiKey }), rpcHistory()] }) // Helius 失败时用节点
532
+ ```
533
+
534
+ - `Activity`:`type` 为 `transfer | swap | approve | liquidity | mint | contract | other`,`direction`(`in | out | self`,转账等只有一个方向时)、`counterparty`、`label`、`changes`(这个地址的资产变化,SOL 不含手续费、Wrapped SOL 并入 SOL,symbol 自动补全)、`fee`(这个地址付的手续费)、`protocol`;失败的交易 `status: 'failed'`、没有资产变化、只有手续费
535
+ - 选项:`limit`(默认 20,最多 100)、`cursor`、`before` / `after`(区块时间,unix 秒,按签名里的 blockTime 先过滤、不多取详情)、`types`(不够 `limit` 时往后多找,最多看 `limit × 5` 笔,`nextCursor` 接着往后)、`source`、`symbols: false`(不补全代币符号)
536
+ - **默认数据源 `rpcHistory()`**:`getSignaturesForAddress`(`before` 翻页)+ `getTransaction`(逐笔,`concurrency` 默认 6,限频由节点层退避重试和换节点)。类型按指令(认得的协议 / 转账 / 授权 / 铸造)加余额变化判断:流动性 > 兑换 > 授权 > 铸造;只有转账指令是转账;有不认得的程序时一减一增推断为兑换,否则是 `contract`
537
+ - **`heliusHistory({ apiKey })`**:Helius 增强交易(`/v0/addresses/{address}/transactions`),按 `accountData` 的余额变化整理成同样的 `Activity`;apiKey 由应用传入。传多个数据源时按顺序尝试,前一个失败换下一个,游标通用
538
+ - 主网已经有 v1 格式的交易(2026-10 约三成),`getTransaction` 要传 `maxSupportedTransactionVersion: 1`(只传 0 时遇到它们整个请求报 -32015),本库已处理
539
+ - 限制:免费节点不支持按时间 / 区块直接定位,`before` / `after` 只能从最新往后翻到目标;每页的请求数 ≈ `limit + 1`;Helius 数据源按文档的返回格式写,单元测试用样例数据,没有用真实 key 核对
540
+
541
+ 主网核对:热钱包翻两页(游标衔接、没有重复)、USDC / SOL 转账的方向和对手方、从 Raydium CLMM 池子最近的一笔交易里找到交易者并按 `types: ['swap']` 过滤、`after` 时间范围。
542
+
543
+ 打包:在已经用到 `SolanaClient` 的基础上,`/history` 约增加 21KB(minify,gzip 约 8KB),`heliusHistory` 约 3KB。
544
+
209
545
  ## 余额的三种模式
210
546
 
211
547
  | | 指定 mints(默认,`accounts: 'ata'`) | 指定 mints + `accounts: 'all'` | 扫描(不传 mints,或 `scan: true`) |
@@ -434,7 +770,7 @@ await batchCall(client, [{ from: signer, instructions }])
434
770
 
435
771
  **共同选项**(`SendOptions`):`feePayer`、`priorityFee` / `maxPriorityFee`、`wait`(默认 true)、`timeout`(默认 120 秒)、`pollInterval`(默认 2 秒)、`retries`(默认 2)、`commitment`(`'confirmed'` / `'finalized'`)、`concurrency`(不同手续费支付者并发数,默认 8)、`dryRun`、`signal`、`onUpdate`。
436
772
 
437
- **指令工具**:`systemTransfer`、`transferChecked`、`createAssociatedTokenAccountIdempotent`、`closeAccount`、`setComputeUnitLimit`、`setComputeUnitPrice`,生成本库格式的指令,可直接放进 `batchCall`。
773
+ **指令工具**:`systemTransfer`、`transferChecked`、`createAssociatedTokenAccountIdempotent`、`closeAccount`、`revoke`、`harvestWithheldTokensToMint`、`setComputeUnitLimit`、`setComputeUnitPrice`,生成本库格式的指令,可直接放进 `batchCall`。
438
774
 
439
775
  **体积**(esbuild 打包、minify,对比只用 `SolanaClient`):引入 `batchTransfer` 增加约 22KB(gzip 约 8KB),三个函数都引入增加约 28KB(gzip 约 10KB),含浏览器钱包适配;没有新增依赖。
440
776
 
@@ -466,6 +802,10 @@ pnpm test # 单元测试(模拟节点,不需要网络)
466
802
  pnpm test:live # 连真实节点(SOLANA_RPC 指定支持索引方法的节点;需要代理时加 NODE_USE_ENV_PROXY=1)
467
803
  pnpm test:live:lp # LP 仓位连主网核对
468
804
  pnpm test:live:liquidity # 加减流动性在主网上模拟(simulateTransaction,不发送)
805
+ pnpm test:live:swap # 兑换:本地报价和主网模拟逐位核对(RECORD=1 时更新 test/fixtures/swap-mainnet.json.gz)
806
+ pnpm test:live:risk # 代币风险和买入 / 卖出模拟连主网
807
+ pnpm test:live:simulate # 交易模拟连主网(自己构建的交易、Jupiter 的 v0 交易)
808
+ pnpm test:live:history # 交易历史连主网(翻页、类型过滤、时间范围)
469
809
  pnpm demo # 本地运行 demo(demo/,直接用 src 的源码)
470
810
  pnpm build
471
811
  ```
@@ -0,0 +1,129 @@
1
+ import type { RequestOptions, SolanaClient } from './client.js';
2
+ import type { BatchCallOptions } from './write/batch.js';
3
+ import type { WriteResult } from './write/send.js';
4
+ import type { SignerInput } from './write/signer.js';
5
+ import type { Instruction } from './write/transaction.js';
6
+ export interface ApprovalsQuery extends RequestOptions {
7
+ /**
8
+ * 要检查的代币 mint:读这些代币的关联代币账户(ATA,SPL Token 和 Token-2022 都看),免费公共节点也能用;非 ATA 的代币账户查不到。
9
+ * 不传时列出地址的全部代币账户(getTokenAccountsByOwner),需要支持它的节点
10
+ */
11
+ mints?: readonly string[];
12
+ /** 返回全部代币账户(含没有授权、有余额的),默认 false:只返回有授权(delegate / 别人的关闭权限)的账户和余额为 0 的账户 */
13
+ all?: boolean;
14
+ }
15
+ /** 关不掉的原因:balance 有余额(Wrapped SOL 除外);close-authority 关闭权限是别的地址;confidential-balance Token-2022 机密余额不为 0 */
16
+ export type CloseBlocker = 'balance' | 'close-authority' | 'confidential-balance';
17
+ /** 一个代币账户的授权状态,字段都是字符串 / 数字 / 布尔 / null,可以直接 JSON.stringify */
18
+ export interface TokenAccountApproval {
19
+ /** 代币账户地址 */
20
+ account: string;
21
+ mint: string;
22
+ owner: string;
23
+ tokenProgram: string;
24
+ /** 是 owner 的关联代币账户(ATA) */
25
+ ata: boolean;
26
+ /** Wrapped SOL 账户:amount 是包装的 SOL,关闭时连同租金一起退回(相当于 unwrap) */
27
+ native: boolean;
28
+ symbol: string | null;
29
+ /** 精度;mint 读不到时为 null(formatted 也为 null) */
30
+ decimals: number | null;
31
+ amount: string;
32
+ formatted: string | null;
33
+ /** 被授权方(delegate);没有授权时为 null */
34
+ delegate: string | null;
35
+ /** 被授权方还能转走的数量(最小单位) */
36
+ delegatedAmount: string;
37
+ delegatedFormatted: string | null;
38
+ /** 关闭权限:设置了且不是 owner 时,owner 自己关不掉,余额为 0 时它可以关闭账户并拿走租金(只有它自己能撤销) */
39
+ closeAuthority: string | null;
40
+ /** mint 的永久授权(Token-2022 PermanentDelegate 扩展):可以转走 / 销毁任何账户里的这种代币,撤销不了 */
41
+ permanentDelegate: string | null;
42
+ /** 账户被冻结:不能 Revoke(关闭不受影响) */
43
+ frozen: boolean;
44
+ /** 账户的 lamports:关闭时全部退回 destination(Wrapped SOL 含包装的 SOL) */
45
+ lamports: string;
46
+ /** Token-2022 账户里暂存的转账手续费(不属于 owner):不为 0 时要先收到 mint 上才能关闭,buildCloses 自动加上这一步 */
47
+ withheldAmount: string;
48
+ /** 能撤销授权(有 delegate、没有冻结) */
49
+ revocable: boolean;
50
+ /** 能关闭;不能时 closeBlocker 是原因 */
51
+ closable: boolean;
52
+ closeBlocker: CloseBlocker | null;
53
+ }
54
+ /** 代币账户的完整布局(SPL Token / Token-2022 前 165 字节相同)及关闭相关的 Token-2022 扩展 */
55
+ interface ParsedAccount {
56
+ mint: string;
57
+ owner: string;
58
+ amount: bigint;
59
+ delegate: string | null;
60
+ frozen: boolean;
61
+ native: boolean;
62
+ delegatedAmount: bigint;
63
+ closeAuthority: string | null;
64
+ withheldAmount: bigint;
65
+ confidentialBalance: boolean;
66
+ }
67
+ /** 解析代币账户;不是已初始化的代币账户时为 null */
68
+ export declare function parseTokenAccountState(data: Uint8Array): ParsedAccount | null;
69
+ /**
70
+ * 查一个地址的代币账户给出的授权,和可以关闭回收租金的空账户(结果按账户,同一账户的授权和关闭状态在一起)。
71
+ * 传 mints 时读这些代币的 ATA(一次 getMultipleAccounts,免费公共节点也能用);不传时列出全部代币账户(需要支持 getTokenAccountsByOwner 的节点)
72
+ *
73
+ * ```ts
74
+ * await approvals(client, owner, { mints: [USDC, BONK] })
75
+ * // [{ account: '…', mint: USDC, delegate: 'JUP…', delegatedAmount: '1000000', revocable: true, closable: false, closeBlocker: 'balance', … }]
76
+ * await approvals(new SolanaClient('https://my-rpc…'), owner) // 全部代币账户
77
+ * ```
78
+ */
79
+ export declare function approvals(client: SolanaClient, owner: string, query?: ApprovalsQuery): Promise<TokenAccountApproval[]>;
80
+ /** 一个账户要执行的指令(作为 batchCall 的一个调用,原子执行) */
81
+ export interface ApprovalCall {
82
+ /** revoke:撤销 delegate;close:关闭账户(有暂存的转账手续费时先收到 mint 上) */
83
+ kind: 'revoke' | 'close';
84
+ account: string;
85
+ /** 要签名的地址(代币账户的 owner) */
86
+ owner: string;
87
+ instructions: Instruction[];
88
+ approval: TokenAccountApproval;
89
+ }
90
+ /** 撤销授权的指令:每个有 delegate、没有冻结的账户一条 Revoke;其他的跳过 */
91
+ export declare function buildRevokes(list: readonly TokenAccountApproval[]): ApprovalCall[];
92
+ export interface BuildClosesOptions {
93
+ /** 租金(和 Wrapped SOL 包装的 SOL)退给谁,默认账户的 owner */
94
+ destination?: string;
95
+ }
96
+ /**
97
+ * 关闭账户的指令:每个 closable 的账户一条 CloseAccount(关闭同时去掉授权);Token-2022 账户里有暂存的转账手续费时,
98
+ * 先加一条 HarvestWithheldTokensToMint(不需要签名)。关不掉的跳过。
99
+ * 注意 Wrapped SOL 账户有余额时也能关闭(余额作为 SOL 退回 destination):只想回收空账户时先按 amount 过滤
100
+ */
101
+ export declare function buildCloses(list: readonly TokenAccountApproval[], options?: BuildClosesOptions): ApprovalCall[];
102
+ export interface ApprovalResult extends WriteResult {
103
+ kind: ApprovalCall['kind'];
104
+ account: string;
105
+ approval: TokenAccountApproval;
106
+ }
107
+ export interface CloseAccountsOptions extends BatchCallOptions, BuildClosesOptions {
108
+ }
109
+ /**
110
+ * 撤销授权:构建(同 buildRevokes)后用 owner 签名发送,钱包和选项同 batchCall(多个账户打包进同一笔交易,feePayer 可以替 owner 付手续费)。
111
+ * 默认一个失败不影响后面的(continueOnError: true);结果与构建出的调用一一对应
112
+ *
113
+ * ```ts
114
+ * const list = await approvals(client, owner, { mints })
115
+ * await revokeApprovals(client, key, list)
116
+ * ```
117
+ */
118
+ export declare function revokeApprovals(client: SolanaClient, from: SignerInput, list: readonly TokenAccountApproval[], options?: BatchCallOptions): Promise<ApprovalResult[]>;
119
+ /**
120
+ * 关闭代币账户回收租金(同 buildCloses),发送同 revokeApprovals。
121
+ *
122
+ * ```ts
123
+ * const empty = (await approvals(client, owner, { mints })).filter((a) => a.amount === '0')
124
+ * await closeTokenAccounts(client, key, empty) // 租金退回 owner
125
+ * ```
126
+ */
127
+ export declare function closeTokenAccounts(client: SolanaClient, from: SignerInput, list: readonly TokenAccountApproval[], options?: CloseAccountsOptions): Promise<ApprovalResult[]>;
128
+ export {};
129
+ //# sourceMappingURL=approvals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approvals.d.ts","sourceRoot":"","sources":["../src/approvals.ts"],"names":[],"mappings":"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"}