@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,155 @@
1
+ export type Commitment = 'processed' | 'confirmed' | 'finalized';
2
+ /**
3
+ * tokenPrograms 参数的取值:简写 'spl'(SPL Token)/ 'token-2022',也可以直接传程序地址
4
+ * (TOKEN_PROGRAM_ID / TOKEN_2022_PROGRAM_ID)
5
+ */
6
+ export type TokenProgramName = 'spl' | 'token-2022';
7
+ export type TokenProgramOption = TokenProgramName | (string & {});
8
+ /**
9
+ * 单项失败(success: false)的原因:
10
+ * - invalid-address:地址格式非法(不是 base58 的 32 字节)
11
+ * - not-found:地址合法,但链上没有这个账户(如 mint 不存在)
12
+ * - not-token:账户存在,但不是代币 mint(或代币账户)
13
+ * - no-metadata:代币存在,但没有元数据(nfts)
14
+ * - no-holder:没有持有人(nftOwners:供应量为 0 或已销毁)
15
+ * - missing-field:代币存在,但请求的字段读不到(tokens),见 errorField
16
+ */
17
+ export type FailureReason = 'invalid-address' | 'not-found' | 'not-token' | 'no-metadata' | 'no-holder' | 'missing-field';
18
+ /** balances() 的单项结果,字段都是字符串 / 数字 / 布尔 / null,可以直接 JSON.stringify */
19
+ export interface TokenBalance {
20
+ /** 代币的 mint(原样) */
21
+ token: string;
22
+ /** 是否按原生 SOL 查询 */
23
+ native: boolean;
24
+ /** 余额(最小单位的十进制字符串);需要计算时用 BigInt(balance) */
25
+ balance: string;
26
+ decimals: number;
27
+ /** 按 decimals 换算后的十进制字符串,如 "1234.5";整数不带小数点 */
28
+ formatted: string;
29
+ /** 代币所属程序(SPL Token / Token-2022);主币或未知时为 null */
30
+ tokenProgram: string | null;
31
+ /** 仅在 symbol: true 时返回;读不到时为 null */
32
+ symbol?: string | null;
33
+ /** 查询失败(mint 不存在、地址非法等)时为 false,balance / formatted 为 "0" */
34
+ success: boolean;
35
+ /** 失败原因,仅 success 为 false 时返回 */
36
+ error?: FailureReason;
37
+ /** 传了 withSlot / minContextSlot 时返回:读取时节点的 slot(一次查询发了多个请求时取最小的) */
38
+ slot?: number;
39
+ }
40
+ /** ownerTokens() / getOwnerTokens() 的单项结果(总是 success: true) */
41
+ export interface OwnedToken extends Omit<TokenBalance, 'symbol' | 'error'> {
42
+ /** 该代币的代币账户数量;大于 1 时 balance 为所有账户的合计 */
43
+ accounts: number;
44
+ /** metadata 为 true(默认)时返回;没有元数据或读取失败时为 null(见 metadataStatus) */
45
+ name?: string | null;
46
+ symbol?: string | null;
47
+ /** metadata 为 true 时返回:ok 读到了;missing 确认没有元数据;failed 节点问题没能读到(可以稍后单独用 getTokens 重查) */
48
+ metadataStatus?: 'ok' | 'missing' | 'failed';
49
+ }
50
+ export interface SolBalance {
51
+ address: string;
52
+ /** lamports(十进制字符串) */
53
+ balance: string;
54
+ /** 按 9 位精度换算后的 SOL */
55
+ formatted: string;
56
+ success: boolean;
57
+ /** 失败原因(只有 invalid-address),仅 success 为 false 时返回 */
58
+ error?: FailureReason;
59
+ /** 传了 withSlot / minContextSlot 时返回:读取时节点的 slot(多个请求时取最小的);没有发请求(地址全部非法)时不返回 */
60
+ slot?: number;
61
+ }
62
+ /** tokens() / getTokens() 可选的字段 */
63
+ export type TokenField = 'name' | 'symbol' | 'uri' | 'decimals' | 'supply' | 'tokenProgram' | 'mintAuthority' | 'freezeAuthority' | 'transferFee';
64
+ /** Token-2022 转账手续费的一套配置 */
65
+ export interface TransferFee {
66
+ /** 费率,万分比(100 = 1%) */
67
+ basisPoints: number;
68
+ /** 单笔最多收取的手续费(最小单位的十进制字符串) */
69
+ maximumFee: string;
70
+ /** 从这个 epoch 开始生效 */
71
+ epoch: string;
72
+ }
73
+ /**
74
+ * tokens() 的 transferFee 字段:顶层是**当前 epoch** 生效的配置(按 getEpochInfo 选 newer / older),
75
+ * older / newer 是扩展里的两套原始配置。收到的数量 = 转账数量 - min(数量 × basisPoints / 10000 向上取整, maximumFee)
76
+ */
77
+ export interface TransferFeeConfig extends TransferFee {
78
+ older: TransferFee;
79
+ newer: TransferFee;
80
+ }
81
+ export declare const DEFAULT_TOKEN_FIELDS: readonly ["name", "symbol", "decimals"];
82
+ export type DefaultTokenField = (typeof DEFAULT_TOKEN_FIELDS)[number];
83
+ interface TokenFieldTypes {
84
+ /** 优先取 Token-2022 TokenMetadata 扩展,其次 Metaplex 元数据 */
85
+ name: string | null;
86
+ symbol: string | null;
87
+ uri: string | null;
88
+ decimals: number | null;
89
+ /** 最小单位的十进制字符串 */
90
+ supply: string | null;
91
+ tokenProgram: string | null;
92
+ /** 没有 mint 权限(已放弃)时为 null */
93
+ mintAuthority: string | null;
94
+ freezeAuthority: string | null;
95
+ /** Token-2022 转账手续费;SPL Token、没有这个扩展、主币时为 null(不算失败) */
96
+ transferFee: TransferFeeConfig | null;
97
+ }
98
+ /**
99
+ * tokens() / getTokens() 的单项结果,只包含请求的字段。
100
+ * 请求 supply 时额外带 supplyFormatted(按 decimals 换算)。
101
+ */
102
+ export type TokenDetails<F extends TokenField = DefaultTokenField> = {
103
+ /** mint 地址 */
104
+ address: string;
105
+ native: boolean;
106
+ /** mint 存在且请求的 name / symbol / uri / decimals / supply 都读到时为 true */
107
+ success: boolean;
108
+ /** 失败原因,仅 success 为 false 时返回 */
109
+ error?: FailureReason;
110
+ /** error 为 missing-field 时:第一个读不到的字段 */
111
+ errorField?: F;
112
+ } & {
113
+ [K in F]: TokenFieldTypes[K];
114
+ } & ('supply' extends F ? {
115
+ supplyFormatted: string | null;
116
+ } : unknown);
117
+ export type TokenStandard = 'NonFungible' | 'FungibleAsset' | 'Fungible' | 'NonFungibleEdition' | 'ProgrammableNonFungible' | 'ProgrammableNonFungibleEdition';
118
+ /** nfts() / getNfts() 的单项结果(Metaplex 元数据;Token-2022 NFT 只有 name / symbol / uri) */
119
+ export interface NftDetails {
120
+ mint: string;
121
+ name: string | null;
122
+ symbol: string | null;
123
+ /** 链下元数据 JSON 的地址 */
124
+ uri: string | null;
125
+ /** 所属集合;verified 为 false 表示未经集合方签名确认 */
126
+ collection: {
127
+ address: string;
128
+ verified: boolean;
129
+ } | null;
130
+ creators: Array<{
131
+ address: string;
132
+ verified: boolean;
133
+ share: number;
134
+ }>;
135
+ /** 版税,万分比 */
136
+ sellerFeeBasisPoints: number | null;
137
+ tokenStandard: TokenStandard | null;
138
+ isMutable: boolean | null;
139
+ updateAuthority: string | null;
140
+ success: boolean;
141
+ /** 失败原因,仅 success 为 false 时返回 */
142
+ error?: FailureReason;
143
+ }
144
+ export interface NftOwner {
145
+ mint: string;
146
+ /** 持有人钱包地址 */
147
+ owner: string | null;
148
+ /** 持有该 NFT 的代币账户 */
149
+ tokenAccount: string | null;
150
+ success: boolean;
151
+ /** 失败原因,仅 success 为 false 时返回 */
152
+ error?: FailureReason;
153
+ }
154
+ export {};
155
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_TOKEN_FIELDS = ['name', 'symbol', 'decimals'];
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA0FA,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,UAAU,CAA0C,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "@w3lib/solana-batch-call",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Batched Solana reads: SOL / SPL / Token-2022 balances, token metadata, NFTs. Auto-batching JSON-RPC, getMultipleAccounts chunking, RPC failover. No Solana SDK dependency.",
5
+ "keywords": [
6
+ "solana",
7
+ "spl-token",
8
+ "token-2022",
9
+ "metaplex",
10
+ "batch",
11
+ "getMultipleAccounts",
12
+ "json-rpc"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "L",
16
+ "type": "module",
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src",
28
+ "README.md",
29
+ "LICENSE",
30
+ "CHANGELOG.md"
31
+ ],
32
+ "sideEffects": false,
33
+ "engines": {
34
+ "node": ">=20.19.0"
35
+ },
36
+ "scripts": {
37
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
38
+ "typecheck": "tsc -p tsconfig.json --noEmit",
39
+ "test": "vitest run",
40
+ "test:live": "LIVE=1 vitest run test/live.test.ts",
41
+ "prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/web3-lib/libs.git",
46
+ "directory": "packages/solana-batch-call"
47
+ },
48
+ "homepage": "https://github.com/web3-lib/libs/tree/main/packages/solana-batch-call#readme",
49
+ "bugs": {
50
+ "url": "https://github.com/web3-lib/libs/issues"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "dependencies": {
56
+ "@noble/hashes": "^1.8.0 || ^2.0.0",
57
+ "@scure/base": "^2.4.0"
58
+ },
59
+ "devDependencies": {
60
+ "@noble/curves": "^2.4.0"
61
+ }
62
+ }
package/src/address.ts ADDED
@@ -0,0 +1,134 @@
1
+ import { sha256 } from '@noble/hashes/sha2.js'
2
+ import { base58 } from '@scure/base'
3
+
4
+ import { ASSOCIATED_TOKEN_PROGRAM_ID, METADATA_PROGRAM_ID } from './constants.js'
5
+
6
+ /** 是否是合法的 Solana 地址(base58,32 字节) */
7
+ export function isAddress(value: unknown): value is string {
8
+ if (typeof value !== 'string' || value.length < 32 || value.length > 44) {
9
+ return false
10
+ }
11
+ try {
12
+ return base58.decode(value).length === 32
13
+ } catch {
14
+ return false
15
+ }
16
+ }
17
+
18
+ export function decodeAddress(address: string): Uint8Array {
19
+ const bytes = base58.decode(address)
20
+ if (bytes.length !== 32) {
21
+ throw new Error(`Invalid Solana address: ${address}`)
22
+ }
23
+ return bytes
24
+ }
25
+
26
+ export function encodeAddress(bytes: Uint8Array): string {
27
+ return base58.encode(bytes)
28
+ }
29
+
30
+ // ed25519:-x² + y² = 1 + d·x²·y²(mod p)
31
+ const P = 2n ** 255n - 19n
32
+ const D = 37095705934669439343138083508754565189542113879843219016388785533085940283555n
33
+
34
+ /** x^(2^k) mod p */
35
+ function pow2k(x: bigint, k: number): bigint {
36
+ let r = x
37
+ for (let i = 0; i < k; i++) r = (r * r) % P
38
+ return r
39
+ }
40
+
41
+ /** x^((p-1)/2) mod p(欧拉判别法),(p-1)/2 = (2^250 - 1)·2^4 + 6,用加法链:约 254 次平方 + 12 次乘法 */
42
+ function legendre(x: bigint): bigint {
43
+ const b2 = (((x * x) % P) * x) % P // x^(2^2-1)
44
+ const b4 = (pow2k(b2, 2) * b2) % P
45
+ const b5 = (pow2k(b4, 1) * x) % P
46
+ const b10 = (pow2k(b5, 5) * b5) % P
47
+ const b20 = (pow2k(b10, 10) * b10) % P
48
+ const b40 = (pow2k(b20, 20) * b20) % P
49
+ const b80 = (pow2k(b40, 40) * b40) % P
50
+ const b160 = (pow2k(b80, 80) * b80) % P
51
+ const b240 = (pow2k(b160, 80) * b80) % P
52
+ const b250 = (pow2k(b240, 10) * b10) % P
53
+ return (pow2k(b250, 4) * ((b2 * b2) % P)) % P
54
+ }
55
+
56
+ /**
57
+ * 32 字节是否是合法的 ed25519 压缩点(PDA 必须不在曲线上)。
58
+ * 按 RFC 8032 5.1.3 解压缩,与 @noble/curves 的 Point.fromBytes(web3.js 的 isOnCurve 同样如此)一致:
59
+ * y ≥ p、x = 0 但符号位为 1 都算不在曲线上。只需判断 x² = (y² - 1) / (d·y² + 1) 有没有解,不必真的开方
60
+ */
61
+ export function isOnCurve(bytes: Uint8Array): boolean {
62
+ if (bytes.length !== 32) {
63
+ return false
64
+ }
65
+ let y = 0n
66
+ for (let i = 31; i >= 0; i--) {
67
+ y = (y << 8n) | BigInt(i === 31 ? (bytes[i] as number) & 0x7f : (bytes[i] as number))
68
+ }
69
+ if (y >= P) {
70
+ return false
71
+ }
72
+ const y2 = (y * y) % P
73
+ const u = (y2 - 1n + P) % P
74
+ const v = (D * y2 + 1n) % P // d 不是平方数,v 不会为 0
75
+ if (u === 0n) {
76
+ // x = 0:符号位必须为 0
77
+ return ((bytes[31] as number) & 0x80) === 0
78
+ }
79
+ // u / v 是平方数 ⇔ u·v 是平方数(欧拉判别法)
80
+ return legendre((u * v) % P) === 1n
81
+ }
82
+
83
+ const PDA_MARKER = new TextEncoder().encode('ProgramDerivedAddress')
84
+
85
+ function concat(parts: Uint8Array[]): Uint8Array {
86
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0))
87
+ let offset = 0
88
+ for (const part of parts) {
89
+ out.set(part, offset)
90
+ offset += part.length
91
+ }
92
+ return out
93
+ }
94
+
95
+ // PDA 推导是纯计算但要做多次 sha256 + 曲线判断,结果缓存
96
+ const pdaCache = new Map<string, string>()
97
+
98
+ /**
99
+ * 推导 PDA(与 web3.js PublicKey.findProgramAddressSync 相同),返回地址。
100
+ * seeds 每项不超过 32 字节。
101
+ */
102
+ export function findProgramAddress(seeds: Uint8Array[], programId: string): string {
103
+ const key = `${programId}:${seeds.map((s) => encodeAddress(s)).join(',')}`
104
+ const cached = pdaCache.get(key)
105
+ if (cached) {
106
+ return cached
107
+ }
108
+ const program = decodeAddress(programId)
109
+ for (let bump = 255; bump >= 0; bump--) {
110
+ const hash = sha256(concat([...seeds, new Uint8Array([bump]), program, PDA_MARKER]))
111
+ if (!isOnCurve(hash)) {
112
+ const address = encodeAddress(hash)
113
+ if (pdaCache.size > 10_000) {
114
+ pdaCache.clear()
115
+ }
116
+ pdaCache.set(key, address)
117
+ return address
118
+ }
119
+ }
120
+ throw new Error('Unable to find a viable program address bump seed')
121
+ }
122
+
123
+ /** 关联代币账户(ATA)地址 */
124
+ export function getAssociatedTokenAddress(owner: string, mint: string, tokenProgram: string): string {
125
+ return findProgramAddress([decodeAddress(owner), decodeAddress(tokenProgram), decodeAddress(mint)], ASSOCIATED_TOKEN_PROGRAM_ID)
126
+ }
127
+
128
+ /** Metaplex Token Metadata 账户地址 */
129
+ export function getMetadataAddress(mint: string): string {
130
+ return findProgramAddress(
131
+ [new TextEncoder().encode('metadata'), decodeAddress(METADATA_PROGRAM_ID), decodeAddress(mint)],
132
+ METADATA_PROGRAM_ID,
133
+ )
134
+ }