@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
@@ -0,0 +1,358 @@
1
+ /**
2
+ * 授权管理(子路径 `@w3lib/solana-batch-call/approvals`):Solana 上的“授权”是代币账户的 delegate(Approve 设置,可以转走 delegatedAmount 以内的代币)。
3
+ * 批量查一个地址的代币账户给出的授权、可以关闭回收租金的空账户;批量撤销(Revoke)、关闭(CloseAccount,租金退回)
4
+ */
5
+ import { base64 } from '@scure/base'
6
+
7
+ import { encodeAddress, getAssociatedTokenAddress, getMetadataAddress, isAddress } from './address.js'
8
+ import type { RequestOptions, SolanaClient } from './client.js'
9
+ import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from './constants.js'
10
+ import { findExtension, formatUnits, parseMetaplexMetadata, parseMint, type AccountInfo } from './layout.js'
11
+ import { methodUnavailable } from './lp.js'
12
+ import { closeAccount, harvestWithheldTokensToMint, revoke } from './write/instructions.js'
13
+ import type { BatchCallOptions } from './write/batch.js'
14
+ import type { WriteResult } from './write/send.js'
15
+ import type { SignerInput } from './write/signer.js'
16
+ import type { Instruction } from './write/transaction.js'
17
+
18
+ export interface ApprovalsQuery extends RequestOptions {
19
+ /**
20
+ * 要检查的代币 mint:读这些代币的关联代币账户(ATA,SPL Token 和 Token-2022 都看),免费公共节点也能用;非 ATA 的代币账户查不到。
21
+ * 不传时列出地址的全部代币账户(getTokenAccountsByOwner),需要支持它的节点
22
+ */
23
+ mints?: readonly string[]
24
+ /** 返回全部代币账户(含没有授权、有余额的),默认 false:只返回有授权(delegate / 别人的关闭权限)的账户和余额为 0 的账户 */
25
+ all?: boolean
26
+ }
27
+
28
+ /** 关不掉的原因:balance 有余额(Wrapped SOL 除外);close-authority 关闭权限是别的地址;confidential-balance Token-2022 机密余额不为 0 */
29
+ export type CloseBlocker = 'balance' | 'close-authority' | 'confidential-balance'
30
+
31
+ /** 一个代币账户的授权状态,字段都是字符串 / 数字 / 布尔 / null,可以直接 JSON.stringify */
32
+ export interface TokenAccountApproval {
33
+ /** 代币账户地址 */
34
+ account: string
35
+ mint: string
36
+ owner: string
37
+ tokenProgram: string
38
+ /** 是 owner 的关联代币账户(ATA) */
39
+ ata: boolean
40
+ /** Wrapped SOL 账户:amount 是包装的 SOL,关闭时连同租金一起退回(相当于 unwrap) */
41
+ native: boolean
42
+ symbol: string | null
43
+ /** 精度;mint 读不到时为 null(formatted 也为 null) */
44
+ decimals: number | null
45
+ amount: string
46
+ formatted: string | null
47
+ /** 被授权方(delegate);没有授权时为 null */
48
+ delegate: string | null
49
+ /** 被授权方还能转走的数量(最小单位) */
50
+ delegatedAmount: string
51
+ delegatedFormatted: string | null
52
+ /** 关闭权限:设置了且不是 owner 时,owner 自己关不掉,余额为 0 时它可以关闭账户并拿走租金(只有它自己能撤销) */
53
+ closeAuthority: string | null
54
+ /** mint 的永久授权(Token-2022 PermanentDelegate 扩展):可以转走 / 销毁任何账户里的这种代币,撤销不了 */
55
+ permanentDelegate: string | null
56
+ /** 账户被冻结:不能 Revoke(关闭不受影响) */
57
+ frozen: boolean
58
+ /** 账户的 lamports:关闭时全部退回 destination(Wrapped SOL 含包装的 SOL) */
59
+ lamports: string
60
+ /** Token-2022 账户里暂存的转账手续费(不属于 owner):不为 0 时要先收到 mint 上才能关闭,buildCloses 自动加上这一步 */
61
+ withheldAmount: string
62
+ /** 能撤销授权(有 delegate、没有冻结) */
63
+ revocable: boolean
64
+ /** 能关闭;不能时 closeBlocker 是原因 */
65
+ closable: boolean
66
+ closeBlocker: CloseBlocker | null
67
+ }
68
+
69
+ const ACCOUNT_SIZE = 165
70
+ const ACCOUNT_TYPE_ACCOUNT = 2
71
+ const EXT_TRANSFER_FEE_AMOUNT = 2
72
+ const EXT_CONFIDENTIAL_TRANSFER_ACCOUNT = 5
73
+ const EXT_PERMANENT_DELEGATE = 12
74
+ const EXT_CONFIDENTIAL_TRANSFER_FEE_AMOUNT = 17
75
+ const ZERO_PUBKEY = '11111111111111111111111111111111'
76
+
77
+ /** 代币账户的完整布局(SPL Token / Token-2022 前 165 字节相同)及关闭相关的 Token-2022 扩展 */
78
+ interface ParsedAccount {
79
+ mint: string
80
+ owner: string
81
+ amount: bigint
82
+ delegate: string | null
83
+ frozen: boolean
84
+ native: boolean
85
+ delegatedAmount: bigint
86
+ closeAuthority: string | null
87
+ withheldAmount: bigint
88
+ confidentialBalance: boolean
89
+ }
90
+
91
+ /** 解析代币账户;不是已初始化的代币账户时为 null */
92
+ export function parseTokenAccountState(data: Uint8Array): ParsedAccount | null {
93
+ if (data.length !== ACCOUNT_SIZE && !(data.length > ACCOUNT_SIZE && data[ACCOUNT_SIZE] === ACCOUNT_TYPE_ACCOUNT)) {
94
+ return null
95
+ }
96
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength)
97
+ const pubkey = (offset: number) => encodeAddress(data.slice(offset, offset + 32))
98
+ // COption:u32 标记(1 有、0 没有)+ 值
99
+ const option = (offset: number) => view.getUint32(offset, true) === 1
100
+ const state = data[108] as number
101
+ if (state === 0) {
102
+ return null // 未初始化
103
+ }
104
+ const withheld = findExtension(data, EXT_TRANSFER_FEE_AMOUNT)
105
+ // 机密转账:待入账(lo / hi)和可用余额的密文全为 0 才能关闭;手续费扩展同理(Token-2022 的 closable 检查)
106
+ const confidential = findExtension(data, EXT_CONFIDENTIAL_TRANSFER_ACCOUNT)
107
+ const confidentialFee = findExtension(data, EXT_CONFIDENTIAL_TRANSFER_FEE_AMOUNT)
108
+ const nonZero = (bytes: Uint8Array | null, from: number, to: number) => !!bytes && bytes.slice(from, to).some((b) => b !== 0)
109
+ return {
110
+ mint: pubkey(0),
111
+ owner: pubkey(32),
112
+ amount: view.getBigUint64(64, true),
113
+ delegate: option(72) ? pubkey(76) : null,
114
+ frozen: state === 2,
115
+ native: option(109),
116
+ delegatedAmount: view.getBigUint64(121, true),
117
+ closeAuthority: option(129) ? pubkey(133) : null,
118
+ withheldAmount: withheld && withheld.length >= 8 ? new DataView(withheld.buffer, withheld.byteOffset).getBigUint64(0, true) : 0n,
119
+ confidentialBalance: nonZero(confidential, 33, 225) || nonZero(confidentialFee, 0, 64),
120
+ }
121
+ }
122
+
123
+ interface RawTokenAccount {
124
+ pubkey: string
125
+ account: { data: [string, string]; owner: string; lamports: number | string }
126
+ }
127
+
128
+ interface Found {
129
+ address: string
130
+ program: string
131
+ lamports: bigint
132
+ parsed: ParsedAccount
133
+ }
134
+
135
+ /**
136
+ * 查一个地址的代币账户给出的授权,和可以关闭回收租金的空账户(结果按账户,同一账户的授权和关闭状态在一起)。
137
+ * 传 mints 时读这些代币的 ATA(一次 getMultipleAccounts,免费公共节点也能用);不传时列出全部代币账户(需要支持 getTokenAccountsByOwner 的节点)
138
+ *
139
+ * ```ts
140
+ * await approvals(client, owner, { mints: [USDC, BONK] })
141
+ * // [{ account: '…', mint: USDC, delegate: 'JUP…', delegatedAmount: '1000000', revocable: true, closable: false, closeBlocker: 'balance', … }]
142
+ * await approvals(new SolanaClient('https://my-rpc…'), owner) // 全部代币账户
143
+ * ```
144
+ */
145
+ export async function approvals(client: SolanaClient, owner: string, query: ApprovalsQuery = {}): Promise<TokenAccountApproval[]> {
146
+ if (!isAddress(owner)) {
147
+ throw new Error(`Invalid owner address: ${owner}`)
148
+ }
149
+ const { signal } = query
150
+ const found = query.mints ? await ataAccounts(client, owner, query.mints, signal) : await listAccounts(client, owner, signal)
151
+ // 只看 owner 自己的账户(ATA 的 owner 可能被改过:SPL Token 的 ATA 能 SetAuthority 换 owner)
152
+ const mine = found.filter((f) => f.parsed.owner === owner)
153
+ const mints = [...new Set(mine.map((f) => f.parsed.mint))]
154
+ const metaAddresses = mints.flatMap((m) => [m, getMetadataAddress(m)])
155
+ const metaAccounts = metaAddresses.length ? await client.accounts(metaAddresses, { signal }) : []
156
+ const meta = new Map(mints.map((mint, i) => [mint, mintMeta(metaAccounts[2 * i] ?? null, metaAccounts[2 * i + 1] ?? null)]))
157
+
158
+ const list = mine.map((f): TokenAccountApproval => {
159
+ const p = f.parsed
160
+ const m = meta.get(p.mint) ?? { decimals: null, symbol: null, permanentDelegate: null }
161
+ const format = (v: bigint) => (m.decimals === null ? null : formatUnits(v, m.decimals))
162
+ const closeBlocker: CloseBlocker | null =
163
+ p.closeAuthority !== null && p.closeAuthority !== owner ? 'close-authority' : !p.native && p.amount > 0n ? 'balance' : p.confidentialBalance ? 'confidential-balance' : null
164
+ return {
165
+ account: f.address,
166
+ mint: p.mint,
167
+ owner: p.owner,
168
+ tokenProgram: f.program,
169
+ ata: getAssociatedTokenAddress(owner, p.mint, f.program) === f.address,
170
+ native: p.native,
171
+ symbol: m.symbol,
172
+ decimals: m.decimals,
173
+ amount: p.amount.toString(),
174
+ formatted: format(p.amount),
175
+ delegate: p.delegate,
176
+ delegatedAmount: p.delegatedAmount.toString(),
177
+ delegatedFormatted: format(p.delegatedAmount),
178
+ closeAuthority: p.closeAuthority,
179
+ permanentDelegate: m.permanentDelegate,
180
+ frozen: p.frozen,
181
+ lamports: f.lamports.toString(),
182
+ withheldAmount: p.withheldAmount.toString(),
183
+ revocable: p.delegate !== null && !p.frozen,
184
+ closable: closeBlocker === null,
185
+ closeBlocker,
186
+ }
187
+ })
188
+ return query.all ? list : list.filter((a) => a.delegate !== null || a.closeBlocker === 'close-authority' || BigInt(a.amount) === 0n)
189
+ }
190
+
191
+ /** 指定 mints:两个程序的 ATA 都读(mint 所属程序未知时只有一个存在) */
192
+ async function ataAccounts(client: SolanaClient, owner: string, mints: readonly string[], signal: AbortSignal | undefined): Promise<Found[]> {
193
+ const targets = [...new Set(mints.filter((m) => isAddress(m)))].flatMap((mint) =>
194
+ [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID].map((program) => ({ program, address: getAssociatedTokenAddress(owner, mint, program) })),
195
+ )
196
+ const accounts = targets.length ? await client.accounts(targets.map((t) => t.address), { signal }) : []
197
+ return targets.flatMap((t, i) => {
198
+ const account = accounts[i]
199
+ const parsed = account && account.owner === t.program ? parseTokenAccountState(account.data) : null
200
+ return parsed && account ? [{ address: t.address, program: t.program, lamports: account.lamports, parsed }] : []
201
+ })
202
+ }
203
+
204
+ /** 不传 mints:用 getTokenAccountsByOwner 列出两个程序的全部代币账户(base64,自己解析扩展) */
205
+ async function listAccounts(client: SolanaClient, owner: string, signal: AbortSignal | undefined): Promise<Found[]> {
206
+ const lists = await Promise.all(
207
+ [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID].map((programId) =>
208
+ client.request<{ value: RawTokenAccount[] }>('getTokenAccountsByOwner', [owner, { programId }, { encoding: 'base64', commitment: client.commitment }], { signal }).then(
209
+ (res) => ({ programId, value: res.value ?? [] }),
210
+ ),
211
+ ),
212
+ ).catch((err: unknown) => {
213
+ if (signal?.aborted || !methodUnavailable(err)) {
214
+ throw err // 取消、限频、超时等原样抛出(保留错误类型)
215
+ }
216
+ throw new Error(
217
+ `Listing the token accounts of ${owner} failed (${err instanceof Error ? err.message : String(err)}). ` +
218
+ 'Listing needs getTokenAccountsByOwner (most free public nodes do not support it): pass the mints in `mints`, or use a node that supports it',
219
+ { cause: err },
220
+ )
221
+ })
222
+ return lists.flatMap(({ programId, value }) =>
223
+ value.flatMap((entry) => {
224
+ const parsed = entry.account.owner === programId ? parseTokenAccountState(base64.decode(entry.account.data[0])) : null
225
+ return parsed ? [{ address: entry.pubkey, program: programId, lamports: BigInt(entry.account.lamports), parsed }] : []
226
+ }),
227
+ )
228
+ }
229
+
230
+ /** mint 的精度、symbol(Token-2022 TokenMetadata 扩展或 Metaplex 元数据)、永久授权 */
231
+ function mintMeta(mint: AccountInfo | null, metadata: AccountInfo | null): { decimals: number | null; symbol: string | null; permanentDelegate: string | null } {
232
+ if (!mint) {
233
+ return { decimals: null, symbol: null, permanentDelegate: null }
234
+ }
235
+ let decimals: number | null = null
236
+ let symbol: string | null = null
237
+ try {
238
+ const info = parseMint(mint.data)
239
+ decimals = info.decimals
240
+ symbol = info.metadata?.symbol || null
241
+ } catch {
242
+ // 不是 mint
243
+ }
244
+ if (!symbol && metadata) {
245
+ try {
246
+ symbol = parseMetaplexMetadata(metadata.data).symbol || null
247
+ } catch {
248
+ // 元数据格式不对
249
+ }
250
+ }
251
+ const ext = findExtension(mint.data, EXT_PERMANENT_DELEGATE)
252
+ const delegate = ext && ext.length >= 32 ? encodeAddress(ext.slice(0, 32)) : null
253
+ return { decimals, symbol, permanentDelegate: delegate && delegate !== ZERO_PUBKEY ? delegate : null }
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // 撤销、关闭
258
+ // ---------------------------------------------------------------------------
259
+
260
+ /** 一个账户要执行的指令(作为 batchCall 的一个调用,原子执行) */
261
+ export interface ApprovalCall {
262
+ /** revoke:撤销 delegate;close:关闭账户(有暂存的转账手续费时先收到 mint 上) */
263
+ kind: 'revoke' | 'close'
264
+ account: string
265
+ /** 要签名的地址(代币账户的 owner) */
266
+ owner: string
267
+ instructions: Instruction[]
268
+ approval: TokenAccountApproval
269
+ }
270
+
271
+ /** 撤销授权的指令:每个有 delegate、没有冻结的账户一条 Revoke;其他的跳过 */
272
+ export function buildRevokes(list: readonly TokenAccountApproval[]): ApprovalCall[] {
273
+ return list
274
+ .filter((a) => a.revocable)
275
+ .map((a) => ({ kind: 'revoke', account: a.account, owner: a.owner, instructions: [revoke(a.account, a.owner, a.tokenProgram)], approval: a }))
276
+ }
277
+
278
+ export interface BuildClosesOptions {
279
+ /** 租金(和 Wrapped SOL 包装的 SOL)退给谁,默认账户的 owner */
280
+ destination?: string
281
+ }
282
+
283
+ /**
284
+ * 关闭账户的指令:每个 closable 的账户一条 CloseAccount(关闭同时去掉授权);Token-2022 账户里有暂存的转账手续费时,
285
+ * 先加一条 HarvestWithheldTokensToMint(不需要签名)。关不掉的跳过。
286
+ * 注意 Wrapped SOL 账户有余额时也能关闭(余额作为 SOL 退回 destination):只想回收空账户时先按 amount 过滤
287
+ */
288
+ export function buildCloses(list: readonly TokenAccountApproval[], options: BuildClosesOptions = {}): ApprovalCall[] {
289
+ if (options.destination !== undefined && !isAddress(options.destination)) {
290
+ throw new Error(`Invalid destination: ${options.destination}`)
291
+ }
292
+ return list
293
+ .filter((a) => a.closable)
294
+ .map((a) => ({
295
+ kind: 'close',
296
+ account: a.account,
297
+ owner: a.owner,
298
+ instructions: [
299
+ ...(a.tokenProgram === TOKEN_2022_PROGRAM_ID && BigInt(a.withheldAmount) > 0n ? [harvestWithheldTokensToMint(a.mint, [a.account])] : []),
300
+ closeAccount(a.account, options.destination ?? a.owner, a.owner, a.tokenProgram),
301
+ ],
302
+ approval: a,
303
+ }))
304
+ }
305
+
306
+ export interface ApprovalResult extends WriteResult {
307
+ kind: ApprovalCall['kind']
308
+ account: string
309
+ approval: TokenAccountApproval
310
+ }
311
+
312
+ export interface CloseAccountsOptions extends BatchCallOptions, BuildClosesOptions {}
313
+
314
+ /**
315
+ * 撤销授权:构建(同 buildRevokes)后用 owner 签名发送,钱包和选项同 batchCall(多个账户打包进同一笔交易,feePayer 可以替 owner 付手续费)。
316
+ * 默认一个失败不影响后面的(continueOnError: true);结果与构建出的调用一一对应
317
+ *
318
+ * ```ts
319
+ * const list = await approvals(client, owner, { mints })
320
+ * await revokeApprovals(client, key, list)
321
+ * ```
322
+ */
323
+ export function revokeApprovals(client: SolanaClient, from: SignerInput, list: readonly TokenAccountApproval[], options: BatchCallOptions = {}): Promise<ApprovalResult[]> {
324
+ return send(client, from, buildRevokes(list), options)
325
+ }
326
+
327
+ /**
328
+ * 关闭代币账户回收租金(同 buildCloses),发送同 revokeApprovals。
329
+ *
330
+ * ```ts
331
+ * const empty = (await approvals(client, owner, { mints })).filter((a) => a.amount === '0')
332
+ * await closeTokenAccounts(client, key, empty) // 租金退回 owner
333
+ * ```
334
+ */
335
+ export function closeTokenAccounts(client: SolanaClient, from: SignerInput, list: readonly TokenAccountApproval[], options: CloseAccountsOptions = {}): Promise<ApprovalResult[]> {
336
+ const { destination, ...rest } = options
337
+ return send(client, from, buildCloses(list, { destination }), rest)
338
+ }
339
+
340
+ async function send(client: SolanaClient, from: SignerInput, calls: readonly ApprovalCall[], options: BatchCallOptions): Promise<ApprovalResult[]> {
341
+ if (!calls.length) return []
342
+ // 发送的代码按需加载:只查授权时打包不会带上
343
+ const [{ batchCall }, { resolveSigner }] = await Promise.all([import('./write/batch.js'), import('./write/signer.js')])
344
+ const { address } = await resolveSigner(from)
345
+ const other = calls.find((c) => c.owner !== address)
346
+ if (other) {
347
+ throw new Error(`Token account ${other.account} belongs to ${other.owner}, not the signer ${address}`)
348
+ }
349
+ const results = await batchCall(
350
+ client,
351
+ calls.map((c) => ({ from, instructions: c.instructions })),
352
+ { continueOnError: true, ...options },
353
+ )
354
+ return results.map((r, i) => {
355
+ const call = calls[i] as ApprovalCall
356
+ return { ...r, kind: call.kind, account: call.account, approval: call.approval }
357
+ })
358
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * 批量计划的公共部分(/reclaim、/claim 用):一组“调用”(每个调用的指令原子执行)按交易大小打包、先模拟再发送。
3
+ * 打包规则同 batchCall(相邻的调用放进同一笔交易,不超过 1232 字节和 64 个账户)
4
+ */
5
+ import type { SolanaClient } from './client.js'
6
+ import { isAddress } from './address.js'
7
+ import type { SimulationResult } from './simulate.js'
8
+ import type { BatchCallOptions } from './write/batch.js'
9
+ import { MAX_COMPUTE_UNITS, setComputeUnitLimit, setComputeUnitPrice } from './write/instructions.js'
10
+ import type { WriteResult } from './write/send.js'
11
+ import type { SignerInput } from './write/signer.js'
12
+ import { MAX_ACCOUNT_LOCKS, PACKET_DATA_SIZE, compileMessage, transactionSize, type Instruction } from './write/transaction.js'
13
+
14
+ /** 一个调用:要签名的地址(代币账户 / 仓位的 owner)和原子执行的指令 */
15
+ export interface PlannedCall {
16
+ owner: string
17
+ instructions: Instruction[]
18
+ }
19
+
20
+ /** 把调用按顺序打包成交易,返回每笔交易包含的调用下标。单个调用放不进一笔交易时自己成一笔(发送时会报“transaction too large”) */
21
+ export function packCalls(payer: string, calls: readonly PlannedCall[], batchSize = Number.POSITIVE_INFINITY): number[][] {
22
+ const packs: number[][] = []
23
+ let current: number[] = []
24
+ let instructions: Instruction[] = []
25
+ const fits = (list: readonly Instruction[]) => {
26
+ const probe = compileMessage(payer, [setComputeUnitLimit(MAX_COMPUTE_UNITS), setComputeUnitPrice(1n), ...list], '11111111111111111111111111111111')
27
+ return transactionSize(probe) <= PACKET_DATA_SIZE && probe.accountKeys.length <= MAX_ACCOUNT_LOCKS
28
+ }
29
+ calls.forEach((call, i) => {
30
+ const next = [...instructions, ...call.instructions]
31
+ if (current.length && (current.length >= batchSize || !fits(next))) {
32
+ packs.push(current)
33
+ current = []
34
+ instructions = []
35
+ }
36
+ current.push(i)
37
+ instructions.push(...call.instructions)
38
+ })
39
+ if (current.length) {
40
+ packs.push(current)
41
+ }
42
+ return packs
43
+ }
44
+
45
+ export interface PlanSimulationOptions {
46
+ /** 手续费支付者,默认第一个调用的 owner */
47
+ feePayer?: string
48
+ /** 每笔交易最多几个调用,默认不限(只受交易大小限制) */
49
+ batchSize?: number
50
+ /** 额外要看的地址(如收款地址) */
51
+ watch?: readonly string[]
52
+ signal?: AbortSignal
53
+ /** 算美元价值(/simulate 的 prices) */
54
+ prices?: boolean
55
+ fetch?: typeof fetch
56
+ }
57
+
58
+ export interface PlanSimulation {
59
+ /** 所有交易都模拟通过 */
60
+ success: boolean
61
+ /** 打包后的每笔交易:包含的调用下标和 /simulate 的结果 */
62
+ transactions: Array<{ calls: number[]; simulation: SimulationResult }>
63
+ /** 模拟失败的调用(整笔失败时逐个重新模拟找出来):下标和原因 */
64
+ failed: Array<{ index: number; error: string }>
65
+ /** 各笔交易手续费之和(lamports,十进制字符串) */
66
+ fee: string
67
+ /** 手续费支付者的 SOL 变化之和(lamports,带符号,不含手续费:/simulate 的 SOL 余额变化已经把手续费加回) */
68
+ payerNativeDelta: string
69
+ }
70
+
71
+ /** 打包后逐笔用 /simulate 模拟(不签名、不发送);失败的交易逐个调用重新模拟,找出是哪些调用失败 */
72
+ export async function simulatePlan(client: SolanaClient, calls: readonly PlannedCall[], options: PlanSimulationOptions = {}): Promise<PlanSimulation> {
73
+ const first = calls[0]
74
+ if (!first) {
75
+ return { success: true, transactions: [], failed: [], fee: '0', payerNativeDelta: '0' }
76
+ }
77
+ const payer = options.feePayer ?? first.owner
78
+ if (!isAddress(payer)) {
79
+ throw new Error(`Invalid fee payer: ${payer}`)
80
+ }
81
+ const { simulate } = await import('./simulate.js')
82
+ const run = (list: readonly PlannedCall[]) =>
83
+ simulate(
84
+ client,
85
+ { feePayer: payer, instructions: list.flatMap((c) => c.instructions) },
86
+ { signers: [...new Set(list.map((c) => c.owner))], watch: options.watch, signal: options.signal, prices: options.prices, fetch: options.fetch },
87
+ )
88
+ const packs = packCalls(payer, calls, options.batchSize)
89
+ const transactions: PlanSimulation['transactions'] = []
90
+ const failed: PlanSimulation['failed'] = []
91
+ for (const pack of packs) {
92
+ const simulation = await run(pack.map((i) => calls[i] as PlannedCall))
93
+ transactions.push({ calls: pack, simulation })
94
+ if (!simulation.success) {
95
+ if (pack.length === 1) {
96
+ failed.push({ index: pack[0] as number, error: simulation.error ?? 'simulation failed' })
97
+ continue
98
+ }
99
+ for (const i of pack) {
100
+ const single = await run([calls[i] as PlannedCall])
101
+ if (!single.success) {
102
+ failed.push({ index: i, error: single.error ?? 'simulation failed' })
103
+ }
104
+ }
105
+ }
106
+ }
107
+ let fee = 0n
108
+ let delta = 0n
109
+ for (const { simulation } of transactions) {
110
+ fee += BigInt(simulation.fee ?? '0')
111
+ for (const change of simulation.balanceChanges) {
112
+ if (change.address === payer && change.asset === 'native') {
113
+ delta += BigInt(change.delta)
114
+ }
115
+ }
116
+ }
117
+ return { success: transactions.every((t) => t.simulation.success), transactions, failed, fee: fee.toString(), payerNativeDelta: delta.toString() }
118
+ }
119
+
120
+ /** 用 from 签名发送:调用的 owner 都必须是签名者本人;一个调用失败不影响后面的(continueOnError),feePayer 可以替 owner 付手续费 */
121
+ export async function sendCalls(client: SolanaClient, from: SignerInput, calls: readonly PlannedCall[], options: BatchCallOptions = {}): Promise<WriteResult[]> {
122
+ if (!calls.length) {
123
+ return []
124
+ }
125
+ // 发送的代码按需加载:只查询、模拟时打包不会带上
126
+ const [{ batchCall }, { resolveSigner }] = await Promise.all([import('./write/batch.js'), import('./write/signer.js')])
127
+ const { address } = await resolveSigner(from)
128
+ const other = calls.find((c) => c.owner !== address)
129
+ if (other) {
130
+ throw new Error(`The account belongs to ${other.owner}, not the signer ${address}`)
131
+ }
132
+ return batchCall(
133
+ client,
134
+ calls.map((c) => ({ from, instructions: c.instructions })),
135
+ { continueOnError: true, ...options },
136
+ )
137
+ }