@w3lib/solana-batch-call 0.6.0 → 0.7.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 (165) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +16 -8
  3. package/dist/accountReader.d.ts +57 -0
  4. package/dist/accountReader.d.ts.map +1 -0
  5. package/dist/accountReader.js +149 -0
  6. package/dist/accountReader.js.map +1 -0
  7. package/dist/address.d.ts +9 -0
  8. package/dist/address.d.ts.map +1 -1
  9. package/dist/address.js +36 -6
  10. package/dist/address.js.map +1 -1
  11. package/dist/approvals.d.ts +5 -3
  12. package/dist/approvals.d.ts.map +1 -1
  13. package/dist/approvals.js +25 -36
  14. package/dist/approvals.js.map +1 -1
  15. package/dist/balances.d.ts +178 -0
  16. package/dist/balances.d.ts.map +1 -0
  17. package/dist/balances.js +524 -0
  18. package/dist/balances.js.map +1 -0
  19. package/dist/batchPlan.d.ts +2 -2
  20. package/dist/batchPlan.d.ts.map +1 -1
  21. package/dist/batchPlan.js +5 -26
  22. package/dist/batchPlan.js.map +1 -1
  23. package/dist/bytes.d.ts +31 -0
  24. package/dist/bytes.d.ts.map +1 -0
  25. package/dist/bytes.js +74 -0
  26. package/dist/bytes.js.map +1 -0
  27. package/dist/claim.d.ts +5 -3
  28. package/dist/claim.d.ts.map +1 -1
  29. package/dist/claim.js +10 -10
  30. package/dist/claim.js.map +1 -1
  31. package/dist/client.d.ts +26 -41
  32. package/dist/client.d.ts.map +1 -1
  33. package/dist/client.js +41 -986
  34. package/dist/client.js.map +1 -1
  35. package/dist/clientConfig.d.ts +11 -0
  36. package/dist/clientConfig.d.ts.map +1 -0
  37. package/dist/clientConfig.js +3 -0
  38. package/dist/clientConfig.js.map +1 -0
  39. package/dist/decode.d.ts.map +1 -1
  40. package/dist/decode.js +2 -29
  41. package/dist/decode.js.map +1 -1
  42. package/dist/fees.d.ts.map +1 -1
  43. package/dist/fees.js +2 -12
  44. package/dist/fees.js.map +1 -1
  45. package/dist/history.d.ts.map +1 -1
  46. package/dist/history.js +4 -15
  47. package/dist/history.js.map +1 -1
  48. package/dist/index.d.ts +3 -3
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +1 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/layout.d.ts +21 -0
  53. package/dist/layout.d.ts.map +1 -1
  54. package/dist/layout.js +50 -69
  55. package/dist/layout.js.map +1 -1
  56. package/dist/liquidity/common.js +2 -2
  57. package/dist/liquidity/common.js.map +1 -1
  58. package/dist/lp.d.ts +9 -13
  59. package/dist/lp.d.ts.map +1 -1
  60. package/dist/lp.js +67 -55
  61. package/dist/lp.js.map +1 -1
  62. package/dist/portfolio.d.ts.map +1 -1
  63. package/dist/portfolio.js +3 -8
  64. package/dist/portfolio.js.map +1 -1
  65. package/dist/reclaim.d.ts +4 -2
  66. package/dist/reclaim.d.ts.map +1 -1
  67. package/dist/reclaim.js +16 -16
  68. package/dist/reclaim.js.map +1 -1
  69. package/dist/risk.js +2 -2
  70. package/dist/risk.js.map +1 -1
  71. package/dist/rpc.d.ts +30 -7
  72. package/dist/rpc.d.ts.map +1 -1
  73. package/dist/rpc.js +400 -109
  74. package/dist/rpc.js.map +1 -1
  75. package/dist/simulate.d.ts.map +1 -1
  76. package/dist/simulate.js +3 -26
  77. package/dist/simulate.js.map +1 -1
  78. package/dist/source.d.ts +8 -4
  79. package/dist/source.d.ts.map +1 -1
  80. package/dist/source.js +11 -4
  81. package/dist/source.js.map +1 -1
  82. package/dist/subpaths/approvals.d.ts +3 -3
  83. package/dist/subpaths/approvals.d.ts.map +1 -1
  84. package/dist/subpaths/approvals.js.map +1 -1
  85. package/dist/subpaths/claim.d.ts +3 -3
  86. package/dist/subpaths/claim.d.ts.map +1 -1
  87. package/dist/subpaths/claim.js.map +1 -1
  88. package/dist/subpaths/reclaim.d.ts +3 -3
  89. package/dist/subpaths/reclaim.d.ts.map +1 -1
  90. package/dist/subpaths/reclaim.js.map +1 -1
  91. package/dist/swap/index.js +3 -3
  92. package/dist/swap/index.js.map +1 -1
  93. package/dist/tokenDetails.d.ts +12 -0
  94. package/dist/tokenDetails.d.ts.map +1 -0
  95. package/dist/tokenDetails.js +196 -0
  96. package/dist/tokenDetails.js.map +1 -0
  97. package/dist/tokenMeta.d.ts +63 -0
  98. package/dist/tokenMeta.d.ts.map +1 -0
  99. package/dist/tokenMeta.js +226 -0
  100. package/dist/tokenMeta.js.map +1 -0
  101. package/dist/tradeCheck.js +3 -3
  102. package/dist/tradeCheck.js.map +1 -1
  103. package/dist/util.d.ts +22 -0
  104. package/dist/util.d.ts.map +1 -0
  105. package/dist/util.js +107 -0
  106. package/dist/util.js.map +1 -0
  107. package/dist/write/chain.d.ts +51 -0
  108. package/dist/write/chain.d.ts.map +1 -0
  109. package/dist/write/chain.js +184 -0
  110. package/dist/write/chain.js.map +1 -0
  111. package/dist/write/collect.js +2 -2
  112. package/dist/write/collect.js.map +1 -1
  113. package/dist/write/errors.d.ts +10 -0
  114. package/dist/write/errors.d.ts.map +1 -0
  115. package/dist/write/errors.js +77 -0
  116. package/dist/write/errors.js.map +1 -0
  117. package/dist/write/pack.d.ts +28 -0
  118. package/dist/write/pack.d.ts.map +1 -0
  119. package/dist/write/pack.js +125 -0
  120. package/dist/write/pack.js.map +1 -0
  121. package/dist/write/send.d.ts +23 -9
  122. package/dist/write/send.d.ts.map +1 -1
  123. package/dist/write/send.js +53 -351
  124. package/dist/write/send.js.map +1 -1
  125. package/dist/write/tracker.d.ts +24 -0
  126. package/dist/write/tracker.d.ts.map +1 -0
  127. package/dist/write/tracker.js +129 -0
  128. package/dist/write/tracker.js.map +1 -0
  129. package/package.json +1 -1
  130. package/src/accountReader.ts +187 -0
  131. package/src/address.ts +41 -6
  132. package/src/approvals.ts +36 -44
  133. package/src/balances.ts +678 -0
  134. package/src/batchPlan.ts +6 -26
  135. package/src/bytes.ts +89 -0
  136. package/src/claim.ts +15 -11
  137. package/src/client.ts +59 -1239
  138. package/src/clientConfig.ts +12 -0
  139. package/src/decode.ts +4 -30
  140. package/src/fees.ts +2 -9
  141. package/src/history.ts +4 -15
  142. package/src/index.ts +13 -1
  143. package/src/layout.ts +63 -82
  144. package/src/liquidity/common.ts +2 -2
  145. package/src/lp.ts +86 -60
  146. package/src/portfolio.ts +8 -11
  147. package/src/reclaim.ts +20 -17
  148. package/src/risk.ts +2 -2
  149. package/src/rpc.ts +472 -116
  150. package/src/simulate.ts +3 -21
  151. package/src/source.ts +14 -5
  152. package/src/subpaths/approvals.ts +3 -2
  153. package/src/subpaths/claim.ts +3 -2
  154. package/src/subpaths/reclaim.ts +3 -2
  155. package/src/swap/index.ts +3 -3
  156. package/src/tokenDetails.ts +240 -0
  157. package/src/tokenMeta.ts +270 -0
  158. package/src/tradeCheck.ts +3 -3
  159. package/src/util.ts +110 -0
  160. package/src/write/chain.ts +218 -0
  161. package/src/write/collect.ts +2 -2
  162. package/src/write/errors.ts +82 -0
  163. package/src/write/pack.ts +142 -0
  164. package/src/write/send.ts +74 -407
  165. package/src/write/tracker.ts +152 -0
@@ -0,0 +1,129 @@
1
+ import { sleep } from '../util.js';
2
+ /**
3
+ * 统一轮询所有待确认的交易:一次 getSignatureStatuses 查全部(每 256 个一组),未确认的每两次轮询重发一次,blockhash 过期后再查一次历史确认没上链。
4
+ * 区块高度(getEpochInfo)不是每轮都查:记下上次查到的(高度, slot),之后用状态查询返回的 context.slot 估计高度的上限
5
+ * (高度每个 slot 最多加 1),估计值超过某笔交易的 lastValidBlockHeight 时才查,确认过期
6
+ */
7
+ export class Tracker {
8
+ chain;
9
+ #watched = new Set();
10
+ #running = false;
11
+ /** 上次查到的区块高度和对应的 slot */
12
+ #anchor = null;
13
+ constructor(chain) {
14
+ this.chain = chain;
15
+ }
16
+ watch(signature, raw, lastValidBlockHeight) {
17
+ const { timeout = 120_000 } = this.chain.options;
18
+ return new Promise((resolve) => {
19
+ this.#watched.add({ signature, raw, lastValidBlockHeight, deadline: Date.now() + timeout, polls: 0, resolve });
20
+ if (!this.#running) {
21
+ this.#running = true;
22
+ void this.#loop();
23
+ }
24
+ });
25
+ }
26
+ /** 查交易状态;contextSlot 是各次响应里最小的 context.slot(节点的数据新到哪个 slot),响应没带时为 0 */
27
+ async #statuses(signatures, history) {
28
+ const found = new Map();
29
+ let contextSlot = Number.POSITIVE_INFINITY;
30
+ for (let i = 0; i < signatures.length; i += 256) {
31
+ const chunk = signatures.slice(i, i + 256);
32
+ const res = await this.chain.client.request('getSignatureStatuses', [
33
+ chunk,
34
+ { searchTransactionHistory: history },
35
+ ]);
36
+ contextSlot = Math.min(contextSlot, res.context?.slot ?? 0);
37
+ res.value.forEach((s, k) => s && found.set(chunk[k], s));
38
+ }
39
+ return { found, contextSlot };
40
+ }
41
+ #done(status) {
42
+ const level = status.confirmationStatus;
43
+ return this.chain.commitment === 'finalized' ? level === 'finalized' : level === 'confirmed' || level === 'finalized';
44
+ }
45
+ #settle(item, result) {
46
+ this.#watched.delete(item);
47
+ item.resolve(result);
48
+ }
49
+ /** 是否可能有交易过期了(需要查区块高度确认):不知道当前高度、或状态查询没带 slot 时总是查 */
50
+ #mayHaveExpired(waiting, contextSlot) {
51
+ const known = waiting.filter((w) => Number.isFinite(w.lastValidBlockHeight));
52
+ if (!known.length) {
53
+ return false;
54
+ }
55
+ if (!this.#anchor || !(contextSlot > 0) || !Number.isFinite(contextSlot)) {
56
+ return true;
57
+ }
58
+ const highest = this.#anchor.height + Math.max(0, contextSlot - this.#anchor.slot);
59
+ return known.some((w) => highest > w.lastValidBlockHeight);
60
+ }
61
+ async #loop() {
62
+ const { pollInterval = 2_000, signal } = this.chain.options;
63
+ try {
64
+ while (this.#watched.size) {
65
+ await sleep(pollInterval, signal, false);
66
+ if (signal?.aborted) {
67
+ ;
68
+ [...this.#watched].forEach((w) => this.#settle(w, { kind: 'aborted' }));
69
+ return;
70
+ }
71
+ const items = [...this.#watched];
72
+ try {
73
+ const { found: statuses, contextSlot } = await this.#statuses(items.map((w) => w.signature), false);
74
+ for (const item of items) {
75
+ const status = statuses.get(item.signature);
76
+ if (status && this.#done(status)) {
77
+ this.#settle(item, { kind: 'landed', slot: status.slot, err: status.err });
78
+ }
79
+ }
80
+ const waiting = items.filter((w) => this.#watched.has(w));
81
+ if (waiting.length && this.#mayHaveExpired(waiting, contextSlot)) {
82
+ // 区块高度和对应的 slot 一起取(同一个节点、同一个时刻)
83
+ const epoch = await this.chain.client.request('getEpochInfo', [{ commitment: 'confirmed' }]);
84
+ this.#anchor = { height: epoch.blockHeight, slot: epoch.absoluteSlot };
85
+ const expired = waiting.filter((w) => epoch.blockHeight > w.lastValidBlockHeight);
86
+ if (expired.length) {
87
+ // 过期了:再查一次历史确认确实没上链。请求可能落到另一个节点(多节点 / 负载均衡),
88
+ // 它的数据必须不早于看到过期的那个 slot,否则可能只是它还没看到这笔交易,这一轮先不判断
89
+ const history = await this.#statuses(expired.map((w) => w.signature), true);
90
+ const caughtUp = history.contextSlot >= epoch.absoluteSlot;
91
+ for (const item of expired) {
92
+ const status = history.found.get(item.signature);
93
+ if (!status) {
94
+ if (!caughtUp)
95
+ continue;
96
+ this.#settle(item, { kind: 'expired' });
97
+ }
98
+ else if (this.#done(status)) {
99
+ this.#settle(item, { kind: 'landed', slot: status.slot, err: status.err });
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ catch {
106
+ // 节点暂时出错:下次再查
107
+ }
108
+ const now = Date.now();
109
+ for (const item of [...this.#watched]) {
110
+ if (now >= item.deadline) {
111
+ this.#settle(item, { kind: 'timeout' });
112
+ }
113
+ else if (++item.polls % 2 === 0) {
114
+ // 每两次轮询重发一次(每笔都重发会让待确认交易多时很快触发节点限频)
115
+ void this.chain.broadcast(item.raw).catch(() => { });
116
+ }
117
+ }
118
+ }
119
+ }
120
+ finally {
121
+ this.#running = false;
122
+ if (this.#watched.size) {
123
+ this.#running = true;
124
+ void this.#loop();
125
+ }
126
+ }
127
+ }
128
+ }
129
+ //# sourceMappingURL=tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracker.js","sourceRoot":"","sources":["../../src/write/tracker.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@w3lib/solana-batch-call",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Batched Solana reads: SOL / SPL / Token-2022 balances, token metadata, NFTs; batch transfer, collect and program calls (packed transactions, fee payer). Auto-batching JSON-RPC, getMultipleAccounts chunking, RPC failover. No Solana SDK dependency.",
5
5
  "keywords": [
6
6
  "solana",
@@ -0,0 +1,187 @@
1
+ // 账户读取管线(getMultipleAccounts 分组 / 并发 / 重试)和查询上下文
2
+
3
+ import { base64 } from '@scure/base'
4
+
5
+ import { isAddress } from './address.js'
6
+ import type { RequestOptions, SlotOptions } from './client.js'
7
+ import type { AccountInfo } from './layout.js'
8
+ import { AllNodesFailedError, HttpError, RpcError, isBehind, type RpcTransport } from './rpc.js'
9
+ import { NetworkMismatchError } from './source.js'
10
+ import type { Commitment } from './types.js'
11
+ import { mapPool, sleep } from './util.js'
12
+
13
+ /** 一次查询的上下文:取消、最低 slot,以及读到的 slot(各请求 context.slot 的最小值) */
14
+ export interface QueryContext {
15
+ signal?: AbortSignal
16
+ minContextSlot?: number
17
+ slot?: number
18
+ }
19
+
20
+ /** 节点高度不够时的重试间隔与总时长 */
21
+ const BEHIND_RETRY_INTERVAL = 500
22
+ const BEHIND_RETRY_TIMEOUT = 10_000
23
+
24
+ export const MAX_ACCOUNTS_PER_REQUEST = 100
25
+ /** 同时进行的 getMultipleAccounts 请求数上限 */
26
+ const MAX_PARALLEL_ACCOUNT_REQUESTS = 3
27
+ /** fetchAccounts 里没能读到(节点问题)的账户 */
28
+ export const FAILED = Symbol('failed')
29
+
30
+ /** 可以降级处理的错误:网络 / HTTP 错误、节点限制(限频、需要 Key 等)、节点不在期望的网络上、所有节点都失败;其他(参数错误、程序 bug)照常抛出 */
31
+ export function isRecoverable(err: unknown): boolean {
32
+ return err instanceof HttpError || (err instanceof RpcError && err.nodeFault) || err instanceof NetworkMismatchError || err instanceof AllNodesFailedError
33
+ }
34
+
35
+ interface RawAccount {
36
+ lamports: number | string
37
+ owner: string
38
+ data: [string, string] | string
39
+ executable: boolean
40
+ }
41
+
42
+ function toAccountInfo(address: string, raw: RawAccount | null): AccountInfo | null {
43
+ if (!raw) {
44
+ return null
45
+ }
46
+ const encoded = Array.isArray(raw.data) ? raw.data[0] : raw.data
47
+ return {
48
+ address,
49
+ lamports: BigInt(raw.lamports),
50
+ owner: raw.owner,
51
+ data: base64.decode(encoded),
52
+ executable: raw.executable,
53
+ }
54
+ }
55
+
56
+ /**
57
+ * 账户读取管线:getMultipleAccounts 按 100 个一组、去重、并发池(最多同时 3 个),节点高度不够时等待重试,
58
+ * 记录读到的 slot;tolerant 时节点问题只标记失败。SolanaClient 和余额 / 代币详情模块共用
59
+ */
60
+ export class AccountReader {
61
+ constructor(
62
+ readonly transport: RpcTransport,
63
+ readonly commitment: Commitment,
64
+ ) {}
65
+
66
+ /** 发给传输层:有 signal 时作为第三个参数传下去(没有时只传两个参数,自定义传输不受影响) */
67
+ send<T>(method: string, params: readonly unknown[] | undefined, signal: AbortSignal | undefined): Promise<T> {
68
+ return signal ? this.transport.request<T>(method, params, { signal }) : this.transport.request<T>(method, params)
69
+ }
70
+
71
+ /**
72
+ * 发一个读账户类的请求:params 最后一项是配置对象,传了 minContextSlot 时带上;
73
+ * 节点高度不够(所有节点都落后)时每 500ms 重试,最多约 10 秒;记录返回的 context.slot(取最小值)
74
+ */
75
+ async call<T>(ctx: QueryContext, method: string, params: unknown[]): Promise<T> {
76
+ const last = params[params.length - 1]
77
+ const sent =
78
+ ctx.minContextSlot === undefined ? params : [...params.slice(0, -1), { ...(last as object), minContextSlot: ctx.minContextSlot }]
79
+ const started = Date.now()
80
+ for (;;) {
81
+ try {
82
+ const res = await this.send<T>(method, sent, ctx.signal)
83
+ const slot = (res as { context?: { slot?: unknown } } | null)?.context?.slot
84
+ if (typeof slot === 'number') {
85
+ ctx.slot = ctx.slot === undefined ? slot : Math.min(ctx.slot, slot)
86
+ }
87
+ return res
88
+ } catch (err) {
89
+ if (ctx.minContextSlot === undefined || !isBehind(err) || Date.now() - started + BEHIND_RETRY_INTERVAL > BEHIND_RETRY_TIMEOUT) {
90
+ throw err
91
+ }
92
+ await sleep(BEHIND_RETRY_INTERVAL, ctx.signal)
93
+ }
94
+ }
95
+ }
96
+
97
+ async accounts(addresses: readonly string[], ctx: QueryContext): Promise<(AccountInfo | null)[]> {
98
+ const fetched = await this.fetchAccounts(addresses, false, ctx)
99
+ return addresses.map((address) => (fetched.get(address) as AccountInfo | null | undefined) ?? null)
100
+ }
101
+
102
+ /**
103
+ * 只读 lamports(主币余额):getMultipleAccounts 带 dataSlice { offset: 0, length: 0 },不读回账户数据——
104
+ * 钱包可能是数据很大的程序账户(上百 KB),整份读回会拖慢甚至超时。结果与 addresses 一一对应,账户不存在或地址非法时为 null
105
+ */
106
+ async lamports(addresses: readonly string[], ctx: QueryContext): Promise<(bigint | null)[]> {
107
+ const [fetched] = await this.fetchGroups([{ addresses, lamportsOnly: true }], false, ctx)
108
+ return addresses.map((address) => (fetched?.get(address) as AccountInfo | null | undefined)?.lamports ?? null)
109
+ }
110
+
111
+ /** getMultipleAccounts(读账户数据):见 fetchGroups */
112
+ async fetchAccounts(
113
+ addresses: readonly string[],
114
+ tolerant: boolean,
115
+ ctx: QueryContext,
116
+ failure?: { error?: unknown },
117
+ ): Promise<Map<string, AccountInfo | null | typeof FAILED>> {
118
+ const [fetched] = await this.fetchGroups([{ addresses, lamportsOnly: false }], tolerant, ctx, failure)
119
+ return fetched as Map<string, AccountInfo | null | typeof FAILED>
120
+ }
121
+
122
+ /**
123
+ * getMultipleAccounts:每组去重、按 100 个一组拆分,所有组的请求放进并发池,最多同时 3 个(一个结束就发下一个,不必等同一波里最慢的)。
124
+ * lamportsOnly 的组带 dataSlice(长度 0,对同一请求里的所有账户生效,所以只要 lamports 的账户要单独成组)。
125
+ * tolerant 为 true 时,某个请求因节点问题(网络、限频等)失败不抛错:该请求的账户标记为 FAILED,并且不再发后面的请求(节点已经在限频了),
126
+ * 第一个这样的错误记在 failure.error;否则第一个错误抛出,后面的请求也不再发。结果按组返回
127
+ */
128
+ async fetchGroups(
129
+ groups: ReadonlyArray<{ addresses: readonly string[]; lamportsOnly: boolean }>,
130
+ tolerant: boolean,
131
+ ctx: QueryContext,
132
+ failure?: { error?: unknown },
133
+ ): Promise<Array<Map<string, AccountInfo | null | typeof FAILED>>> {
134
+ const chunks: Array<{ group: number; addresses: string[]; lamportsOnly: boolean }> = []
135
+ groups.forEach(({ addresses, lamportsOnly }, group) => {
136
+ const unique = [...new Set(addresses.filter((a) => isAddress(a)))]
137
+ for (let i = 0; i < unique.length; i += MAX_ACCOUNTS_PER_REQUEST) {
138
+ chunks.push({ group, addresses: unique.slice(i, i + MAX_ACCOUNTS_PER_REQUEST), lamportsOnly })
139
+ }
140
+ })
141
+ const results = groups.map(() => new Map<string, AccountInfo | null | typeof FAILED>())
142
+ // 出错后不再发后面的请求
143
+ let stopped = false
144
+ // 并发池最先发出的几个请求在同一 tick(合并成一个 JSON-RPC 批量请求)
145
+ await mapPool(chunks, MAX_PARALLEL_ACCOUNT_REQUESTS, async (chunk) => {
146
+ const result = results[chunk.group] as Map<string, AccountInfo | null | typeof FAILED>
147
+ if (stopped) {
148
+ chunk.addresses.forEach((address) => result.set(address, FAILED))
149
+ return
150
+ }
151
+ // 已取消的查询不再发后面的请求
152
+ if (ctx.signal?.aborted) {
153
+ throw ctx.signal.reason
154
+ }
155
+ try {
156
+ const res = await this.call<{ value: Array<RawAccount | null> }>(ctx, 'getMultipleAccounts', [
157
+ chunk.addresses,
158
+ { encoding: 'base64', commitment: this.commitment, ...(chunk.lamportsOnly ? { dataSlice: { offset: 0, length: 0 } } : {}) },
159
+ ])
160
+ chunk.addresses.forEach((address, j) => result.set(address, toAccountInfo(address, res?.value?.[j] ?? null)))
161
+ } catch (err) {
162
+ stopped = true
163
+ if (!tolerant || !isRecoverable(err)) {
164
+ throw err
165
+ }
166
+ if (failure && !('error' in failure)) {
167
+ failure.error = err
168
+ }
169
+ chunk.addresses.forEach((address) => result.set(address, FAILED))
170
+ }
171
+ })
172
+ return results
173
+ }
174
+ }
175
+
176
+ /** 结果项带上 slot(传了 withSlot / minContextSlot,且这次查询发了请求时) */
177
+ export function attachSlot<T extends object>(items: T[], ctx: QueryContext, options: SlotOptions): T[] {
178
+ if ((!options.withSlot && options.minContextSlot === undefined) || ctx.slot === undefined) {
179
+ return items
180
+ }
181
+ const slot = ctx.slot
182
+ return items.map((item) => ({ ...item, slot }))
183
+ }
184
+
185
+ export function queryContext(options: RequestOptions & SlotOptions): QueryContext {
186
+ return { signal: options.signal, minContextSlot: options.minContextSlot }
187
+ }
package/src/address.ts CHANGED
@@ -18,7 +18,7 @@ export function isAddress(value: unknown): value is string {
18
18
  export function decodeAddress(address: string): Uint8Array {
19
19
  const bytes = base58.decode(address)
20
20
  if (bytes.length !== 32) {
21
- throw new Error(`Invalid Solana address: ${address}`)
21
+ throw new InvalidAddressError(`Invalid Solana address: ${address}`, address)
22
22
  }
23
23
  return bytes
24
24
  }
@@ -95,8 +95,28 @@ function concat(parts: Uint8Array[]): Uint8Array {
95
95
  const MAX_SEED_LENGTH = 32
96
96
  const MAX_SEEDS = 16
97
97
 
98
- // PDA 推导是纯计算但要做多次 sha256 + 曲线判断,结果缓存
98
+ // PDA 推导是纯计算但要做多次 sha256 + 曲线判断,结果缓存(LRU:命中时移到最后,满了删最久没用的)
99
99
  const pdaCache = new Map<string, string>()
100
+ const MAX_PDA_CACHE = 10_000
101
+
102
+ const HEX = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'))
103
+
104
+ /** 缓存键:程序地址 + 各种子的十六进制(比 base58 编码快得多;用 ',' 分隔,不同的种子划分不会撞键) */
105
+ function pdaKey(seeds: readonly Uint8Array[], programId: string): string {
106
+ let key = programId
107
+ for (const seed of seeds) {
108
+ key += ','
109
+ for (let i = 0; i < seed.length; i++) {
110
+ key += HEX[seed[i] as number]
111
+ }
112
+ }
113
+ return key
114
+ }
115
+
116
+ /** 测试用:这组种子的 PDA 是否在缓存里 */
117
+ export function isPdaCached(seeds: readonly Uint8Array[], programId: string): boolean {
118
+ return pdaCache.has(pdaKey(seeds, programId))
119
+ }
100
120
 
101
121
  /**
102
122
  * 推导 PDA(与 web3.js PublicKey.findProgramAddressSync 相同),返回地址。
@@ -110,9 +130,11 @@ export function findProgramAddress(seeds: Uint8Array[], programId: string): stri
110
130
  if (seeds.length + 1 > MAX_SEEDS) {
111
131
  throw new TypeError('Max seeds exceeded')
112
132
  }
113
- const key = `${programId}:${seeds.map((s) => encodeAddress(s)).join(',')}`
133
+ const key = pdaKey(seeds, programId)
114
134
  const cached = pdaCache.get(key)
115
135
  if (cached) {
136
+ pdaCache.delete(key)
137
+ pdaCache.set(key, cached)
116
138
  return cached
117
139
  }
118
140
  const program = decodeAddress(programId)
@@ -120,10 +142,10 @@ export function findProgramAddress(seeds: Uint8Array[], programId: string): stri
120
142
  const hash = sha256(concat([...seeds, new Uint8Array([bump]), program, PDA_MARKER]))
121
143
  if (!isOnCurve(hash)) {
122
144
  const address = encodeAddress(hash)
123
- if (pdaCache.size > 10_000) {
124
- pdaCache.clear()
125
- }
126
145
  pdaCache.set(key, address)
146
+ if (pdaCache.size > MAX_PDA_CACHE) {
147
+ pdaCache.delete(pdaCache.keys().next().value as string)
148
+ }
127
149
  return address
128
150
  }
129
151
  }
@@ -151,3 +173,16 @@ export function getMetadataAddress(mint: string): string {
151
173
  METADATA_PROGRAM_ID,
152
174
  )
153
175
  }
176
+
177
+ /** 地址不合法时抛出(继承 Error,message 与之前的普通 Error 相同;用 code / address 判断) */
178
+ export class InvalidAddressError extends Error {
179
+ readonly code = 'INVALID_ADDRESS'
180
+ /** 不合法的那个值 */
181
+ readonly address: string
182
+
183
+ constructor(message: string, address: string) {
184
+ super(message)
185
+ this.name = 'InvalidAddressError'
186
+ this.address = address
187
+ }
188
+ }
package/src/approvals.ts CHANGED
@@ -4,18 +4,19 @@
4
4
  */
5
5
  import { base64 } from '@scure/base'
6
6
 
7
- import { encodeAddress, getAssociatedTokenAddress, getMetadataAddress, isAddress } from './address.js'
7
+ import { encodeAddress, getAssociatedTokenAddress, getMetadataAddress, InvalidAddressError, isAddress } from './address.js'
8
8
  import type { RequestOptions, SolanaClient } from './client.js'
9
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'
10
+ import { Bytes } from './bytes.js'
11
+ import { findExtension, formatUnits, parseMetaplexMetadata, parseMint, parseTokenAccountState as parseTokenAccountBase, type AccountInfo } from './layout.js'
12
+ import { withMethodHint } from './lp.js'
12
13
  import { closeAccount, harvestWithheldTokensToMint, revoke } from './write/instructions.js'
13
14
  import type { BatchCallOptions } from './write/batch.js'
14
15
  import type { WriteResult } from './write/send.js'
15
16
  import type { SignerInput } from './write/signer.js'
16
17
  import type { Instruction } from './write/transaction.js'
17
18
 
18
- export interface ApprovalsQuery extends RequestOptions {
19
+ export interface ApprovalsOptions extends RequestOptions {
19
20
  /**
20
21
  * 要检查的代币 mint:读这些代币的关联代币账户(ATA,SPL Token 和 Token-2022 都看),免费公共节点也能用;非 ATA 的代币账户查不到。
21
22
  * 不传时列出地址的全部代币账户(getTokenAccountsByOwner),需要支持它的节点
@@ -25,6 +26,9 @@ export interface ApprovalsQuery extends RequestOptions {
25
26
  all?: boolean
26
27
  }
27
28
 
29
+ /** @deprecated 改名为 ApprovalsOptions(与其他查询的 options 命名统一),旧名保留 */
30
+ export type ApprovalsQuery = ApprovalsOptions
31
+
28
32
  /** 关不掉的原因:balance 有余额(Wrapped SOL 除外);close-authority 关闭权限是别的地址;confidential-balance Token-2022 机密余额不为 0 */
29
33
  export type CloseBlocker = 'balance' | 'close-authority' | 'confidential-balance'
30
34
 
@@ -66,8 +70,6 @@ export interface TokenAccountApproval {
66
70
  closeBlocker: CloseBlocker | null
67
71
  }
68
72
 
69
- const ACCOUNT_SIZE = 165
70
- const ACCOUNT_TYPE_ACCOUNT = 2
71
73
  const EXT_TRANSFER_FEE_AMOUNT = 2
72
74
  const EXT_CONFIDENTIAL_TRANSFER_ACCOUNT = 5
73
75
  const EXT_PERMANENT_DELEGATE = 12
@@ -88,34 +90,27 @@ interface ParsedAccount {
88
90
  confidentialBalance: boolean
89
91
  }
90
92
 
91
- /** 解析代币账户;不是已初始化的代币账户时为 null */
93
+ /** 解析代币账户(基础布局用 layout 的 parseTokenAccountState)及关闭相关的 Token-2022 扩展;不是已初始化的代币账户时为 null */
92
94
  export function parseTokenAccountState(data: Uint8Array): ParsedAccount | null {
93
- if (data.length !== ACCOUNT_SIZE && !(data.length > ACCOUNT_SIZE && data[ACCOUNT_SIZE] === ACCOUNT_TYPE_ACCOUNT)) {
95
+ const base = parseTokenAccountBase(data)
96
+ if (!base) {
94
97
  return null
95
98
  }
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
99
  const withheld = findExtension(data, EXT_TRANSFER_FEE_AMOUNT)
105
100
  // 机密转账:待入账(lo / hi)和可用余额的密文全为 0 才能关闭;手续费扩展同理(Token-2022 的 closable 检查)
106
101
  const confidential = findExtension(data, EXT_CONFIDENTIAL_TRANSFER_ACCOUNT)
107
102
  const confidentialFee = findExtension(data, EXT_CONFIDENTIAL_TRANSFER_FEE_AMOUNT)
108
103
  const nonZero = (bytes: Uint8Array | null, from: number, to: number) => !!bytes && bytes.slice(from, to).some((b) => b !== 0)
109
104
  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,
105
+ mint: base.mint,
106
+ owner: base.owner,
107
+ amount: base.amount,
108
+ delegate: base.delegate,
109
+ frozen: base.frozen,
110
+ native: base.native,
111
+ delegatedAmount: base.delegatedAmount,
112
+ closeAuthority: base.closeAuthority,
113
+ withheldAmount: withheld && withheld.length >= 8 ? new Bytes(withheld).u64(0) : 0n,
119
114
  confidentialBalance: nonZero(confidential, 33, 225) || nonZero(confidentialFee, 0, 64),
120
115
  }
121
116
  }
@@ -142,12 +137,12 @@ interface Found {
142
137
  * await approvals(new SolanaClient('https://my-rpc…'), owner) // 全部代币账户
143
138
  * ```
144
139
  */
145
- export async function approvals(client: SolanaClient, owner: string, query: ApprovalsQuery = {}): Promise<TokenAccountApproval[]> {
140
+ export async function approvals(client: SolanaClient, owner: string, options: ApprovalsOptions = {}): Promise<TokenAccountApproval[]> {
146
141
  if (!isAddress(owner)) {
147
- throw new Error(`Invalid owner address: ${owner}`)
142
+ throw new InvalidAddressError(`Invalid owner address: ${owner}`, owner)
148
143
  }
149
- const { signal } = query
150
- const found = query.mints ? await ataAccounts(client, owner, query.mints, signal) : await listAccounts(client, owner, signal)
144
+ const { signal } = options
145
+ const found = options.mints ? await ataAccounts(client, owner, options.mints, signal) : await listAccounts(client, owner, signal)
151
146
  // 只看 owner 自己的账户(ATA 的 owner 可能被改过:SPL Token 的 ATA 能 SetAuthority 换 owner)
152
147
  const mine = found.filter((f) => f.parsed.owner === owner)
153
148
  const mints = [...new Set(mine.map((f) => f.parsed.mint))]
@@ -185,7 +180,7 @@ export async function approvals(client: SolanaClient, owner: string, query: Appr
185
180
  closeBlocker,
186
181
  }
187
182
  })
188
- return query.all ? list : list.filter((a) => a.delegate !== null || a.closeBlocker === 'close-authority' || BigInt(a.amount) === 0n)
183
+ return options.all ? list : list.filter((a) => a.delegate !== null || a.closeBlocker === 'close-authority' || BigInt(a.amount) === 0n)
189
184
  }
190
185
 
191
186
  /** 指定 mints:两个程序的 ATA 都读(mint 所属程序未知时只有一个存在) */
@@ -203,22 +198,19 @@ async function ataAccounts(client: SolanaClient, owner: string, mints: readonly
203
198
 
204
199
  /** 不传 mints:用 getTokenAccountsByOwner 列出两个程序的全部代币账户(base64,自己解析扩展) */
205
200
  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 ?? [] }),
201
+ const lists = await withMethodHint(
202
+ Promise.all(
203
+ [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID].map((programId) =>
204
+ client
205
+ .request<{ value: RawTokenAccount[] }>('getTokenAccountsByOwner', [owner, { programId }, { encoding: 'base64', commitment: client.commitment }], { signal })
206
+ .then((res) => ({ programId, value: res.value ?? [] })),
210
207
  ),
211
208
  ),
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
- })
209
+ signal,
210
+ (detail) =>
211
+ `Listing the token accounts of ${owner} failed (${detail}). ` +
212
+ 'Listing needs getTokenAccountsByOwner (most free public nodes do not support it): pass the mints in `mints`, or use a node that supports it',
213
+ )
222
214
  return lists.flatMap(({ programId, value }) =>
223
215
  value.flatMap((entry) => {
224
216
  const parsed = entry.account.owner === programId ? parseTokenAccountState(base64.decode(entry.account.data[0])) : null