@w3lib/solana-batch-call 0.0.0-stage → 0.2.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 (49) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +270 -2
  4. package/dist/address.d.ts +20 -0
  5. package/dist/address.d.ts.map +1 -0
  6. package/dist/address.js +120 -0
  7. package/dist/address.js.map +1 -0
  8. package/dist/client.d.ts +190 -0
  9. package/dist/client.d.ts.map +1 -0
  10. package/dist/client.js +971 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/constants.d.ts +21 -0
  13. package/dist/constants.d.ts.map +1 -0
  14. package/dist/constants.js +35 -0
  15. package/dist/constants.js.map +1 -0
  16. package/dist/index.d.ts +9 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +9 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/layout.d.ts +84 -0
  21. package/dist/layout.d.ts.map +1 -0
  22. package/dist/layout.js +237 -0
  23. package/dist/layout.js.map +1 -0
  24. package/dist/rpc.d.ts +115 -0
  25. package/dist/rpc.d.ts.map +1 -0
  26. package/dist/rpc.js +458 -0
  27. package/dist/rpc.js.map +1 -0
  28. package/dist/shortcuts.d.ts +93 -0
  29. package/dist/shortcuts.d.ts.map +1 -0
  30. package/dist/shortcuts.js +281 -0
  31. package/dist/shortcuts.js.map +1 -0
  32. package/dist/source.d.ts +35 -0
  33. package/dist/source.d.ts.map +1 -0
  34. package/dist/source.js +124 -0
  35. package/dist/source.js.map +1 -0
  36. package/dist/types.d.ts +155 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +2 -0
  39. package/dist/types.js.map +1 -0
  40. package/package.json +60 -4
  41. package/src/address.ts +134 -0
  42. package/src/client.ts +1298 -0
  43. package/src/constants.ts +41 -0
  44. package/src/index.ts +98 -0
  45. package/src/layout.ts +320 -0
  46. package/src/rpc.ts +549 -0
  47. package/src/shortcuts.ts +367 -0
  48. package/src/source.ts +149 -0
  49. package/src/types.ts +164 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ - `getBalances` / `getOwnerTokens` 新增 `tokenPrograms`:按代币类型过滤,`['spl']` 不含 Token-2022(扫描时也不再请求 Token-2022 的代币账户),`['token-2022']` 反之;也可以传程序地址。不认识的值或空数组报错
6
+ - `getBalances` 新增 `accounts: 'ata' | 'all'`:指定 mints 时用按 mint 过滤的 `getTokenAccountsByOwner` 统计全部代币账户(含非 ATA),比全量扫描轻
7
+ - 失败项带原因:`success: false` 时返回 `error`(`invalid-address` / `not-found` / `not-token` / `no-metadata` / `no-holder` / `missing-field`),`getTokens` 另带 `errorField`;新增导出类型 `FailureReason`。mint 地址非法和 mint 不存在可以区分了
8
+ - 去掉运行时依赖 `@noble/curves`:PDA 推导的“是否在曲线上”改为内置实现(与 noble / web3.js 结果一致,新增导出 `isOnCurve`);`@noble/hashes` 版本范围放宽为 `^1.8.0 || ^2.0.0`,可与 `@solana/web3.js` 带的 1.x 去重。打包体积(minify)64.2KB → 36.4KB,gzip 24.7KB → 14.0KB
9
+ - 修复:`nativeMints` 不含 `So111…112` 时,扫描模式的余额列表里 wSOL 出现两次、没有原生 SOL
10
+ - 文档:`nativeMints` 可以包含非 Solana 地址(作为调用方自己的主币标识)
11
+ - 所有节点都失败时抛 `AllNodesFailedError`(`errors: { node, error }[]`,node 只含 URL 的 host),不再只有最后一个节点的错误;单节点时仍抛原始错误
12
+ - 超时与出错分开计:超时只换节点、不冷却,同一节点连续 3 次超时才冷却;`HttpError.timeout` 标记超时
13
+ - 节点健康状态(冷却、连续超时)全局共享:URL 相同即同一节点,客户端重建后仍然有效
14
+ - `HttpRpc` 报错信息里的地址只保留 origin,不再带出 path / query 里的 API Key
15
+ - 单个节点也包一层 `FallbackRpc`(`client.transport` 现在总是 `FallbackRpc`,原节点在 `transport.nodes[0]`)
16
+ - `onRequest(listener)`:监听每次调用的节点、方法、耗时、成功与否、第几次尝试;新增 `nodeLabel`、`RequestEvent`,自定义传输可以设 `label`
17
+ - 所有查询方法和独立函数支持 `signal`(`AbortSignal`)
18
+ - `minContextSlot` / `withSlot`(`balances` / `multiBalances` / `solBalances` / `ownerTokens`,`accounts` 支持 `minContextSlot`):节点高度不够时换节点或等待重试(最多约 10 秒),结果带读取时的 `slot`
19
+ - `multiBalances` / `getMultiBalances`:多个钱包一次查,ATA 模式下合并成一次 `getMultipleAccounts`
20
+ - `watchBalances`:轮询余额,相同参数的订阅共用一份轮询,余额变化时才通知
21
+ - 代币信息缓存可持久化:`persistTokenMetaCache(localStorage)`,或 `exportTokenMetaCache()` / `importTokenMetaCache()`
22
+ - `getTokens` 新增字段 `transferFee`:Token-2022 转账手续费(按当前 epoch 选出生效的配置);新增 `parseTransferFeeConfig`
23
+ - 新增 `formatUnits`(与 ethers 同名),`formatAmount` 保留为别名并标记 deprecated
24
+ - `engines.node` 改为 `>=20.19.0`(依赖的 `@noble/hashes` 2.x 的要求)
25
+
26
+ ## 0.1.0
27
+
28
+ 首个版本:
29
+
30
+ - 批量余额(SOL / SPL Token / Token-2022),指定代币时用 ATA + `getMultipleAccounts`,免费节点可用;扫描模式统计全部代币账户
31
+ - 代币详情(字段可选),Metaplex 与 Token-2022 TokenMetadata 扩展
32
+ - 资产列表 `getOwnerTokens`:持有人的全部代币,带 name / symbol,多账户合计,元数据分批读取、失败不影响余额
33
+ - NFT 元数据、持有人、某地址持有的全部 NFT
34
+ - JSON-RPC 自动合并、批量降级、429 重试,多节点故障切换,按创世区块哈希校验网络
35
+ - 支持 URL、web3.js Connection、自定义传输
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 L
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,271 @@
1
- # Temporary Holding Version
1
+ # @w3lib/solana-batch-call
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Solana 批量读取库:余额(SOL / SPL Token / Token-2022)、代币详情、NFT。不依赖 Solana SDK,直接发 JSON-RPC。
4
+
5
+ ```bash
6
+ pnpm add @w3lib/solana-batch-call
7
+ ```
8
+
9
+ - **一次请求查一批**:账户读取用 `getMultipleAccounts`(自动去重、按 100 个一组拆分、最多同时 3 个请求);同一 tick 内发起的 RPC 调用合并成一个 JSON-RPC 批量请求
10
+ - **免费节点也能查余额**:指定代币时在本地推导关联代币账户(ATA)地址,和 SOL 账户、mint 账户一起一次读完,不需要 `getTokenAccountsByOwner` 这类索引方法
11
+ - **Token-2022**:余额、decimals、TokenMetadata 扩展里的 name / symbol / uri 都支持
12
+ - **多节点故障切换**:限频、403、需要 API Key、不支持某方法等节点问题自动换下一个节点;节点不支持批量请求或限制批量大小时自动降级
13
+ - **网络校验**:按创世区块哈希确认节点所在网络,不会把 devnet 的数据当成 mainnet 的返回
14
+ - 结果字段都是字符串 / 数字 / 布尔 / null,可以直接 `JSON.stringify`
15
+
16
+ ## 快速上手
17
+
18
+ ```ts
19
+ import { NATIVE_MINT, getBalances, getMultiBalances, getNftOwners, getNfts, getOwnerNfts, getOwnerTokens, getSolBalances, getTokens, watchBalances } from '@w3lib/solana-batch-call'
20
+
21
+ // 资产列表:持有人拥有的全部代币(SOL 在第一位,带 name / symbol;需要支持索引方法的节点)
22
+ await getOwnerTokens(owner, { provider: 'https://my-rpc.example' })
23
+ // [
24
+ // { token: NATIVE_MINT, native: true, name: 'Solana', symbol: 'SOL', formatted: '1.5', accounts: 1, … },
25
+ // { token: USDC, native: false, name: 'USD Coin', symbol: 'USDC', formatted: '1235', accounts: 2, … }, // 2 个代币账户合计
26
+ // ...
27
+ // ]
28
+
29
+ // 余额:SOL + 代币,带 decimals 换算(不传 provider 时使用内置的 mainnet 公共节点)
30
+ await getBalances(owner, [NATIVE_MINT, USDC, PYUSD], { symbol: true })
31
+ // [
32
+ // { token: NATIVE_MINT, native: true, balance: '1500000000', decimals: 9, formatted: '1.5', tokenProgram: null, symbol: 'SOL', success: true },
33
+ // { token: USDC, native: false, balance: '1234500000', decimals: 6, formatted: '1234.5', tokenProgram: 'Tokenkeg…', symbol: 'USDC', success: true },
34
+ // ...
35
+ // ]
36
+
37
+ // 只要某类代币:tokenPrograms 传 ['spl'] 时结果不含 Token-2022 代币(SOL 不受影响)
38
+ await getBalances(owner, undefined, { provider: 'https://my-rpc.example', tokenPrograms: ['spl'] })
39
+
40
+ // 多个地址的 SOL 余额
41
+ await getSolBalances([addressA, addressB])
42
+
43
+ // 多个钱包一次查(ATA 模式下合并成一次 getMultipleAccounts),结果与 queries 一一对应
44
+ await getMultiBalances([{ owner: walletA, mints: [NATIVE_MINT, USDC] }, { owner: walletB, mints: [USDC] }])
45
+
46
+ // 刚成交后刷新余额:要求节点高度至少到成交所在的 slot(落后的节点自动换掉),结果带 slot
47
+ await getBalances(owner, [NATIVE_MINT, USDC], { minContextSlot: txSlot })
48
+
49
+ // 多处共用一份轮询,余额变化时才通知
50
+ const stop = watchBalances(owner, [NATIVE_MINT, USDC], { interval: 10_000, onChange: (list) => render(list) })
51
+
52
+ // 代币详情,字段可选:name / symbol / uri / decimals / supply / tokenProgram / mintAuthority / freezeAuthority / transferFee(默认 name、symbol、decimals)
53
+ await getTokens([USDC, PYUSD], { fields: ['name', 'symbol', 'decimals', 'supply'] })
54
+ // [{ address: USDC, native: false, name: 'USD Coin', symbol: 'USDC', decimals: 6, supply: '…', supplyFormatted: '…', success: true }, ...]
55
+
56
+ // NFT
57
+ await getNfts([mint]) // 名称、uri、所属集合(是否已认证)、版税、标准、创作者
58
+ await getNftOwners([mint]) // 持有人
59
+ await getOwnerNfts(owner) // 某地址持有的全部 NFT(需要支持索引方法的节点)
60
+ ```
61
+
62
+ | 函数 | SolanaClient 方法 | 说明 |
63
+ | --- | --- | --- |
64
+ | `getBalances(owner, mints?, opts)` | `balances` | SOL + SPL Token + Token-2022 余额;`symbol: true` 额外返回 symbol;不传 mints 返回全部持仓;`tokenPrograms` 按代币类型过滤 |
65
+ | `getOwnerTokens(owner, opts)` | `ownerTokens` | 持有人的全部代币(资产列表),带 name / symbol;默认不含 NFT 和余额为 0 的账户;`tokenPrograms` 按代币类型过滤 |
66
+ | `getMultiBalances(queries, opts)` | `multiBalances` | 多个钱包一次查,参数同 `getBalances` |
67
+ | `watchBalances(owner, mints?, opts)` | — | 轮询余额,相同参数的订阅共用一份轮询,余额变化时才通知 `onChange`;返回取消函数 |
68
+ | `getSolBalances(addresses, opts)` | `solBalances` | 多个地址的 SOL 余额 |
69
+ | `getTokens(mints, opts)` | `tokens` | 代币详情,`fields` 选择字段,结果类型随之收窄;`transferFee` 返回 Token-2022 转账手续费 |
70
+ | `getNfts(mints, opts)` | `nfts` | NFT 元数据(Metaplex;Token-2022 NFT 取扩展) |
71
+ | `getNftOwners(mints, opts)` | `nftOwners` | NFT 持有人 |
72
+ | `getOwnerNfts(owner, opts)` | `ownerNfts` | 某地址持有的全部 NFT |
73
+ | — | `accounts(addresses)` | 批量读原始账户(`AccountInfo \| null`) |
74
+ | — | `request(method, params)` | 直接发 JSON-RPC(同样参与合并与故障切换) |
75
+
76
+ ## 资产列表
77
+
78
+ `getOwnerTokens(owner)` 扫描持有人的全部代币账户(Token + Token-2022),同一代币的多个账户合计,返回:
79
+
80
+ - 第一项是 SOL,之后是 SPL Token、Token-2022 的代币(各自按节点返回的顺序;库里没有价格信息,不按价值排序)
81
+ - 每项带 `accounts`(代币账户数量);默认还带 `name` / `symbol` / `metadataStatus`(`ok` 读到了、`missing` 确认没有元数据、`failed` 节点问题没读到),`metadata: false` 时不读元数据、不返回这三个字段
82
+ - Wrapped SOL 代币账户单独列出(`token` 同样是 `So111…112`,`native: false`),与原生 SOL 那一项区分
83
+ - 默认不含 NFT 和余额为 0 的代币账户(已清空的 ATA),用 `includeNfts` / `includeZero` 打开;`includeNative: false` 不返回 SOL。NFT 的判断按 mint 汇总:精度 0、合计数量 1(与 `getOwnerNfts` 一致)
84
+
85
+ 持仓很多时读元数据较慢:读取账户最多同时 3 个请求、每个 100 个地址,以避免限频(实测 4000 个代币在官方节点上约 80 秒,不读元数据约 4 秒)。可以先用 `metadata: false` 拿到列表,再对需要展示的代币调用 `getTokens`。元数据读取遇到限频等节点问题时不再继续请求,对应代币的 `metadataStatus` 为 `failed`、`name` / `symbol` 为 `null`,余额列表照常返回;之后可以对这些代币单独重查。
86
+
87
+ 需要支持 `getTokenAccountsByOwner` 的节点,内置的免费公共节点大多不支持。
88
+
89
+ ## 余额的三种模式
90
+
91
+ | | 指定 mints(默认,`accounts: 'ata'`) | 指定 mints + `accounts: 'all'` | 扫描(不传 mints,或 `scan: true`) |
92
+ | --- | --- | --- | --- |
93
+ | 实现 | 本地推导 ATA + 一次 `getMultipleAccounts` | 每个代币一次按 mint 过滤的 `getTokenAccountsByOwner`(同一 tick 发出,合并成批量请求) | `getTokenAccountsByOwner`(Token / Token-2022 各一次) |
94
+ | 统计范围 | 只统计 **ATA** 里的余额 | 指定代币的**全部**代币账户,多个账户合计 | 该地址的**全部**代币账户,同一代币多个账户合计 |
95
+ | 返回 | 指定的代币 | 指定的代币 | 不传 mints:所有余额大于 0 的代币;传 mints:指定的代币 |
96
+ | 免费公共节点 | 可用 | 大多不支持,需要自己的节点(Helius、QuickNode、Triton 等) | 同左 |
97
+
98
+ 普通钱包的代币都在 ATA 里,用默认模式即可。交易所、托管类地址常有非 ATA 的代币账户(实际碰到过 PYUSD 余额全在非 ATA 账户里的持有人),默认模式会少算:只关心少数几个代币时用 `accounts: 'all'`,要全部持仓时用扫描。同时传 `scan: true` 和 `accounts` 时按扫描处理。
99
+
100
+ ```ts
101
+ await getBalances(owner, [USDC, PYUSD], { provider: 'https://my-rpc.example', accounts: 'all' })
102
+ ```
103
+
104
+ 其他规则:
105
+
106
+ - **按代币类型过滤**:`tokenPrograms` 默认两种都返回,传 `['spl']` 只要 SPL Token、传 `['token-2022']` 只要 Token-2022(也可以传程序地址 `TOKEN_PROGRAM_ID` / `TOKEN_2022_PROGRAM_ID`;不认识的值或空数组会报错),不符合的代币直接从结果里去掉(结果条数可能少于 mints)。扫描模式下只请求对应程序的代币账户;指定 mints 时所属程序未知的代币先读 mint 账户确认(与 ATA 在同一次请求里)。mint 不存在、无法判断类型的项照常返回 `success: false`;主币 SOL 不受影响(`getOwnerTokens` 用 `includeNative` 控制)
107
+
108
+ - **主币**:`So111…112`(`NATIVE_MINT`,Wrapped SOL 的 mint)和 `11111…1`(System Program)按原生 SOL 查询(lamports,9 位精度);此时 Wrapped SOL 代币账户里的余额不单独列出。可用 `nativeMints` 配置,传 `[SYSTEM_PROGRAM_ID]` 时 `So111…112` 按普通代币(Wrapped SOL)查询。`nativeMints` 也可以包含非 Solana 地址,作为调用方自己的主币标识(如多链项目统一用 `0xEeee…EEeE` 表示主币),`balances` / `tokens` 遇到它时按 SOL 返回、`token` 原样带回,这是支持的用法
109
+ - **缓存**:decimals、所属程序永久缓存;name、symbol 来自可修改的元数据,缓存 1 小时(确认没有元数据的代币同样缓存,不会每次重查)。按网络共享(各函数共用);自定义节点且没指定 `cluster` 时只在该客户端内缓存。最多 5 万个代币,超出淘汰最早的
110
+ - **大数**:lamports 等 u64 超过 2⁵³ 时不会丢精度
111
+ - **失败**:mint 不存在、地址非法等只让该项 `success: false`,并带 `error` 说明原因;节点不可用等整个请求失败时才抛错(多个节点都失败时是 `AllNodesFailedError`,`err.errors` 里有每个节点的名称和原因)
112
+ - **取消**:所有查询都可以传 `signal`(`AbortSignal`)。已取消时直接 reject、不发请求;中途取消时立即 reject,已经发出、和其他查询合并在一起的底层请求继续完成
113
+ - **最低 slot**:`getBalances` / `getMultiBalances` / `getSolBalances` / `getOwnerTokens` / `accounts` 可以传 `minContextSlot`:节点高度不够时换下一个节点,都不够时每 500ms 重试,最多约 10 秒;`withSlot: true`(或传了 `minContextSlot`)时结果项带读取时的 `slot`(一次查询发了多个请求时取最小的)
114
+
115
+ ## 轮询余额
116
+
117
+ ```ts
118
+ const stop = watchBalances(owner, [NATIVE_MINT, USDC], {
119
+ interval: 10_000, // 默认 15 秒;多处订阅同一份时取最小的
120
+ onChange: (list, previous) => render(list), // 第一次结果、以及之后余额变化时调用
121
+ onError: (err) => console.warn(err), // 某次失败,之后照常轮询
122
+ })
123
+ stop() // 最后一个订阅者取消时停止轮询
124
+ ```
125
+
126
+ - 同样的节点配置 + owner + mints(顺序无关)+ 影响结果的选项共用一份轮询;结果顺序按第一个订阅者传的 mints
127
+ - 上一次请求没完成时不会发起下一次;新订阅者加入时如果已经有结果,立即收到一次
128
+
129
+ ## 代币信息缓存持久化
130
+
131
+ decimals / 所属程序永久缓存、name / symbol 缓存 1 小时,默认只在内存里。刷新页面后不想重查 decimals 时:
132
+
133
+ ```ts
134
+ import { persistTokenMetaCache } from '@w3lib/solana-batch-call'
135
+
136
+ const stop = persistTokenMetaCache(localStorage) // 读入上次保存的,之后有变化约 1 秒后写回(最多 5000 条,可用 maxEntries 调整)
137
+ ```
138
+
139
+ 也可以自己存:`exportTokenMetaCache()` 返回可以 `JSON.stringify` 的快照,`importTokenMetaCache(snapshot)` 导入(格式不对的条目跳过,已有的不覆盖)。只包含按网络共享的条目(自定义节点且没指定 `cluster` 的客户端不导出)。
140
+
141
+ ## 转账手续费(Token-2022)
142
+
143
+ ```ts
144
+ const [token] = await getTokens([mint], { fields: ['decimals', 'transferFee'] })
145
+ token.transferFee
146
+ // { basisPoints: 50, maximumFee: '5000', epoch: '100', older: { … }, newer: { basisPoints: 200, maximumFee: '9000', epoch: '600' } }
147
+ ```
148
+
149
+ 顶层是当前 epoch 生效的配置(请求时顺带发一次 `getEpochInfo`,与账户读取在同一个批量请求里),`older` / `newer` 是扩展里的两套原始配置。SPL Token、没有这个扩展、主币为 `null`(不算失败)。实际到账 = 数量 − min(⌈数量 × basisPoints / 10000⌉, maximumFee)。
150
+
151
+ ## 失败原因
152
+
153
+ 各函数的单项结果在 `success: false` 时带 `error`(类型 `FailureReason`),成功时不带:
154
+
155
+ | `error` | 含义 | 出现在 |
156
+ | --- | --- | --- |
157
+ | `invalid-address` | 地址格式非法(不是 base58 的 32 字节) | 全部 |
158
+ | `not-found` | 地址合法,但链上没有这个账户(如 mint 不存在) | `getBalances` / `getTokens` / `getNfts` / `getNftOwners` |
159
+ | `not-token` | 账户存在,但不是代币 mint | 同上 |
160
+ | `no-metadata` | 代币存在,但没有元数据 | `getNfts` |
161
+ | `no-holder` | 没有持有人(供应量为 0 或已销毁) | `getNftOwners` |
162
+ | `missing-field` | 代币存在,但请求的字段读不到;`errorField` 是第一个读不到的字段(如没有元数据时的 `name`) | `getTokens` |
163
+
164
+ ```ts
165
+ const [item] = await getBalances(owner, [mint])
166
+ if (item.error === 'invalid-address') throw new Error('bad mint') // 参数错误
167
+ if (item.error === 'not-found') { /* 按 0 处理 */ }
168
+ ```
169
+
170
+ ## 节点
171
+
172
+ ```ts
173
+ import { SolanaClient } from '@w3lib/solana-batch-call'
174
+
175
+ new SolanaClient() // 内置 mainnet 公共节点
176
+ new SolanaClient(undefined, { cluster: 'devnet' }) // 内置 devnet 节点
177
+ new SolanaClient('https://my-rpc.example') // 自己的节点
178
+ new SolanaClient(['https://rpc-1.example', 'https://rpc-2.example']) // 主节点 + 备用节点
179
+ new SolanaClient(connection) // @solana/web3.js 的 Connection(使用其 rpcEndpoint)
180
+ new SolanaClient({ request: (method, params) => myRpc(method, params) }) // 自定义传输
181
+
182
+ // 独立函数的节点参数相同
183
+ await getBalances(owner, mints, { provider: 'https://my-rpc.example', cluster: 'mainnet' })
184
+ ```
185
+
186
+ **内置公共节点**(`DEFAULT_RPC_URLS`):mainnet 依次是 publicnode 和官方节点。2026-10 实测:
187
+
188
+ - publicnode:放行跨域、支持批量请求,但不支持 `getTokenAccountsByOwner`、`getTokenLargestAccounts` 等需要索引的方法
189
+ - 官方节点 `api.mainnet-beta.solana.com`:对带浏览器 `Origin` 的请求返回 403,只能在服务端使用;限频较严
190
+
191
+ 所以扫描持仓、`getNftOwners`、`getOwnerNfts` 在内置节点上可能不可用,生产环境建议传入自己的节点。
192
+
193
+ **传输层行为**(`HttpRpc`):
194
+
195
+ - 收集窗口(`batchWait`,默认 0 即同一 tick)内的调用合并成一个批量请求,最多 `maxBatchSize`(默认 20)条
196
+ - 节点不支持批量请求时自动改为逐条并发;返回 “Batch of more than N” 时按 N 缩小批量;请求体过大(413)时批量减半。批量请求被限频或拒绝访问时不会降级(降级只会让限频更严重)
197
+ - HTTP 429 和 JSON-RPC 错误码 429 都会退避重试(`retries`,默认 2;传入多个自定义节点时为 0,直接换节点;内置节点保留重试),批量里只重试被限频的那几条
198
+ - 单次请求超时 `timeout`,默认 10 秒
199
+
200
+ **故障切换**(多节点时):网络错误、超时、限频、403、需要 API Key、请求被拦截、高度不到 `minContextSlot` 等节点问题换下一个节点;参数错误等确定性错误直接抛出(`RpcError`,`isNodeFault(err)` 可判断)。
201
+
202
+ - 出错的节点冷却 `cooldown`(默认 30 秒),期间排到最后。**超时不冷却**(大批量查询慢一点很正常),同一节点连续 3 次超时才冷却;高度不够也不冷却
203
+ - 节点健康状态全局共享:URL 相同即同一个节点(其他节点按对象),客户端重建后刚出过错的节点仍排在后面
204
+ - 所有节点都失败时抛 `AllNodesFailedError`,`err.errors` 是 `{ node, error }[]`;单个节点时抛原始错误
205
+ - 节点名称(`node`)只取 URL 的 host,报错信息里的地址也只保留 origin,不会带出 path / query 里的 API Key;自定义传输可以设 `label`
206
+
207
+ **可观测性**:`onRequest(listener)` 监听所有客户端发出的调用,返回取消函数:
208
+
209
+ ```ts
210
+ import { onRequest } from '@w3lib/solana-batch-call'
211
+
212
+ const stop = onRequest(({ node, method, ms, ok, error, attempt }) => {
213
+ metrics.record({ node, method, ms, ok }) // attempt > 0 表示这次调用换过节点
214
+ })
215
+ ```
216
+
217
+ 每个 JSON-RPC 调用对每个节点的每次尝试一条(合并成批量 HTTP 请求时,批里每个调用各一条,`ms` 是该调用从发出到返回的时间);单节点时也会触发。监听函数出错不影响请求。
218
+
219
+ **网络校验**:传了 `cluster`,或传了多个节点时,会用 `getGenesisHash` 确认每个节点所在的网络,不一致时报 `NetworkMismatchError`(多节点时换下一个节点)。校验与请求并行发出,结果按节点缓存,每个节点只多一次请求;内置节点不校验。`await client.getCluster()` 返回节点所在网络。
220
+
221
+ ## 配置
222
+
223
+ ```ts
224
+ new SolanaClient(provider?, {
225
+ cluster: 'mainnet', // 'mainnet' | 'devnet' | 'testnet'
226
+ commitment: 'confirmed', // 'processed' | 'confirmed' | 'finalized'
227
+ nativeMints: [NATIVE_MINT, SYSTEM_PROGRAM_ID],
228
+ timeout: 10_000,
229
+ maxBatchSize: 20,
230
+ batchWait: 0,
231
+ batch: true, // false:不合并成批量请求
232
+ retries: 2,
233
+ cooldown: 30_000,
234
+ headers: { 'x-api-key': '…' },
235
+ fetch: customFetch, // 需要代理等场景
236
+ })
237
+ ```
238
+
239
+ 在 Node 里通过代理访问节点时:Node 自带的 `fetch` 默认不读 `HTTPS_PROXY`,Node 24+ 可以设置 `NODE_USE_ENV_PROXY=1`,或者通过 `fetch` 选项传入自定义实现。
240
+
241
+ ## 工具函数
242
+
243
+ ```ts
244
+ import {
245
+ getAssociatedTokenAddress, // ATA 地址
246
+ getMetadataAddress, // Metaplex 元数据账户地址
247
+ findProgramAddress, // 通用 PDA 推导(与 web3.js findProgramAddressSync 相同)
248
+ isAddress,
249
+ isOnCurve, // 32 字节是否是 ed25519 曲线上的点(与 web3.js PublicKey.isOnCurve 相同)
250
+ parseMint, parseTokenAccount, parseMetaplexMetadata, // 账户数据解析
251
+ parseTransferFeeConfig, // Token-2022 转账手续费扩展
252
+ formatUnits, // 按精度换算:formatUnits(1_500_000_000n, 9) === '1.5'(formatAmount 是同一个函数的旧名字,已不推荐)
253
+ TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, NATIVE_MINT, GENESIS_HASHES,
254
+ } from '@w3lib/solana-batch-call'
255
+ ```
256
+
257
+ ## 运行环境
258
+
259
+ Node 20.19+(依赖的 `@noble/hashes` 2.x 要求)、现代浏览器。
260
+
261
+ ## 开发
262
+
263
+ ```bash
264
+ pnpm test # 单元测试(模拟节点,不需要网络)
265
+ pnpm test:live # 连真实节点(SOLANA_RPC 指定支持索引方法的节点;需要代理时加 NODE_USE_ENV_PROXY=1)
266
+ pnpm build
267
+ ```
268
+
269
+ ## License
270
+
271
+ MIT
@@ -0,0 +1,20 @@
1
+ /** 是否是合法的 Solana 地址(base58,32 字节) */
2
+ export declare function isAddress(value: unknown): value is string;
3
+ export declare function decodeAddress(address: string): Uint8Array;
4
+ export declare function encodeAddress(bytes: Uint8Array): string;
5
+ /**
6
+ * 32 字节是否是合法的 ed25519 压缩点(PDA 必须不在曲线上)。
7
+ * 按 RFC 8032 5.1.3 解压缩,与 @noble/curves 的 Point.fromBytes(web3.js 的 isOnCurve 同样如此)一致:
8
+ * y ≥ p、x = 0 但符号位为 1 都算不在曲线上。只需判断 x² = (y² - 1) / (d·y² + 1) 有没有解,不必真的开方
9
+ */
10
+ export declare function isOnCurve(bytes: Uint8Array): boolean;
11
+ /**
12
+ * 推导 PDA(与 web3.js PublicKey.findProgramAddressSync 相同),返回地址。
13
+ * seeds 每项不超过 32 字节。
14
+ */
15
+ export declare function findProgramAddress(seeds: Uint8Array[], programId: string): string;
16
+ /** 关联代币账户(ATA)地址 */
17
+ export declare function getAssociatedTokenAddress(owner: string, mint: string, tokenProgram: string): string;
18
+ /** Metaplex Token Metadata 账户地址 */
19
+ export declare function getMetadataAddress(mint: string): string;
20
+ //# sourceMappingURL=address.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAKA,qCAAqC;AACrC,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CASzD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAMzD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAEvD;AA4BD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAoBpD;AAiBD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAmBjF;AAED,oBAAoB;AACpB,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEnG;AAED,mCAAmC;AACnC,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKvD"}
@@ -0,0 +1,120 @@
1
+ import { sha256 } from '@noble/hashes/sha2.js';
2
+ import { base58 } from '@scure/base';
3
+ import { ASSOCIATED_TOKEN_PROGRAM_ID, METADATA_PROGRAM_ID } from './constants.js';
4
+ /** 是否是合法的 Solana 地址(base58,32 字节) */
5
+ export function isAddress(value) {
6
+ if (typeof value !== 'string' || value.length < 32 || value.length > 44) {
7
+ return false;
8
+ }
9
+ try {
10
+ return base58.decode(value).length === 32;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ export function decodeAddress(address) {
17
+ const bytes = base58.decode(address);
18
+ if (bytes.length !== 32) {
19
+ throw new Error(`Invalid Solana address: ${address}`);
20
+ }
21
+ return bytes;
22
+ }
23
+ export function encodeAddress(bytes) {
24
+ return base58.encode(bytes);
25
+ }
26
+ // ed25519:-x² + y² = 1 + d·x²·y²(mod p)
27
+ const P = 2n ** 255n - 19n;
28
+ const D = 37095705934669439343138083508754565189542113879843219016388785533085940283555n;
29
+ /** x^(2^k) mod p */
30
+ function pow2k(x, k) {
31
+ let r = x;
32
+ for (let i = 0; i < k; i++)
33
+ r = (r * r) % P;
34
+ return r;
35
+ }
36
+ /** x^((p-1)/2) mod p(欧拉判别法),(p-1)/2 = (2^250 - 1)·2^4 + 6,用加法链:约 254 次平方 + 12 次乘法 */
37
+ function legendre(x) {
38
+ const b2 = (((x * x) % P) * x) % P; // x^(2^2-1)
39
+ const b4 = (pow2k(b2, 2) * b2) % P;
40
+ const b5 = (pow2k(b4, 1) * x) % P;
41
+ const b10 = (pow2k(b5, 5) * b5) % P;
42
+ const b20 = (pow2k(b10, 10) * b10) % P;
43
+ const b40 = (pow2k(b20, 20) * b20) % P;
44
+ const b80 = (pow2k(b40, 40) * b40) % P;
45
+ const b160 = (pow2k(b80, 80) * b80) % P;
46
+ const b240 = (pow2k(b160, 80) * b80) % P;
47
+ const b250 = (pow2k(b240, 10) * b10) % P;
48
+ return (pow2k(b250, 4) * ((b2 * b2) % P)) % P;
49
+ }
50
+ /**
51
+ * 32 字节是否是合法的 ed25519 压缩点(PDA 必须不在曲线上)。
52
+ * 按 RFC 8032 5.1.3 解压缩,与 @noble/curves 的 Point.fromBytes(web3.js 的 isOnCurve 同样如此)一致:
53
+ * y ≥ p、x = 0 但符号位为 1 都算不在曲线上。只需判断 x² = (y² - 1) / (d·y² + 1) 有没有解,不必真的开方
54
+ */
55
+ export function isOnCurve(bytes) {
56
+ if (bytes.length !== 32) {
57
+ return false;
58
+ }
59
+ let y = 0n;
60
+ for (let i = 31; i >= 0; i--) {
61
+ y = (y << 8n) | BigInt(i === 31 ? bytes[i] & 0x7f : bytes[i]);
62
+ }
63
+ if (y >= P) {
64
+ return false;
65
+ }
66
+ const y2 = (y * y) % P;
67
+ const u = (y2 - 1n + P) % P;
68
+ const v = (D * y2 + 1n) % P; // d 不是平方数,v 不会为 0
69
+ if (u === 0n) {
70
+ // x = 0:符号位必须为 0
71
+ return (bytes[31] & 0x80) === 0;
72
+ }
73
+ // u / v 是平方数 ⇔ u·v 是平方数(欧拉判别法)
74
+ return legendre((u * v) % P) === 1n;
75
+ }
76
+ const PDA_MARKER = new TextEncoder().encode('ProgramDerivedAddress');
77
+ function concat(parts) {
78
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
79
+ let offset = 0;
80
+ for (const part of parts) {
81
+ out.set(part, offset);
82
+ offset += part.length;
83
+ }
84
+ return out;
85
+ }
86
+ // PDA 推导是纯计算但要做多次 sha256 + 曲线判断,结果缓存
87
+ const pdaCache = new Map();
88
+ /**
89
+ * 推导 PDA(与 web3.js PublicKey.findProgramAddressSync 相同),返回地址。
90
+ * seeds 每项不超过 32 字节。
91
+ */
92
+ export function findProgramAddress(seeds, programId) {
93
+ const key = `${programId}:${seeds.map((s) => encodeAddress(s)).join(',')}`;
94
+ const cached = pdaCache.get(key);
95
+ if (cached) {
96
+ return cached;
97
+ }
98
+ const program = decodeAddress(programId);
99
+ for (let bump = 255; bump >= 0; bump--) {
100
+ const hash = sha256(concat([...seeds, new Uint8Array([bump]), program, PDA_MARKER]));
101
+ if (!isOnCurve(hash)) {
102
+ const address = encodeAddress(hash);
103
+ if (pdaCache.size > 10_000) {
104
+ pdaCache.clear();
105
+ }
106
+ pdaCache.set(key, address);
107
+ return address;
108
+ }
109
+ }
110
+ throw new Error('Unable to find a viable program address bump seed');
111
+ }
112
+ /** 关联代币账户(ATA)地址 */
113
+ export function getAssociatedTokenAddress(owner, mint, tokenProgram) {
114
+ return findProgramAddress([decodeAddress(owner), decodeAddress(tokenProgram), decodeAddress(mint)], ASSOCIATED_TOKEN_PROGRAM_ID);
115
+ }
116
+ /** Metaplex Token Metadata 账户地址 */
117
+ export function getMetadataAddress(mint) {
118
+ return findProgramAddress([new TextEncoder().encode('metadata'), decodeAddress(METADATA_PROGRAM_ID), decodeAddress(mint)], METADATA_PROGRAM_ID);
119
+ }
120
+ //# sourceMappingURL=address.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.js","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"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"}