@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/src/risk.ts ADDED
@@ -0,0 +1,243 @@
1
+ /**
2
+ * 代币风险检查(子路径 `@w3lib/solana-batch-call/risk`):mint 的权限(还能不能增发、能不能冻结持有人)和 Token-2022 扩展里
3
+ * 影响持有人的设置(转账手续费、永久授权、转账 hook、不可转让、新账户默认冻结、可暂停、保密转账、mint 可关闭等),一次 getMultipleAccounts 读完。
4
+ * 可选:持币集中度(getTokenLargestAccounts,免费公共节点大多不支持);能不能买、能不能卖用 tradeChecks(在主网模拟“买入 → 卖出”)
5
+ */
6
+ import { isAddress } from './address.js'
7
+ import type { RequestOptions, SolanaClient } from './client.js'
8
+ import { NATIVE_MINT, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from './constants.js'
9
+ import { findExtension, parseMint, parseTransferFeeConfig, type AccountInfo } from './layout.js'
10
+ import { Bytes } from './lp.js'
11
+ import { CLOCK_SYSVAR_ID, parseClock } from './swap/common.js'
12
+
13
+ /** Token-2022 扩展的类型编号(spl-token-2022 ExtensionType) */
14
+ const EXT = {
15
+ transferFeeConfig: 1,
16
+ mintCloseAuthority: 3,
17
+ confidentialTransferMint: 4,
18
+ defaultAccountState: 6,
19
+ nonTransferable: 9,
20
+ interestBearingConfig: 10,
21
+ permanentDelegate: 12,
22
+ transferHook: 14,
23
+ scaledUiAmount: 25,
24
+ pausable: 26,
25
+ permissionedBurn: 28,
26
+ } as const
27
+
28
+ const ZERO = '11111111111111111111111111111111'
29
+ const key = (b: Bytes, o: number) => {
30
+ const k = b.key(o)
31
+ return k === ZERO ? null : k
32
+ }
33
+
34
+ export interface SolanaTokenRisk {
35
+ mint: string
36
+ /** mint 账户存在且是 SPL Token / Token-2022 的 mint */
37
+ exists: boolean
38
+ program: 'spl-token' | 'token-2022' | null
39
+ decimals: number | null
40
+ /** 总量(最小单位) */
41
+ supply: string | null
42
+ /** 增发权限:不为 null 时还能继续增发 */
43
+ mintAuthority: string | null
44
+ /** 冻结权限:不为 null 时可以冻结任何持有人的代币账户 */
45
+ freezeAuthority: string | null
46
+ /** Token-2022 转账手续费(当前 epoch 生效的);authority 不为 null 时费率还能改(新费率两个 epoch 后生效) */
47
+ transferFee: { basisPoints: number; maximumFee: string; authority: string | null; next: { basisPoints: number; maximumFee: string; epoch: string } | null } | null
48
+ /** 永久授权:这个地址可以转走 / 销毁任何持有人的代币 */
49
+ permanentDelegate: string | null
50
+ /** 转账 hook:每次转账都会调用这个程序(可以拒绝转账);authority 不为 null 时程序还能换 */
51
+ transferHook: { program: string | null; authority: string | null } | null
52
+ /** 不可转让(灵魂绑定) */
53
+ nonTransferable: boolean
54
+ /** 新建的代币账户默认是冻结的(要冻结权限解冻后才能用) */
55
+ defaultFrozen: boolean
56
+ /** 可暂停:authority 可以暂停全部转账、增发、销毁;paused 为当前状态 */
57
+ pausable: { paused: boolean; authority: string | null } | null
58
+ /** 保密转账(余额和转账数量加密) */
59
+ confidentialTransfer: { authority: string | null; autoApprove: boolean } | null
60
+ /** mint 关闭权限(总量为 0 时可以关闭 mint) */
61
+ mintCloseAuthority: string | null
62
+ /** 显示数量会随时间 / 系数变化(InterestBearingConfig、ScaledUiAmount),authority 可以改利率 / 系数 */
63
+ uiAmountAuthority: string | null
64
+ /** 只有这个地址能批准销毁(PermissionedBurn) */
65
+ burnAuthority: string | null
66
+ /** 持币集中度(concentration: true 时):最大的 20 个代币账户里前 10 个占总量的比例;节点不支持或总量为 0 时为 null */
67
+ topHolders: { top10Share: number; accounts: Array<{ address: string; amount: string; share: number }> } | null
68
+ /**
69
+ * 汇总的风险标记:not-found、mintable、freezable、transfer-fee(> 0)、high-transfer-fee(> 10%)、fee-changeable、permanent-delegate、
70
+ * transfer-hook、non-transferable、default-frozen、pausable、paused、confidential-transfer、closable、ui-amount-changes、permissioned-burn、
71
+ * concentrated(前 10 个账户 > 50%,含池子金库、交易所地址)
72
+ */
73
+ flags: string[]
74
+ }
75
+
76
+ export interface TokenRisksOptions extends RequestOptions {
77
+ /** 查持币集中度(getTokenLargestAccounts,每个 mint 一次请求;免费公共节点大多不支持,不支持时 topHolders 为 null),默认 false */
78
+ concentration?: boolean
79
+ }
80
+
81
+ /**
82
+ * 检查代币的风险(mint 和 Clock 一次 getMultipleAccounts 读完)。原生 SOL(客户端的 nativeMints)按 Wrapped SOL 的 mint 查。
83
+ *
84
+ * ```ts
85
+ * const [risk] = await tokenRisks(client, [PYUSD])
86
+ * // { program: 'token-2022', mintAuthority: '22mK…', freezeAuthority: '2apB…', permanentDelegate: '2apB…', flags: ['mintable', 'freezable', 'permanent-delegate', …] }
87
+ * ```
88
+ */
89
+ export async function tokenRisks(client: SolanaClient, mints: readonly string[], options: TokenRisksOptions = {}): Promise<SolanaTokenRisk[]> {
90
+ const keys = mints.map((m) => (client.isNativeMint(m) ? NATIVE_MINT : m))
91
+ for (const m of keys) {
92
+ if (!isAddress(m)) throw new Error(`Invalid mint address: ${m}`)
93
+ }
94
+ const unique = [...new Set(keys)]
95
+ const accounts = await client.accounts([CLOCK_SYSVAR_ID, ...unique], { signal: options.signal })
96
+ const clock = accounts[0]
97
+ const epoch = clock ? parseClock(clock.data).epoch : null
98
+ const byMint = new Map(unique.map((m, i) => [m, accounts[i + 1] ?? null]))
99
+ const holders = options.concentration ? await largestAccounts(client, unique, options.signal) : new Map<string, Array<{ address: string; amount: bigint }> | null>()
100
+ return mints.map((mint, i) => {
101
+ const k = keys[i] as string
102
+ const risk = parseRisk(mint, byMint.get(k) ?? null, epoch)
103
+ const list = holders.get(k)
104
+ if (list && risk.supply && BigInt(risk.supply) > 0n) {
105
+ const supply = BigInt(risk.supply)
106
+ const share = (a: bigint) => Number((a * 1_000_000n) / supply) / 1_000_000
107
+ const sorted = [...list].sort((a, b) => (a.amount < b.amount ? 1 : a.amount > b.amount ? -1 : 0))
108
+ risk.topHolders = {
109
+ top10Share: share(sorted.slice(0, 10).reduce((s, a) => s + a.amount, 0n)),
110
+ accounts: sorted.map((a) => ({ address: a.address, amount: a.amount.toString(), share: share(a.amount) })),
111
+ }
112
+ if (risk.topHolders.top10Share > 0.5) risk.flags.push('concentrated')
113
+ }
114
+ return risk
115
+ })
116
+ }
117
+
118
+ async function largestAccounts(client: SolanaClient, mints: readonly string[], signal: AbortSignal | undefined) {
119
+ const out = new Map<string, Array<{ address: string; amount: bigint }> | null>()
120
+ await Promise.all(
121
+ mints.map(async (mint) => {
122
+ try {
123
+ const res = await client.request<{ value: Array<{ address: string; amount: string }> }>('getTokenLargestAccounts', [mint, { commitment: client.commitment }], { signal })
124
+ out.set(
125
+ mint,
126
+ res.value.map((a) => ({ address: a.address, amount: BigInt(a.amount) })),
127
+ )
128
+ } catch (err) {
129
+ if (signal?.aborted) throw err
130
+ out.set(mint, null) // 节点不支持
131
+ }
132
+ }),
133
+ )
134
+ return out
135
+ }
136
+
137
+ function empty(mint: string): SolanaTokenRisk {
138
+ return {
139
+ mint,
140
+ exists: false,
141
+ program: null,
142
+ decimals: null,
143
+ supply: null,
144
+ mintAuthority: null,
145
+ freezeAuthority: null,
146
+ transferFee: null,
147
+ permanentDelegate: null,
148
+ transferHook: null,
149
+ nonTransferable: false,
150
+ defaultFrozen: false,
151
+ pausable: null,
152
+ confidentialTransfer: null,
153
+ mintCloseAuthority: null,
154
+ uiAmountAuthority: null,
155
+ burnAuthority: null,
156
+ topHolders: null,
157
+ flags: ['not-found'],
158
+ }
159
+ }
160
+
161
+ /** 解析一个 mint 账户的风险项(包内使用,测试也用);epoch 用来选转账手续费的新旧配置(null 时按新配置) */
162
+ export function parseRisk(mint: string, account: AccountInfo | null, epoch: bigint | null): SolanaTokenRisk {
163
+ if (!account || (account.owner !== TOKEN_PROGRAM_ID && account.owner !== TOKEN_2022_PROGRAM_ID)) return empty(mint)
164
+ let info: ReturnType<typeof parseMint>
165
+ try {
166
+ info = parseMint(account.data)
167
+ } catch {
168
+ return empty(mint)
169
+ }
170
+ const risk: SolanaTokenRisk = {
171
+ ...empty(mint),
172
+ exists: true,
173
+ program: account.owner === TOKEN_PROGRAM_ID ? 'spl-token' : 'token-2022',
174
+ decimals: info.decimals,
175
+ supply: info.supply.toString(),
176
+ mintAuthority: info.mintAuthority,
177
+ freezeAuthority: info.freezeAuthority,
178
+ flags: [],
179
+ }
180
+ if (account.owner === TOKEN_2022_PROGRAM_ID) {
181
+ const data = account.data
182
+ const ext = (type: number) => findExtension(data, type)
183
+ const fee = parseTransferFeeConfig(data)
184
+ if (fee) {
185
+ const current = epoch === null || epoch >= fee.newer.epoch ? fee.newer : fee.older
186
+ const pending = epoch !== null && epoch < fee.newer.epoch ? fee.newer : null
187
+ risk.transferFee = {
188
+ basisPoints: current.basisPoints,
189
+ maximumFee: current.maximumFee.toString(),
190
+ authority: fee.transferFeeConfigAuthority,
191
+ next: pending ? { basisPoints: pending.basisPoints, maximumFee: pending.maximumFee.toString(), epoch: pending.epoch.toString() } : null,
192
+ }
193
+ }
194
+ const close = ext(EXT.mintCloseAuthority)
195
+ if (close && close.length >= 32) risk.mintCloseAuthority = key(new Bytes(close), 0)
196
+ const confidential = ext(EXT.confidentialTransferMint)
197
+ if (confidential && confidential.length >= 33) risk.confidentialTransfer = { authority: key(new Bytes(confidential), 0), autoApprove: confidential[32] === 1 }
198
+ const state = ext(EXT.defaultAccountState)
199
+ // AccountState:0 未初始化、1 正常、2 冻结
200
+ risk.defaultFrozen = !!state && state[0] === 2
201
+ risk.nonTransferable = ext(EXT.nonTransferable) !== null
202
+ const delegate = ext(EXT.permanentDelegate)
203
+ if (delegate && delegate.length >= 32) risk.permanentDelegate = key(new Bytes(delegate), 0)
204
+ const hook = ext(EXT.transferHook)
205
+ if (hook && hook.length >= 64) {
206
+ const b = new Bytes(hook)
207
+ risk.transferHook = { authority: key(b, 0), program: key(b, 32) }
208
+ }
209
+ const pausable = ext(EXT.pausable)
210
+ if (pausable && pausable.length >= 33) risk.pausable = { authority: key(new Bytes(pausable), 0), paused: pausable[32] === 1 }
211
+ for (const type of [EXT.interestBearingConfig, EXT.scaledUiAmount]) {
212
+ const e = ext(type)
213
+ if (e && e.length >= 32) risk.uiAmountAuthority ??= key(new Bytes(e), 0) ?? null
214
+ }
215
+ const burn = ext(EXT.permissionedBurn)
216
+ if (burn && burn.length >= 32) risk.burnAuthority = key(new Bytes(burn), 0)
217
+ }
218
+ risk.flags = flagsOf(risk)
219
+ return risk
220
+ }
221
+
222
+ function flagsOf(r: SolanaTokenRisk): string[] {
223
+ const flags: string[] = []
224
+ if (r.mintAuthority) flags.push('mintable')
225
+ if (r.freezeAuthority) flags.push('freezable')
226
+ if (r.transferFee) {
227
+ const bps = Math.max(r.transferFee.basisPoints, r.transferFee.next?.basisPoints ?? 0)
228
+ if (bps > 0) flags.push('transfer-fee')
229
+ if (bps > 1000) flags.push('high-transfer-fee')
230
+ if (r.transferFee.authority) flags.push('fee-changeable')
231
+ }
232
+ if (r.permanentDelegate) flags.push('permanent-delegate')
233
+ if (r.transferHook?.program || r.transferHook?.authority) flags.push('transfer-hook')
234
+ if (r.nonTransferable) flags.push('non-transferable')
235
+ if (r.defaultFrozen) flags.push('default-frozen')
236
+ if (r.pausable) flags.push('pausable')
237
+ if (r.pausable?.paused) flags.push('paused')
238
+ if (r.confidentialTransfer) flags.push('confidential-transfer')
239
+ if (r.mintCloseAuthority) flags.push('closable')
240
+ if (r.uiAmountAuthority) flags.push('ui-amount-changes')
241
+ if (r.burnAuthority) flags.push('permissioned-burn')
242
+ return flags
243
+ }
package/src/rpc.ts CHANGED
@@ -61,7 +61,32 @@ const RATE_LIMITED = /rate.?limit|too many requests/i
61
61
  const TRANSIENT = /\bbusy\b|overload|temporar(il)?y|unavailable|try again|timed? ?out|timeout/i
62
62
 
63
63
  /** 节点限制单次 getMultipleAccounts 的账户数(如 “Too many accounts requested”):缩小每次的账户数后重试,不是节点故障 */
64
- const TOO_MANY_ACCOUNTS = /too many accounts/i
64
+ const TOO_MANY_ACCOUNTS = /too many accounts|maximum of \d+ accounts/i
65
+ /**
66
+ * publicnode 对单次超过 10 个账户的 getMultipleAccounts 返回 HTTP 403 +“Request blocked”(2026-10,而且要等 3 秒多才返回):
67
+ * 账户数超过 BLOCKED_LIMIT 时直接按它拆分(逐次减半要多等好几轮),还被拒绝再减半;
68
+ * 账户数已经很少(≤ BLOCKED_FLOOR)还被拒绝时,是节点真的拒绝访问,交给上层换节点
69
+ */
70
+ const REQUEST_BLOCKED = /request blocked/i
71
+ const BLOCKED_LIMIT = 10
72
+ const BLOCKED_FLOOR = 8
73
+
74
+ /** 从报错里学到的节点限制,按节点地址共享(同一个节点的多个客户端不用各自再试一遍) */
75
+ const learnedLimits = new Map<string, { methods: Map<string, number>; maxAccounts?: number }>()
76
+
77
+ function learnedFor(url: string): { methods: Map<string, number>; maxAccounts?: number } {
78
+ let entry = learnedLimits.get(url)
79
+ if (!entry) {
80
+ entry = { methods: new Map() }
81
+ learnedLimits.set(url, entry)
82
+ }
83
+ return entry
84
+ }
85
+
86
+ /** 测试用:清空学到的节点限制 */
87
+ export function resetLearnedLimits(): void {
88
+ learnedLimits.clear()
89
+ }
65
90
 
66
91
  /** HTTP 层错误(非 2xx、网络错误、超时) */
67
92
  export class HttpError extends Error {
@@ -178,20 +203,19 @@ export class HttpRpc implements RpcTransport {
178
203
  readonly #retries: number
179
204
  readonly #fetch: typeof fetch
180
205
  #maxBatchSize: number
181
- /** 节点对单个方法在一个批量里的数量限制(如 publicnode:getMultipleAccounts 最多 1 个),从报错里学到 */
182
- readonly #methodLimits = new Map<string, number>()
206
+ /** 从报错里学到的限制(按节点地址共享):单个方法在一个批量里的数量(如 publicnode:getMultipleAccounts 最多 1 个)、单次的账户数 */
207
+ readonly #learned: { methods: Map<string, number>; maxAccounts?: number }
183
208
  /** options.batch:调用方是否允许批量 */
184
209
  readonly #batchOption: boolean
185
210
  /** 节点明确表示不支持批量时,在这之前逐条请求(之后重新尝试批量) */
186
211
  #batchDisabledUntil = 0
187
- /** 节点限制单次 getMultipleAccounts 的账户数时学到的上限 */
188
- #maxAccounts: number | undefined
189
212
  #queue: Pending[] = []
190
213
  #timer: ReturnType<typeof setTimeout> | null = null
191
214
 
192
215
  constructor(url: string, options: HttpRpcOptions = {}) {
193
216
  this.url = url
194
217
  this.#safeUrl = safeOrigin(url)
218
+ this.#learned = learnedFor(url)
195
219
  this.#timeout = options.timeout ?? 10_000
196
220
  this.#headers = { 'content-type': 'application/json', ...options.headers }
197
221
  this.#maxBatchSize = Math.max(1, options.maxBatchSize ?? 20)
@@ -214,7 +238,7 @@ export class HttpRpc implements RpcTransport {
214
238
  */
215
239
  async #getMultipleAccounts(params: readonly unknown[]): Promise<unknown> {
216
240
  const [addresses, ...rest] = params as [unknown[], ...unknown[]]
217
- const max = this.#maxAccounts
241
+ const max = this.#learned.maxAccounts
218
242
  if (max !== undefined && addresses.length > max) {
219
243
  const parts: unknown[][] = []
220
244
  for (let i = 0; i < addresses.length; i += max) {
@@ -234,8 +258,13 @@ export class HttpRpc implements RpcTransport {
234
258
  try {
235
259
  return await this.#enqueue('getMultipleAccounts', params)
236
260
  } catch (err) {
237
- if (err instanceof RpcError && TOO_MANY_ACCOUNTS.test(err.message) && addresses.length > 1) {
238
- this.#maxAccounts = Math.max(1, Math.min(this.#maxAccounts ?? Number.POSITIVE_INFINITY, Math.floor(addresses.length / 2)))
261
+ const tooMany = err instanceof RpcError && TOO_MANY_ACCOUNTS.test(err.message) && addresses.length > 1
262
+ const blocked = err instanceof RpcError && REQUEST_BLOCKED.test(err.message) && addresses.length > BLOCKED_FLOOR
263
+ if (tooMany || blocked) {
264
+ // publicnode(2026-10):“A maximum of 10 accounts is allowed”,报错里直接给出上限
265
+ const stated = Number(/maximum of (\d+) accounts/i.exec(err.message)?.[1])
266
+ const next = stated >= 1 ? stated : blocked && addresses.length > BLOCKED_LIMIT ? BLOCKED_LIMIT : Math.floor(addresses.length / 2)
267
+ this.#learned.maxAccounts = Math.max(1, Math.min(this.#learned.maxAccounts ?? Number.POSITIVE_INFINITY, next))
239
268
  return this.#getMultipleAccounts(params)
240
269
  }
241
270
  throw err
@@ -276,7 +305,7 @@ export class HttpRpc implements RpcTransport {
276
305
  let current: Pending[] = []
277
306
  let counts = new Map<string, number>()
278
307
  for (const item of items) {
279
- const limit = this.#methodLimits.get(item.method)
308
+ const limit = this.#learned.methods.get(item.method)
280
309
  const count = counts.get(item.method) ?? 0
281
310
  if (current.length >= this.#maxBatchSize || (limit !== undefined && count >= limit)) {
282
311
  chunks.push(current)
@@ -303,7 +332,7 @@ export class HttpRpc implements RpcTransport {
303
332
  if (!method || !Number.isFinite(limit) || limit < 1 || items.filter((item) => item.method === method).length <= limit) {
304
333
  return false
305
334
  }
306
- this.#methodLimits.set(method, limit)
335
+ this.#learned.methods.set(method, limit)
307
336
  await Promise.all(this.#split(items).map((chunk) => this.#dispatch(chunk)))
308
337
  return true
309
338
  }