@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.
- package/CHANGELOG.md +33 -0
- package/README.md +16 -8
- package/dist/accountReader.d.ts +57 -0
- package/dist/accountReader.d.ts.map +1 -0
- package/dist/accountReader.js +149 -0
- package/dist/accountReader.js.map +1 -0
- package/dist/address.d.ts +9 -0
- package/dist/address.d.ts.map +1 -1
- package/dist/address.js +36 -6
- package/dist/address.js.map +1 -1
- package/dist/approvals.d.ts +5 -3
- package/dist/approvals.d.ts.map +1 -1
- package/dist/approvals.js +25 -36
- package/dist/approvals.js.map +1 -1
- package/dist/balances.d.ts +178 -0
- package/dist/balances.d.ts.map +1 -0
- package/dist/balances.js +524 -0
- package/dist/balances.js.map +1 -0
- package/dist/batchPlan.d.ts +2 -2
- package/dist/batchPlan.d.ts.map +1 -1
- package/dist/batchPlan.js +5 -26
- package/dist/batchPlan.js.map +1 -1
- package/dist/bytes.d.ts +31 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/bytes.js +74 -0
- package/dist/bytes.js.map +1 -0
- package/dist/claim.d.ts +5 -3
- package/dist/claim.d.ts.map +1 -1
- package/dist/claim.js +10 -10
- package/dist/claim.js.map +1 -1
- package/dist/client.d.ts +26 -41
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +41 -986
- package/dist/client.js.map +1 -1
- package/dist/clientConfig.d.ts +11 -0
- package/dist/clientConfig.d.ts.map +1 -0
- package/dist/clientConfig.js +3 -0
- package/dist/clientConfig.js.map +1 -0
- package/dist/decode.d.ts.map +1 -1
- package/dist/decode.js +2 -29
- package/dist/decode.js.map +1 -1
- package/dist/fees.d.ts.map +1 -1
- package/dist/fees.js +2 -12
- package/dist/fees.js.map +1 -1
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +4 -15
- package/dist/history.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +21 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +50 -69
- package/dist/layout.js.map +1 -1
- package/dist/liquidity/common.js +2 -2
- package/dist/liquidity/common.js.map +1 -1
- package/dist/lp.d.ts +9 -13
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +67 -55
- package/dist/lp.js.map +1 -1
- package/dist/portfolio.d.ts.map +1 -1
- package/dist/portfolio.js +3 -8
- package/dist/portfolio.js.map +1 -1
- package/dist/reclaim.d.ts +4 -2
- package/dist/reclaim.d.ts.map +1 -1
- package/dist/reclaim.js +16 -16
- package/dist/reclaim.js.map +1 -1
- package/dist/risk.js +2 -2
- package/dist/risk.js.map +1 -1
- package/dist/rpc.d.ts +30 -7
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +400 -109
- package/dist/rpc.js.map +1 -1
- package/dist/simulate.d.ts.map +1 -1
- package/dist/simulate.js +3 -26
- package/dist/simulate.js.map +1 -1
- package/dist/source.d.ts +8 -4
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +11 -4
- package/dist/source.js.map +1 -1
- package/dist/subpaths/approvals.d.ts +3 -3
- package/dist/subpaths/approvals.d.ts.map +1 -1
- package/dist/subpaths/approvals.js.map +1 -1
- package/dist/subpaths/claim.d.ts +3 -3
- package/dist/subpaths/claim.d.ts.map +1 -1
- package/dist/subpaths/claim.js.map +1 -1
- package/dist/subpaths/reclaim.d.ts +3 -3
- package/dist/subpaths/reclaim.d.ts.map +1 -1
- package/dist/subpaths/reclaim.js.map +1 -1
- package/dist/swap/index.js +3 -3
- package/dist/swap/index.js.map +1 -1
- package/dist/tokenDetails.d.ts +12 -0
- package/dist/tokenDetails.d.ts.map +1 -0
- package/dist/tokenDetails.js +196 -0
- package/dist/tokenDetails.js.map +1 -0
- package/dist/tokenMeta.d.ts +63 -0
- package/dist/tokenMeta.d.ts.map +1 -0
- package/dist/tokenMeta.js +226 -0
- package/dist/tokenMeta.js.map +1 -0
- package/dist/tradeCheck.js +3 -3
- package/dist/tradeCheck.js.map +1 -1
- package/dist/util.d.ts +22 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +107 -0
- package/dist/util.js.map +1 -0
- package/dist/write/chain.d.ts +51 -0
- package/dist/write/chain.d.ts.map +1 -0
- package/dist/write/chain.js +184 -0
- package/dist/write/chain.js.map +1 -0
- package/dist/write/collect.js +2 -2
- package/dist/write/collect.js.map +1 -1
- package/dist/write/errors.d.ts +10 -0
- package/dist/write/errors.d.ts.map +1 -0
- package/dist/write/errors.js +77 -0
- package/dist/write/errors.js.map +1 -0
- package/dist/write/pack.d.ts +28 -0
- package/dist/write/pack.d.ts.map +1 -0
- package/dist/write/pack.js +125 -0
- package/dist/write/pack.js.map +1 -0
- package/dist/write/send.d.ts +23 -9
- package/dist/write/send.d.ts.map +1 -1
- package/dist/write/send.js +53 -351
- package/dist/write/send.js.map +1 -1
- package/dist/write/tracker.d.ts +24 -0
- package/dist/write/tracker.d.ts.map +1 -0
- package/dist/write/tracker.js +129 -0
- package/dist/write/tracker.js.map +1 -0
- package/package.json +1 -1
- package/src/accountReader.ts +187 -0
- package/src/address.ts +41 -6
- package/src/approvals.ts +36 -44
- package/src/balances.ts +678 -0
- package/src/batchPlan.ts +6 -26
- package/src/bytes.ts +89 -0
- package/src/claim.ts +15 -11
- package/src/client.ts +59 -1239
- package/src/clientConfig.ts +12 -0
- package/src/decode.ts +4 -30
- package/src/fees.ts +2 -9
- package/src/history.ts +4 -15
- package/src/index.ts +13 -1
- package/src/layout.ts +63 -82
- package/src/liquidity/common.ts +2 -2
- package/src/lp.ts +86 -60
- package/src/portfolio.ts +8 -11
- package/src/reclaim.ts +20 -17
- package/src/risk.ts +2 -2
- package/src/rpc.ts +472 -116
- package/src/simulate.ts +3 -21
- package/src/source.ts +14 -5
- package/src/subpaths/approvals.ts +3 -2
- package/src/subpaths/claim.ts +3 -2
- package/src/subpaths/reclaim.ts +3 -2
- package/src/swap/index.ts +3 -3
- package/src/tokenDetails.ts +240 -0
- package/src/tokenMeta.ts +270 -0
- package/src/tradeCheck.ts +3 -3
- package/src/util.ts +110 -0
- package/src/write/chain.ts +218 -0
- package/src/write/collect.ts +2 -2
- package/src/write/errors.ts +82 -0
- package/src/write/pack.ts +142 -0
- package/src/write/send.ts +74 -407
- 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.
|
|
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
|
|
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 =
|
|
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 {
|
|
11
|
-
import {
|
|
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
|
|
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
|
-
/**
|
|
93
|
+
/** 解析代币账户(基础布局用 layout 的 parseTokenAccountState)及关闭相关的 Token-2022 扩展;不是已初始化的代币账户时为 null */
|
|
92
94
|
export function parseTokenAccountState(data: Uint8Array): ParsedAccount | null {
|
|
93
|
-
|
|
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:
|
|
111
|
-
owner:
|
|
112
|
-
amount:
|
|
113
|
-
delegate:
|
|
114
|
-
frozen:
|
|
115
|
-
native:
|
|
116
|
-
delegatedAmount:
|
|
117
|
-
closeAuthority:
|
|
118
|
-
withheldAmount: withheld && withheld.length >= 8 ? new
|
|
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,
|
|
140
|
+
export async function approvals(client: SolanaClient, owner: string, options: ApprovalsOptions = {}): Promise<TokenAccountApproval[]> {
|
|
146
141
|
if (!isAddress(owner)) {
|
|
147
|
-
throw new
|
|
142
|
+
throw new InvalidAddressError(`Invalid owner address: ${owner}`, owner)
|
|
148
143
|
}
|
|
149
|
-
const { signal } =
|
|
150
|
-
const found =
|
|
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
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|