@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
@@ -0,0 +1,41 @@
1
+ export const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111'
2
+ export const TOKEN_PROGRAM_ID = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
3
+ export const TOKEN_2022_PROGRAM_ID = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'
4
+ export const ASSOCIATED_TOKEN_PROGRAM_ID = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'
5
+ export const METADATA_PROGRAM_ID = 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'
6
+
7
+ /** Wrapped SOL 的 mint,按惯例也用来表示原生 SOL */
8
+ export const NATIVE_MINT = 'So11111111111111111111111111111111111111112'
9
+
10
+ /** 原生 SOL 的精度(lamports) */
11
+ export const SOL_DECIMALS = 9
12
+
13
+ export type Cluster = 'mainnet' | 'devnet' | 'testnet'
14
+
15
+ /** 各网络的创世区块哈希(getGenesisHash),用于识别节点所在网络 */
16
+ export const GENESIS_HASHES: Readonly<Record<Cluster, string>> = {
17
+ mainnet: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d',
18
+ devnet: 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG',
19
+ testnet: '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY',
20
+ }
21
+
22
+ export function clusterOfGenesis(hash: string): Cluster | null {
23
+ for (const [cluster, genesis] of Object.entries(GENESIS_HASHES)) {
24
+ if (genesis === hash) {
25
+ return cluster as Cluster
26
+ }
27
+ }
28
+ return null
29
+ }
30
+
31
+ /**
32
+ * 内置公共节点(按优先级排序,2026-10 实测):
33
+ * - publicnode:放行跨域、支持批量请求,但不支持 getTokenAccountsByOwner 等需要索引的方法
34
+ * - 官方节点:对带浏览器 Origin 的请求返回 403,只能在服务端使用
35
+ * 公共节点限频严,生产环境建议传入自己的节点(Helius / QuickNode / Triton 等)。
36
+ */
37
+ export const DEFAULT_RPC_URLS: Readonly<Record<Cluster, readonly string[]>> = {
38
+ mainnet: ['https://solana-rpc.publicnode.com', 'https://api.mainnet-beta.solana.com'],
39
+ devnet: ['https://api.devnet.solana.com'],
40
+ testnet: ['https://api.testnet.solana.com'],
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,98 @@
1
+ export {
2
+ SolanaClient,
3
+ exportTokenMetaCache,
4
+ importTokenMetaCache,
5
+ persistTokenMetaCache,
6
+ type AccountsOptions,
7
+ type BalanceQuery,
8
+ type BalancesOptions,
9
+ type ClientConfig,
10
+ type OwnerTokensOptions,
11
+ type PersistTokenMetaOptions,
12
+ type RequestOptions,
13
+ type SlotOptions,
14
+ type SolBalancesOptions,
15
+ type TokenMetaSnapshot,
16
+ type TokensOptions,
17
+ } from './client.js'
18
+ export {
19
+ getBalances,
20
+ getMultiBalances,
21
+ getNftOwners,
22
+ getNfts,
23
+ getOwnerNfts,
24
+ getOwnerTokens,
25
+ getSolBalances,
26
+ getTokens,
27
+ watchBalances,
28
+ type GetBalancesOptions,
29
+ type GetNftsOptions,
30
+ type GetOwnerTokensOptions,
31
+ type GetSolBalancesOptions,
32
+ type GetTokensOptions,
33
+ type WatchBalancesOptions,
34
+ type ShortcutOptions,
35
+ } from './shortcuts.js'
36
+ export {
37
+ DEFAULT_TOKEN_FIELDS,
38
+ type Commitment,
39
+ type DefaultTokenField,
40
+ type FailureReason,
41
+ type NftDetails,
42
+ type NftOwner,
43
+ type OwnedToken,
44
+ type SolBalance,
45
+ type TokenBalance,
46
+ type TokenDetails,
47
+ type TokenField,
48
+ type TokenProgramName,
49
+ type TokenProgramOption,
50
+ type TokenStandard,
51
+ type TransferFee,
52
+ type TransferFeeConfig,
53
+ } from './types.js'
54
+ export {
55
+ ASSOCIATED_TOKEN_PROGRAM_ID,
56
+ DEFAULT_RPC_URLS,
57
+ GENESIS_HASHES,
58
+ METADATA_PROGRAM_ID,
59
+ NATIVE_MINT,
60
+ SOL_DECIMALS,
61
+ SYSTEM_PROGRAM_ID,
62
+ TOKEN_2022_PROGRAM_ID,
63
+ TOKEN_PROGRAM_ID,
64
+ clusterOfGenesis,
65
+ type Cluster,
66
+ } from './constants.js'
67
+ export { decodeAddress, encodeAddress, findProgramAddress, getAssociatedTokenAddress, getMetadataAddress, isAddress, isOnCurve } from './address.js'
68
+ export {
69
+ formatAmount,
70
+ formatUnits,
71
+ parseMetaplexMetadata,
72
+ parseMint,
73
+ parseTokenAccount,
74
+ parseTransferFeeConfig,
75
+ type AccountInfo,
76
+ type MetaplexMetadata,
77
+ type MintInfo,
78
+ type TokenAccountInfo,
79
+ type TokenMetadata,
80
+ type TransferFeeConfigInfo,
81
+ type TransferFeeInfo,
82
+ } from './layout.js'
83
+ export {
84
+ AllNodesFailedError,
85
+ FallbackRpc,
86
+ HttpError,
87
+ HttpRpc,
88
+ MIN_CONTEXT_SLOT_NOT_REACHED,
89
+ RpcError,
90
+ isNodeFault,
91
+ nodeLabel,
92
+ onRequest,
93
+ type FallbackOptions,
94
+ type HttpRpcOptions,
95
+ type RequestEvent,
96
+ type RpcTransport,
97
+ } from './rpc.js'
98
+ export { NetworkCheckedRpc, NetworkMismatchError, type RpcSource, type SourceOptions } from './source.js'
package/src/layout.ts ADDED
@@ -0,0 +1,320 @@
1
+ import { encodeAddress } from './address.js'
2
+
3
+ /** getMultipleAccounts 解析后的账户 */
4
+ export interface AccountInfo {
5
+ address: string
6
+ lamports: bigint
7
+ /** 所属程序 */
8
+ owner: string
9
+ data: Uint8Array
10
+ executable: boolean
11
+ }
12
+
13
+ class Reader {
14
+ #offset: number
15
+ readonly #view: DataView
16
+
17
+ constructor(
18
+ readonly bytes: Uint8Array,
19
+ offset = 0,
20
+ ) {
21
+ this.#offset = offset
22
+ this.#view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
23
+ }
24
+
25
+ get offset(): number {
26
+ return this.#offset
27
+ }
28
+
29
+ #need(n: number): void {
30
+ if (this.#offset + n > this.bytes.length) {
31
+ throw new RangeError('Account data is shorter than expected')
32
+ }
33
+ }
34
+
35
+ skip(n: number): void {
36
+ this.#need(n)
37
+ this.#offset += n
38
+ }
39
+
40
+ u8(): number {
41
+ this.#need(1)
42
+ return this.bytes[this.#offset++] as number
43
+ }
44
+
45
+ u16(): number {
46
+ this.#need(2)
47
+ const v = this.#view.getUint16(this.#offset, true)
48
+ this.#offset += 2
49
+ return v
50
+ }
51
+
52
+ u32(): number {
53
+ this.#need(4)
54
+ const v = this.#view.getUint32(this.#offset, true)
55
+ this.#offset += 4
56
+ return v
57
+ }
58
+
59
+ u64(): bigint {
60
+ this.#need(8)
61
+ const v = this.#view.getBigUint64(this.#offset, true)
62
+ this.#offset += 8
63
+ return v
64
+ }
65
+
66
+ pubkey(): string {
67
+ this.#need(32)
68
+ const v = encodeAddress(this.bytes.slice(this.#offset, this.#offset + 32))
69
+ this.#offset += 32
70
+ return v
71
+ }
72
+
73
+ /** borsh string:u32 长度 + UTF-8;去掉 Metaplex 用来补齐长度的 \0 */
74
+ string(): string {
75
+ const len = this.u32()
76
+ this.#need(len)
77
+ const v = new TextDecoder().decode(this.bytes.slice(this.#offset, this.#offset + len))
78
+ this.#offset += len
79
+ return v.replace(/\0+$/, '').trim()
80
+ }
81
+
82
+ option<T>(read: () => T): T | null {
83
+ return this.u8() === 1 ? read() : null
84
+ }
85
+ }
86
+
87
+ export interface MintInfo {
88
+ mintAuthority: string | null
89
+ supply: bigint
90
+ decimals: number
91
+ isInitialized: boolean
92
+ freezeAuthority: string | null
93
+ /** Token-2022 TokenMetadata 扩展(如有) */
94
+ metadata: TokenMetadata | null
95
+ }
96
+
97
+ export interface TokenMetadata {
98
+ name: string
99
+ symbol: string
100
+ uri: string
101
+ }
102
+
103
+ const MINT_SIZE = 82
104
+ const ACCOUNT_SIZE = 165
105
+ /** Token-2022 带扩展的账户:基础布局补齐到 165(Account 的大小),第 165 字节是账户类型,之后是扩展 */
106
+ const ACCOUNT_TYPE_OFFSET = 165
107
+ const EXTENSIONS_OFFSET = 166
108
+ const ACCOUNT_TYPE_MINT = 1
109
+ const ACCOUNT_TYPE_ACCOUNT = 2
110
+ const EXTENSION_TRANSFER_FEE_CONFIG = 1
111
+ const EXTENSION_TOKEN_METADATA = 19
112
+
113
+ /** 解析 Mint 账户(SPL Token / Token-2022 通用,Token-2022 额外解析 TokenMetadata 扩展) */
114
+ export function parseMint(data: Uint8Array): MintInfo {
115
+ // 只接受真正的 mint:SPL Token 恰好 82 字节;Token-2022 带扩展时账户类型必须是 Mint(避免把代币账户 / 多签账户当成 mint)
116
+ if (data.length !== MINT_SIZE && !(data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_MINT)) {
117
+ throw new RangeError('Not a mint account')
118
+ }
119
+ const r = new Reader(data)
120
+ const hasMintAuthority = r.u32() === 1
121
+ const mintAuthority = r.pubkey()
122
+ const supply = r.u64()
123
+ const decimals = r.u8()
124
+ const isInitialized = r.u8() === 1
125
+ const hasFreezeAuthority = r.u32() === 1
126
+ const freezeAuthority = r.pubkey()
127
+ return {
128
+ mintAuthority: hasMintAuthority ? mintAuthority : null,
129
+ supply,
130
+ decimals,
131
+ isInitialized,
132
+ freezeAuthority: hasFreezeAuthority ? freezeAuthority : null,
133
+ metadata: data.length > EXTENSIONS_OFFSET ? parseTokenMetadataExtension(data) : null,
134
+ }
135
+ }
136
+
137
+ /** 在 Token-2022 账户的扩展(TLV:u16 类型 + u16 长度 + 数据)里找指定类型,返回其数据;没有或数据不完整时为 null */
138
+ function findExtension(data: Uint8Array, extensionType: number): Uint8Array | null {
139
+ if (data.length <= EXTENSIONS_OFFSET) {
140
+ return null
141
+ }
142
+ try {
143
+ const r = new Reader(data, EXTENSIONS_OFFSET)
144
+ while (r.offset + 4 <= data.length) {
145
+ const type = r.u16()
146
+ const length = r.u16()
147
+ if (type === 0 && length === 0) {
148
+ return null // 未初始化的填充
149
+ }
150
+ if (type === extensionType) {
151
+ if (r.offset + length > data.length) {
152
+ return null
153
+ }
154
+ return data.slice(r.offset, r.offset + length)
155
+ }
156
+ r.skip(length)
157
+ }
158
+ } catch {
159
+ // 扩展数据不完整:当作没有
160
+ }
161
+ return null
162
+ }
163
+
164
+ function parseTokenMetadataExtension(data: Uint8Array): TokenMetadata | null {
165
+ const ext = findExtension(data, EXTENSION_TOKEN_METADATA)
166
+ if (!ext) {
167
+ return null
168
+ }
169
+ try {
170
+ const m = new Reader(ext)
171
+ m.pubkey() // updateAuthority
172
+ m.pubkey() // mint
173
+ return { name: m.string(), symbol: m.string(), uri: m.string() }
174
+ } catch {
175
+ return null
176
+ }
177
+ }
178
+
179
+ /** Token-2022 转账手续费的一套配置:从 epoch 开始生效,费率 basisPoints(万分比),单笔最多 maximumFee(最小单位) */
180
+ export interface TransferFeeInfo {
181
+ epoch: bigint
182
+ maximumFee: bigint
183
+ basisPoints: number
184
+ }
185
+
186
+ /** Token-2022 TransferFeeConfig 扩展 */
187
+ export interface TransferFeeConfigInfo {
188
+ /** 可以修改手续费的地址;null 表示不能再改 */
189
+ transferFeeConfigAuthority: string | null
190
+ withdrawWithheldAuthority: string | null
191
+ /** mint 上暂存的、已收取未提取的手续费 */
192
+ withheldAmount: bigint
193
+ /** 旧配置:当前 epoch < newer.epoch 时生效 */
194
+ older: TransferFeeInfo
195
+ /** 新配置:从 newer.epoch 起生效 */
196
+ newer: TransferFeeInfo
197
+ }
198
+
199
+ const ZERO_PUBKEY = '11111111111111111111111111111111'
200
+
201
+ /**
202
+ * 解析 mint 账户里的 Token-2022 TransferFeeConfig 扩展(类型 1,108 字节);
203
+ * SPL Token、没有这个扩展或数据不完整时为 null。
204
+ * 当前生效的是 `epoch >= newer.epoch ? newer : older`
205
+ */
206
+ export function parseTransferFeeConfig(data: Uint8Array): TransferFeeConfigInfo | null {
207
+ if (!(data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_MINT)) {
208
+ return null
209
+ }
210
+ const ext = findExtension(data, EXTENSION_TRANSFER_FEE_CONFIG)
211
+ if (!ext) {
212
+ return null
213
+ }
214
+ try {
215
+ const r = new Reader(ext)
216
+ // OptionalNonZeroPubkey:全 0 表示没有
217
+ const configAuthority = r.pubkey()
218
+ const withdrawAuthority = r.pubkey()
219
+ const withheldAmount = r.u64()
220
+ const fee = (): TransferFeeInfo => ({ epoch: r.u64(), maximumFee: r.u64(), basisPoints: r.u16() })
221
+ const older = fee()
222
+ const newer = fee()
223
+ return {
224
+ transferFeeConfigAuthority: configAuthority === ZERO_PUBKEY ? null : configAuthority,
225
+ withdrawWithheldAuthority: withdrawAuthority === ZERO_PUBKEY ? null : withdrawAuthority,
226
+ withheldAmount,
227
+ older,
228
+ newer,
229
+ }
230
+ } catch {
231
+ return null
232
+ }
233
+ }
234
+
235
+ export interface TokenAccountInfo {
236
+ mint: string
237
+ owner: string
238
+ amount: bigint
239
+ }
240
+
241
+ /** 解析代币账户(SPL Token / Token-2022 前 72 字节相同) */
242
+ export function parseTokenAccount(data: Uint8Array): TokenAccountInfo {
243
+ if (data.length !== ACCOUNT_SIZE && !(data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_ACCOUNT)) {
244
+ throw new RangeError('Not a token account')
245
+ }
246
+ const r = new Reader(data)
247
+ return { mint: r.pubkey(), owner: r.pubkey(), amount: r.u64() }
248
+ }
249
+
250
+ export interface MetaplexMetadata extends TokenMetadata {
251
+ updateAuthority: string
252
+ mint: string
253
+ sellerFeeBasisPoints: number
254
+ creators: Array<{ address: string; verified: boolean; share: number }>
255
+ primarySaleHappened: boolean
256
+ isMutable: boolean
257
+ /** 0 NonFungible,1 FungibleAsset,2 Fungible,3 NonFungibleEdition,4 ProgrammableNonFungible,5 ProgrammableNonFungibleEdition */
258
+ tokenStandard: number | null
259
+ collection: { address: string; verified: boolean } | null
260
+ }
261
+
262
+ /** 解析 Metaplex Token Metadata 账户;老版本账户缺少后面的字段时,缺的部分为默认值 */
263
+ export function parseMetaplexMetadata(data: Uint8Array): MetaplexMetadata {
264
+ const r = new Reader(data)
265
+ if (r.u8() !== 4) {
266
+ throw new RangeError('Not a Metaplex metadata account') // Key::MetadataV1 = 4
267
+ }
268
+ const updateAuthority = r.pubkey()
269
+ const mint = r.pubkey()
270
+ const name = r.string()
271
+ const symbol = r.string()
272
+ const uri = r.string()
273
+ const sellerFeeBasisPoints = r.u16()
274
+ const creators =
275
+ r.option(() => {
276
+ const count = r.u32()
277
+ return Array.from({ length: count }, () => ({ address: r.pubkey(), verified: r.u8() === 1, share: r.u8() }))
278
+ }) ?? []
279
+ const result: MetaplexMetadata = {
280
+ updateAuthority,
281
+ mint,
282
+ name,
283
+ symbol,
284
+ uri,
285
+ sellerFeeBasisPoints,
286
+ creators,
287
+ primarySaleHappened: false,
288
+ isMutable: false,
289
+ tokenStandard: null,
290
+ collection: null,
291
+ }
292
+ try {
293
+ result.primarySaleHappened = r.u8() === 1
294
+ result.isMutable = r.u8() === 1
295
+ r.option(() => r.u8()) // editionNonce
296
+ result.tokenStandard = r.option(() => r.u8())
297
+ result.collection = r.option(() => {
298
+ const verified = r.u8() === 1
299
+ return { address: r.pubkey(), verified }
300
+ })
301
+ } catch {
302
+ // 老版本账户没有这些字段
303
+ }
304
+ return result
305
+ }
306
+
307
+ /** 按精度格式化(与 ethers 的 formatUnits 同名同义):formatUnits(1_500_000_000n, 9) === '1.5';不四舍五入,整数不带小数点 */
308
+ export function formatUnits(value: bigint, decimals: number): string {
309
+ const negative = value < 0n
310
+ const digits = (negative ? -value : value).toString().padStart(decimals + 1, '0')
311
+ const whole = digits.slice(0, digits.length - decimals)
312
+ const fraction = digits.slice(digits.length - decimals).replace(/0+$/, '')
313
+ return `${negative ? '-' : ''}${whole}${fraction ? `.${fraction}` : ''}`
314
+ }
315
+
316
+ /**
317
+ * 同 formatUnits。
318
+ * @deprecated 改用 formatUnits:formatAmount 这个名字容易和项目里的数字展示函数撞名
319
+ */
320
+ export const formatAmount = formatUnits