@w3lib/solana-batch-call 0.5.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 +47 -0
- package/README.md +129 -9
- 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 +52 -0
- package/dist/batchPlan.d.ts.map +1 -0
- package/dist/batchPlan.js +64 -0
- package/dist/batchPlan.js.map +1 -0
- 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 +163 -0
- package/dist/claim.d.ts.map +1 -0
- package/dist/claim.js +197 -0
- package/dist/claim.js.map +1 -0
- 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 +22 -0
- package/dist/decode.d.ts.map +1 -0
- package/dist/decode.js +160 -0
- package/dist/decode.js.map +1 -0
- package/dist/describe.d.ts +26 -0
- package/dist/describe.d.ts.map +1 -0
- package/dist/describe.js +58 -0
- package/dist/describe.js.map +1 -0
- 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 +125 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +220 -0
- package/dist/history.js.map +1 -0
- package/dist/historyHelius.d.ts +52 -0
- package/dist/historyHelius.d.ts.map +1 -0
- package/dist/historyHelius.js +125 -0
- package/dist/historyHelius.js.map +1 -0
- 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/liquidity/orca.d.ts +6 -0
- package/dist/liquidity/orca.d.ts.map +1 -1
- package/dist/liquidity/orca.js +14 -3
- package/dist/liquidity/orca.js.map +1 -1
- package/dist/liquidity/raydiumClmm.d.ts +6 -0
- package/dist/liquidity/raydiumClmm.d.ts.map +1 -1
- package/dist/liquidity/raydiumClmm.js +12 -5
- package/dist/liquidity/raydiumClmm.js.map +1 -1
- package/dist/lp.d.ts +55 -12
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +158 -70
- package/dist/lp.js.map +1 -1
- package/dist/lpMath.d.ts +22 -0
- package/dist/lpMath.d.ts.map +1 -1
- package/dist/lpMath.js +21 -0
- package/dist/lpMath.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/programs.d.ts +19 -0
- package/dist/programs.d.ts.map +1 -0
- package/dist/programs.js +175 -0
- package/dist/programs.js.map +1 -0
- package/dist/reclaim.d.ts +192 -0
- package/dist/reclaim.d.ts.map +1 -0
- package/dist/reclaim.js +399 -0
- package/dist/reclaim.js.map +1 -0
- 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 +404 -111
- package/dist/rpc.js.map +1 -1
- package/dist/simulate.d.ts +106 -0
- package/dist/simulate.d.ts.map +1 -0
- package/dist/simulate.js +395 -0
- package/dist/simulate.js.map +1 -0
- 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 +31 -0
- package/dist/subpaths/claim.d.ts.map +1 -0
- package/dist/subpaths/claim.js +31 -0
- package/dist/subpaths/claim.js.map +1 -0
- package/dist/subpaths/history.d.ts +21 -0
- package/dist/subpaths/history.d.ts.map +1 -0
- package/dist/subpaths/history.js +22 -0
- package/dist/subpaths/history.js.map +1 -0
- package/dist/subpaths/lp.d.ts +2 -2
- package/dist/subpaths/lp.d.ts.map +1 -1
- package/dist/subpaths/lp.js +2 -2
- package/dist/subpaths/lp.js.map +1 -1
- package/dist/subpaths/reclaim.d.ts +34 -0
- package/dist/subpaths/reclaim.d.ts.map +1 -0
- package/dist/subpaths/reclaim.js +34 -0
- package/dist/subpaths/reclaim.js.map +1 -0
- package/dist/subpaths/simulate.d.ts +22 -0
- package/dist/subpaths/simulate.d.ts.map +1 -0
- package/dist/subpaths/simulate.js +23 -0
- package/dist/subpaths/simulate.js.map +1 -0
- 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/instructions.d.ts +2 -0
- package/dist/write/instructions.d.ts.map +1 -1
- package/dist/write/instructions.js +16 -0
- package/dist/write/instructions.js.map +1 -1
- 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 +21 -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 +117 -0
- package/src/bytes.ts +89 -0
- package/src/claim.ts +327 -0
- package/src/client.ts +59 -1239
- package/src/clientConfig.ts +12 -0
- package/src/decode.ts +176 -0
- package/src/describe.ts +77 -0
- package/src/fees.ts +2 -9
- package/src/history.ts +325 -0
- package/src/historyHelius.ts +136 -0
- package/src/index.ts +13 -1
- package/src/layout.ts +63 -82
- package/src/liquidity/common.ts +2 -2
- package/src/liquidity/orca.ts +15 -5
- package/src/liquidity/raydiumClmm.ts +13 -5
- package/src/lp.ts +226 -75
- package/src/lpMath.ts +38 -0
- package/src/portfolio.ts +8 -11
- package/src/programs.ts +216 -0
- package/src/reclaim.ts +550 -0
- package/src/risk.ts +2 -2
- package/src/rpc.ts +476 -118
- package/src/simulate.ts +517 -0
- package/src/source.ts +14 -5
- package/src/subpaths/approvals.ts +3 -2
- package/src/subpaths/claim.ts +54 -0
- package/src/subpaths/history.ts +38 -0
- package/src/subpaths/lp.ts +5 -1
- package/src/subpaths/reclaim.ts +58 -0
- package/src/subpaths/simulate.ts +37 -0
- 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/instructions.ts +17 -0
- package/src/write/pack.ts +142 -0
- package/src/write/send.ts +74 -407
- package/src/write/tracker.ts +152 -0
package/dist/write/send.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { SolanaClient } from '../client.js';
|
|
2
|
+
import type { RpcSource } from '../source.js';
|
|
2
3
|
import type { ResolvedSigner, SignerInput } from './signer.js';
|
|
3
4
|
import { type Instruction } from './transaction.js';
|
|
5
|
+
export { priorityFeeFor } from './chain.js';
|
|
6
|
+
export { transactionErrorText } from './errors.js';
|
|
4
7
|
/**
|
|
5
8
|
* 每个调用的状态:
|
|
6
9
|
* - confirmed:已上链且执行成功
|
|
@@ -44,6 +47,26 @@ export interface SendOptions {
|
|
|
44
47
|
signal?: AbortSignal;
|
|
45
48
|
/** 每个结果状态变化时回调,用于展示进度 */
|
|
46
49
|
onUpdate?: (result: WriteResult) => void;
|
|
50
|
+
/**
|
|
51
|
+
* 发送交易用的节点(如专门的发送服务、质押连接的节点):URL、URL 数组、web3.js Connection 或自定义传输。
|
|
52
|
+
* 不传时用 client 的节点;模拟、查确认状态仍用 client
|
|
53
|
+
*/
|
|
54
|
+
sendRpc?: RpcSource | readonly RpcSource[];
|
|
55
|
+
/**
|
|
56
|
+
* 广播给哪些节点(sendRpc,不传时 client 的节点):
|
|
57
|
+
* - 'first'(默认):按故障切换顺序只发给第一个可用的节点
|
|
58
|
+
* - 'all':同时发给所有节点,任一接受即可(多个节点各自转发给出块节点,拥堵时更容易上链;等待确认期间的重发也会发给所有节点)。
|
|
59
|
+
* 默认 'first':公共节点对 sendTransaction 限频严,每两次轮询就重发一次时,全部广播会成倍增加请求;官方节点在浏览器里总是 403,发给它只是白费请求
|
|
60
|
+
*/
|
|
61
|
+
broadcast?: 'first' | 'all';
|
|
62
|
+
/**
|
|
63
|
+
* 计算单元上限 = 模拟消耗 × ratio + extra(默认 ratio 1.1、extra 1000),不超过 140 万。
|
|
64
|
+
* 消耗会随链上状态变化的调用(如兑换穿过的 tick 数不同)可以调大,避免上链时超出上限失败;固定手续费的调用(扫空 SOL)不受影响
|
|
65
|
+
*/
|
|
66
|
+
computeUnitMargin?: {
|
|
67
|
+
ratio?: number;
|
|
68
|
+
extra?: number;
|
|
69
|
+
};
|
|
47
70
|
}
|
|
48
71
|
/** 每个调用的结果,都是字符串 / 数字 / null,可以直接 JSON.stringify */
|
|
49
72
|
export interface WriteResult {
|
|
@@ -89,19 +112,10 @@ export interface Unit {
|
|
|
89
112
|
}
|
|
90
113
|
export declare function emptyOutcome(status: WriteStatus, error?: string | null): TxOutcome;
|
|
91
114
|
export declare function toWriteResult(index: number, from: string, outcome: TxOutcome): WriteResult;
|
|
92
|
-
/** 并发数为 limit 的 map,结果按输入顺序 */
|
|
93
|
-
export declare function mapPool<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
94
115
|
/** 每个签名的基础手续费(lamports) */
|
|
95
116
|
export declare const LAMPORTS_PER_SIGNATURE = 5000n;
|
|
96
117
|
/** 交易手续费:签名数 × 5000 + 优先费用(单价 × 计算单元上限,向上取整) */
|
|
97
118
|
export declare function transactionFee(signatures: number, computeUnits: number, price: bigint): bigint;
|
|
98
|
-
/**
|
|
99
|
-
* 交易错误的可读说明:优先取日志(见 logDetail),否则按错误类型;
|
|
100
|
-
* programs 是每条指令的程序(按下标),用来翻译常见程序的自定义错误码
|
|
101
|
-
*/
|
|
102
|
-
export declare function transactionErrorText(err: unknown, logs?: readonly string[] | null, programs?: readonly string[]): string;
|
|
103
|
-
/** 按 SendOptions 取优先费用单价(micro-lamports / 计算单元);writable 是交易里的可写账户 */
|
|
104
|
-
export declare function priorityFeeFor(client: SolanaClient, options: SendOptions, writable: readonly string[]): Promise<bigint>;
|
|
105
119
|
export interface SendUnitsOptions {
|
|
106
120
|
/** 每笔交易最多打包几个调用,默认不限(按交易大小 1232 字节和 64 个账户的上限) */
|
|
107
121
|
batchSize?: number;
|
package/dist/write/send.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send.d.ts","sourceRoot":"","sources":["../../src/write/send.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"send.d.ts","sourceRoot":"","sources":["../../src/write/send.ts"],"names":[],"mappings":"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"}
|
package/dist/write/send.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { sha256 } from '@noble/hashes/sha2.js';
|
|
2
1
|
import { base58, base64 } from '@scure/base';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { mapPool } from '../util.js';
|
|
3
|
+
import { Chain, isRecentMessage, remember } from './chain.js';
|
|
4
|
+
import { errorText, isBudgetExceeded, isRejection, transactionErrorText } from './errors.js';
|
|
5
|
+
import { MAX_COMPUTE_UNITS } from './instructions.js';
|
|
6
|
+
import { budgetInstructions, packUnits } from './pack.js';
|
|
7
|
+
import { MAX_ACCOUNT_LOCKS, PACKET_DATA_SIZE, compileMessage, messageBlockhash, sameBytes, serializeTransaction } from './transaction.js';
|
|
8
|
+
import { Tracker } from './tracker.js';
|
|
9
|
+
// 拆出去的部分仍从这里导出(包内其他模块和外部引用的路径不变)
|
|
10
|
+
export { priorityFeeFor } from './chain.js';
|
|
11
|
+
export { transactionErrorText } from './errors.js';
|
|
7
12
|
export function emptyOutcome(status, error = null) {
|
|
8
13
|
return { status, signature: null, slot: null, fee: null, computeUnits: null, error };
|
|
9
14
|
}
|
|
@@ -19,337 +24,13 @@ export function toWriteResult(index, from, outcome) {
|
|
|
19
24
|
error: outcome.error,
|
|
20
25
|
};
|
|
21
26
|
}
|
|
22
|
-
/** 并发数为 limit 的 map,结果按输入顺序 */
|
|
23
|
-
export async function mapPool(items, limit, fn) {
|
|
24
|
-
const results = new Array(items.length);
|
|
25
|
-
let next = 0;
|
|
26
|
-
const worker = async () => {
|
|
27
|
-
while (next < items.length) {
|
|
28
|
-
const i = next++;
|
|
29
|
-
results[i] = await fn(items[i], i);
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
await Promise.all(Array.from({ length: Math.min(Math.max(1, limit), items.length) }, worker));
|
|
33
|
-
return results;
|
|
34
|
-
}
|
|
35
|
-
function sleep(ms, signal) {
|
|
36
|
-
return new Promise((resolve) => {
|
|
37
|
-
if (signal?.aborted) {
|
|
38
|
-
resolve();
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
const timer = setTimeout(done, ms);
|
|
42
|
-
function done() {
|
|
43
|
-
clearTimeout(timer);
|
|
44
|
-
signal?.removeEventListener('abort', done);
|
|
45
|
-
resolve();
|
|
46
|
-
}
|
|
47
|
-
signal?.addEventListener('abort', done, { once: true });
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
27
|
/** 每个签名的基础手续费(lamports) */
|
|
51
28
|
export const LAMPORTS_PER_SIGNATURE = 5000n;
|
|
52
29
|
/** 交易手续费:签名数 × 5000 + 优先费用(单价 × 计算单元上限,向上取整) */
|
|
53
30
|
export function transactionFee(signatures, computeUnits, price) {
|
|
54
31
|
return LAMPORTS_PER_SIGNATURE * BigInt(signatures) + (price * BigInt(computeUnits) + 999999n) / 1000000n;
|
|
55
32
|
}
|
|
56
|
-
function errorText(err) {
|
|
57
|
-
return err instanceof Error ? err.message : String(err);
|
|
58
|
-
}
|
|
59
|
-
const TX_ERRORS = {
|
|
60
|
-
InsufficientFundsForFee: 'insufficient SOL for the transaction fee',
|
|
61
|
-
AccountNotFound: 'fee payer account not found (it has no SOL)',
|
|
62
|
-
BlockhashNotFound: 'blockhash not found',
|
|
63
|
-
AlreadyProcessed: 'transaction already processed',
|
|
64
|
-
};
|
|
65
|
-
/**
|
|
66
|
-
* 日志里最有用的一行:程序打印的 “Program log: Error: …”,其次是系统程序直接打印的说明(如 “Transfer: insufficient lamports …”,
|
|
67
|
-
* 不以 “Program ” 开头),最后才是运行时的 “Program … failed: custom program error: 0x1”
|
|
68
|
-
*/
|
|
69
|
-
function logDetail(logs) {
|
|
70
|
-
const reversed = [...logs].reverse();
|
|
71
|
-
const programError = reversed.find((line) => /^Program log: (Error|error)/.test(line));
|
|
72
|
-
if (programError)
|
|
73
|
-
return programError.replace(/^Program log: /, '');
|
|
74
|
-
const plain = reversed.find((line) => !line.startsWith('Program '));
|
|
75
|
-
if (plain)
|
|
76
|
-
return plain;
|
|
77
|
-
return reversed.find((line) => / failed: /.test(line))?.replace(/^Program \w+ failed: /, '');
|
|
78
|
-
}
|
|
79
|
-
// 常见程序的自定义错误码(上链失败时没有日志,只有错误码)
|
|
80
|
-
const SYSTEM_ERRORS = ['account already in use', 'insufficient lamports', 'invalid program id', 'invalid account data length'];
|
|
81
|
-
const TOKEN_ERRORS = {
|
|
82
|
-
0: 'account not rent-exempt',
|
|
83
|
-
1: 'insufficient funds',
|
|
84
|
-
2: 'invalid mint',
|
|
85
|
-
3: 'account not associated with this mint',
|
|
86
|
-
4: 'owner does not match',
|
|
87
|
-
9: 'account is not initialized',
|
|
88
|
-
11: 'non-native account can only be closed if its balance is zero',
|
|
89
|
-
17: 'account is frozen',
|
|
90
|
-
18: 'mint decimals mismatch',
|
|
91
|
-
};
|
|
92
|
-
function customErrorText(program, code) {
|
|
93
|
-
if (program === SYSTEM_PROGRAM_ID)
|
|
94
|
-
return SYSTEM_ERRORS[code];
|
|
95
|
-
if (program === TOKEN_PROGRAM_ID || program === TOKEN_2022_PROGRAM_ID)
|
|
96
|
-
return TOKEN_ERRORS[code];
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* 交易错误的可读说明:优先取日志(见 logDetail),否则按错误类型;
|
|
101
|
-
* programs 是每条指令的程序(按下标),用来翻译常见程序的自定义错误码
|
|
102
|
-
*/
|
|
103
|
-
export function transactionErrorText(err, logs, programs) {
|
|
104
|
-
const detail = logDetail(logs ?? []);
|
|
105
|
-
if (typeof err === 'string') {
|
|
106
|
-
return TX_ERRORS[err] ?? err;
|
|
107
|
-
}
|
|
108
|
-
const instruction = err?.InstructionError;
|
|
109
|
-
if (instruction) {
|
|
110
|
-
const [index, reason] = instruction;
|
|
111
|
-
const custom = reason?.Custom;
|
|
112
|
-
const known = custom === undefined ? undefined : customErrorText(programs?.[index], custom);
|
|
113
|
-
return detail ?? known ?? (typeof reason === 'string' ? reason : JSON.stringify(reason));
|
|
114
|
-
}
|
|
115
|
-
if (err?.InsufficientFundsForRent) {
|
|
116
|
-
return 'account would be left with less SOL than the rent-exempt minimum';
|
|
117
|
-
}
|
|
118
|
-
return detail ?? JSON.stringify(err);
|
|
119
|
-
}
|
|
120
|
-
function isBudgetExceeded(err, logs) {
|
|
121
|
-
const reason = err?.InstructionError?.[1];
|
|
122
|
-
return reason === 'ComputationalBudgetExceeded' || (logs ?? []).some((l) => /exceeded CUs meter|compute units/i.test(l) && /exceed/i.test(l));
|
|
123
|
-
}
|
|
124
|
-
// ---- 节点访问 ----
|
|
125
|
-
/**
|
|
126
|
-
* 最近发出过的交易消息(sha256):同样的消息签名也相同,再发一次只会被节点当成同一笔。
|
|
127
|
-
* 在签名之前检查(钱包签名的交易也适用),重复时调整计算单元上限让消息不同
|
|
128
|
-
*/
|
|
129
|
-
const recentMessages = new Set();
|
|
130
|
-
function messageKey(message) {
|
|
131
|
-
return base64.encode(sha256(message));
|
|
132
|
-
}
|
|
133
|
-
function remember(message) {
|
|
134
|
-
recentMessages.add(messageKey(message));
|
|
135
|
-
if (recentMessages.size > 10_000) {
|
|
136
|
-
recentMessages.delete(recentMessages.values().next().value);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/** 一批发送共用的节点访问:blockhash(缓存 10 秒)、优先费用(按账户缓存 10 秒) */
|
|
140
|
-
class Chain {
|
|
141
|
-
client;
|
|
142
|
-
options;
|
|
143
|
-
#blockhash = null;
|
|
144
|
-
#fees = new Map();
|
|
145
|
-
constructor(client, options) {
|
|
146
|
-
this.client = client;
|
|
147
|
-
this.options = options;
|
|
148
|
-
}
|
|
149
|
-
get commitment() {
|
|
150
|
-
return this.options.commitment ?? 'confirmed';
|
|
151
|
-
}
|
|
152
|
-
#walletChain = null;
|
|
153
|
-
/** Wallet Standard 的链标识('solana:mainnet' 等);网络未知时为 undefined */
|
|
154
|
-
walletChain() {
|
|
155
|
-
this.#walletChain ??= this.client.getCluster().then((cluster) => (cluster ? `solana:${cluster}` : undefined), () => undefined);
|
|
156
|
-
return this.#walletChain;
|
|
157
|
-
}
|
|
158
|
-
blockhash(fresh = false) {
|
|
159
|
-
if (fresh || !this.#blockhash || Date.now() - this.#blockhash.at > 10_000) {
|
|
160
|
-
const value = this.client
|
|
161
|
-
.request('getLatestBlockhash', [{ commitment: 'confirmed' }])
|
|
162
|
-
.then((res) => res.value);
|
|
163
|
-
value.catch(() => {
|
|
164
|
-
if (this.#blockhash?.value === value)
|
|
165
|
-
this.#blockhash = null;
|
|
166
|
-
});
|
|
167
|
-
this.#blockhash = { at: Date.now(), value };
|
|
168
|
-
}
|
|
169
|
-
return this.#blockhash.value;
|
|
170
|
-
}
|
|
171
|
-
async priorityFee(writable) {
|
|
172
|
-
const option = this.options.priorityFee ?? 'auto';
|
|
173
|
-
if (option !== 'auto') {
|
|
174
|
-
return BigInt(option);
|
|
175
|
-
}
|
|
176
|
-
const accounts = [...writable].sort().slice(0, 128);
|
|
177
|
-
const key = accounts.join(',');
|
|
178
|
-
let cached = this.#fees.get(key);
|
|
179
|
-
if (!cached || Date.now() - cached.at > 10_000) {
|
|
180
|
-
const cap = BigInt(this.options.maxPriorityFee ?? 1_000_000);
|
|
181
|
-
const value = this.client
|
|
182
|
-
.request('getRecentPrioritizationFees', [accounts])
|
|
183
|
-
.then((list) => {
|
|
184
|
-
const fees = list.map((f) => BigInt(f.prioritizationFee)).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
185
|
-
const p75 = fees.length ? fees[Math.min(fees.length - 1, Math.floor(fees.length * 0.75))] : 0n;
|
|
186
|
-
return p75 < cap ? p75 : cap;
|
|
187
|
-
})
|
|
188
|
-
.catch(() => 0n); // 节点不支持时不加优先费用
|
|
189
|
-
cached = { at: Date.now(), value };
|
|
190
|
-
this.#fees.set(key, cached);
|
|
191
|
-
}
|
|
192
|
-
return cached.value;
|
|
193
|
-
}
|
|
194
|
-
async simulate(message, minContextSlot) {
|
|
195
|
-
const raw = base64.encode(serializeTransaction(message, message.signers.map(() => null)));
|
|
196
|
-
const config = { encoding: 'base64', sigVerify: false, replaceRecentBlockhash: true, commitment: 'confirmed' };
|
|
197
|
-
if (minContextSlot !== undefined) {
|
|
198
|
-
config.minContextSlot = minContextSlot;
|
|
199
|
-
}
|
|
200
|
-
// 节点暂时的问题(限频、高度还没到 minContextSlot 等)重试几次,不当成调用失败
|
|
201
|
-
for (let attempt = 0;; attempt++) {
|
|
202
|
-
try {
|
|
203
|
-
const res = await this.client.request('simulateTransaction', [raw, config]);
|
|
204
|
-
return res.value;
|
|
205
|
-
}
|
|
206
|
-
catch (err) {
|
|
207
|
-
if (attempt >= 3 || (err instanceof RpcError && !isNodeFault(err)))
|
|
208
|
-
throw err;
|
|
209
|
-
await sleep(500 * (attempt + 1), this.options.signal);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
/** 广播:'accepted';节点明确拒绝时抛出;节点都连不上时返回错误说明(交易可能已经发出) */
|
|
214
|
-
async broadcast(raw) {
|
|
215
|
-
try {
|
|
216
|
-
await this.client.request('sendTransaction', [raw, { encoding: 'base64', skipPreflight: true, maxRetries: 0 }]);
|
|
217
|
-
return 'accepted';
|
|
218
|
-
}
|
|
219
|
-
catch (err) {
|
|
220
|
-
if (/already (been )?processed/i.test(errorText(err))) {
|
|
221
|
-
return 'accepted';
|
|
222
|
-
}
|
|
223
|
-
if (err instanceof RpcError && !isNodeFault(err)) {
|
|
224
|
-
throw err;
|
|
225
|
-
}
|
|
226
|
-
return { uncertain: `broadcast not acknowledged: ${errorText(err)}` };
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
/** 按 SendOptions 取优先费用单价(micro-lamports / 计算单元);writable 是交易里的可写账户 */
|
|
231
|
-
export function priorityFeeFor(client, options, writable) {
|
|
232
|
-
return new Chain(client, options).priorityFee(writable);
|
|
233
|
-
}
|
|
234
|
-
/** 统一轮询所有待确认的交易:一次 getSignatureStatuses 查全部(每 256 个一组),未确认的每次重发,blockhash 过期后再查一次历史确认没上链 */
|
|
235
|
-
class Tracker {
|
|
236
|
-
chain;
|
|
237
|
-
#watched = new Set();
|
|
238
|
-
#running = false;
|
|
239
|
-
constructor(chain) {
|
|
240
|
-
this.chain = chain;
|
|
241
|
-
}
|
|
242
|
-
watch(signature, raw, lastValidBlockHeight) {
|
|
243
|
-
const { timeout = 120_000 } = this.chain.options;
|
|
244
|
-
return new Promise((resolve) => {
|
|
245
|
-
this.#watched.add({ signature, raw, lastValidBlockHeight, deadline: Date.now() + timeout, polls: 0, resolve });
|
|
246
|
-
if (!this.#running) {
|
|
247
|
-
this.#running = true;
|
|
248
|
-
void this.#loop();
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
/** 查交易状态;contextSlot 是各次响应里最小的 context.slot(节点的数据新到哪个 slot) */
|
|
253
|
-
async #statuses(signatures, history) {
|
|
254
|
-
const found = new Map();
|
|
255
|
-
let contextSlot = Number.POSITIVE_INFINITY;
|
|
256
|
-
for (let i = 0; i < signatures.length; i += 256) {
|
|
257
|
-
const chunk = signatures.slice(i, i + 256);
|
|
258
|
-
const res = await this.chain.client.request('getSignatureStatuses', [
|
|
259
|
-
chunk,
|
|
260
|
-
{ searchTransactionHistory: history },
|
|
261
|
-
]);
|
|
262
|
-
contextSlot = Math.min(contextSlot, res.context?.slot ?? 0);
|
|
263
|
-
res.value.forEach((s, k) => s && found.set(chunk[k], s));
|
|
264
|
-
}
|
|
265
|
-
return { found, contextSlot };
|
|
266
|
-
}
|
|
267
|
-
#done(status) {
|
|
268
|
-
const level = status.confirmationStatus;
|
|
269
|
-
return this.chain.commitment === 'finalized' ? level === 'finalized' : level === 'confirmed' || level === 'finalized';
|
|
270
|
-
}
|
|
271
|
-
#settle(item, result) {
|
|
272
|
-
this.#watched.delete(item);
|
|
273
|
-
item.resolve(result);
|
|
274
|
-
}
|
|
275
|
-
async #loop() {
|
|
276
|
-
const { pollInterval = 2_000, signal } = this.chain.options;
|
|
277
|
-
try {
|
|
278
|
-
while (this.#watched.size) {
|
|
279
|
-
await sleep(pollInterval, signal);
|
|
280
|
-
if (signal?.aborted) {
|
|
281
|
-
;
|
|
282
|
-
[...this.#watched].forEach((w) => this.#settle(w, { kind: 'aborted' }));
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
const items = [...this.#watched];
|
|
286
|
-
try {
|
|
287
|
-
const { found: statuses } = await this.#statuses(items.map((w) => w.signature), false);
|
|
288
|
-
for (const item of items) {
|
|
289
|
-
const status = statuses.get(item.signature);
|
|
290
|
-
if (status && this.#done(status)) {
|
|
291
|
-
this.#settle(item, { kind: 'landed', slot: status.slot, err: status.err });
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
const waiting = items.filter((w) => this.#watched.has(w));
|
|
295
|
-
if (!waiting.length) {
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
// 区块高度和对应的 slot 一起取(同一个节点、同一个时刻)
|
|
299
|
-
const epoch = await this.chain.client.request('getEpochInfo', [{ commitment: 'confirmed' }]);
|
|
300
|
-
const expired = waiting.filter((w) => epoch.blockHeight > w.lastValidBlockHeight);
|
|
301
|
-
if (expired.length) {
|
|
302
|
-
// 过期了:再查一次历史确认确实没上链。请求可能落到另一个节点(多节点 / 负载均衡),
|
|
303
|
-
// 它的数据必须不早于看到过期的那个 slot,否则可能只是它还没看到这笔交易,这一轮先不判断
|
|
304
|
-
const history = await this.#statuses(expired.map((w) => w.signature), true);
|
|
305
|
-
const caughtUp = history.contextSlot >= epoch.absoluteSlot;
|
|
306
|
-
for (const item of expired) {
|
|
307
|
-
const status = history.found.get(item.signature);
|
|
308
|
-
if (!status) {
|
|
309
|
-
if (!caughtUp)
|
|
310
|
-
continue;
|
|
311
|
-
this.#settle(item, { kind: 'expired' });
|
|
312
|
-
}
|
|
313
|
-
else if (this.#done(status)) {
|
|
314
|
-
this.#settle(item, { kind: 'landed', slot: status.slot, err: status.err });
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
catch {
|
|
320
|
-
// 节点暂时出错:下次再查
|
|
321
|
-
}
|
|
322
|
-
const now = Date.now();
|
|
323
|
-
for (const item of [...this.#watched]) {
|
|
324
|
-
if (now >= item.deadline) {
|
|
325
|
-
this.#settle(item, { kind: 'timeout' });
|
|
326
|
-
}
|
|
327
|
-
else if (++item.polls % 2 === 0) {
|
|
328
|
-
// 每两次轮询重发一次(每笔都重发会让待确认交易多时很快触发节点限频)
|
|
329
|
-
void this.chain.broadcast(item.raw).catch(() => { });
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
finally {
|
|
335
|
-
this.#running = false;
|
|
336
|
-
if (this.#watched.size) {
|
|
337
|
-
this.#running = true;
|
|
338
|
-
void this.#loop();
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
33
|
// ---- 发送 ----
|
|
344
|
-
/** 打包用的占位 blockhash(大小与真实值相同) */
|
|
345
|
-
const PLACEHOLDER_BLOCKHASH = '11111111111111111111111111111111';
|
|
346
|
-
function budgetInstructions(units, price) {
|
|
347
|
-
return [setComputeUnitLimit(units), setComputeUnitPrice(price)];
|
|
348
|
-
}
|
|
349
|
-
function fits(payer, units) {
|
|
350
|
-
const message = compileMessage(payer, [...budgetInstructions(MAX_COMPUTE_UNITS, 0n), ...units.flatMap((u) => u.instructions)], PLACEHOLDER_BLOCKHASH);
|
|
351
|
-
return transactionSize(message) <= PACKET_DATA_SIZE && message.accountKeys.length <= MAX_ACCOUNT_LOCKS;
|
|
352
|
-
}
|
|
353
34
|
/** 交易错误对应的调用:InstructionError 的指令下标(前两条是计算预算指令),或租金不足的账户所在的调用 */
|
|
354
35
|
function failingUnit(err, units, message) {
|
|
355
36
|
const instruction = err?.InstructionError;
|
|
@@ -376,6 +57,28 @@ function failingUnit(err, units, message) {
|
|
|
376
57
|
}
|
|
377
58
|
return null;
|
|
378
59
|
}
|
|
60
|
+
/** 免租下限:0 字节系统账户(手续费支付者付完手续费后不能掉到它和 0 之间) */
|
|
61
|
+
const RENT_EXEMPT_MINIMUM = 890880n;
|
|
62
|
+
/**
|
|
63
|
+
* 取舍:以前有优先费用时总是按真实的上限和单价再模拟一次,它挡住的是两种情况:
|
|
64
|
+
* - 付款方付不起优先费用:这里用模拟后余额 + 当前余额判断,付不起(或拿不到余额)时照旧再模拟一次,这层保护没有丢;
|
|
65
|
+
* - 收紧后的计算单元上限(消耗 × 1.1 + 1000)不够用:第二次模拟和第一次只隔几百毫秒、基于几乎同一个状态,
|
|
66
|
+
* 消耗与第一次相同,上限按定义高于它,几乎不可能在这里失败;真正的风险是发送时状态已经变了(如兑换路径穿过更多 tick),
|
|
67
|
+
* 第二次模拟同样看不到。所以默认跳过,消耗会波动的调用用 SendOptions.computeUnitMargin 调大余量
|
|
68
|
+
*
|
|
69
|
+
* 第一次模拟(计算单元上限取最大、单价 0,节点已扣基础手续费)后,付款方能否再付得起优先费用(单价 × 真实的上限):
|
|
70
|
+
* 模拟后的余额和当前余额(手续费在执行前从当前余额里扣)各减去基础手续费 / 优先费用后都不低于免租下限才算付得起;
|
|
71
|
+
* 拿不到余额时按付不起处理(再完整模拟一次,同以前)
|
|
72
|
+
*/
|
|
73
|
+
function canAffordPriority(signatures, limit, price, before, sim) {
|
|
74
|
+
const after = sim.accounts?.[0]?.lamports;
|
|
75
|
+
if (before === null || after === undefined || after === null) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const priority = transactionFee(0, limit, price);
|
|
79
|
+
const base = LAMPORTS_PER_SIGNATURE * BigInt(signatures);
|
|
80
|
+
return BigInt(after) - priority >= RENT_EXEMPT_MINIMUM && before - base - priority >= RENT_EXEMPT_MINIMUM;
|
|
81
|
+
}
|
|
379
82
|
/**
|
|
380
83
|
* 发送一批调用:按手续费支付者分组,组内按顺序把相邻的调用打包成交易(不超过交易大小上限),
|
|
381
84
|
* 逐笔模拟(失败的调用剔除后重新模拟其余的)、签名、发送,不等上一笔确认;不同支付者并发。
|
|
@@ -394,12 +97,10 @@ export async function sendUnits(client, units, options, pack = {}) {
|
|
|
394
97
|
});
|
|
395
98
|
await mapPool([...groups.values()], options.concurrency ?? 8, (list) => sendGroup(chain, tracker, list, options, pack.batchSize ?? Number.POSITIVE_INFINITY));
|
|
396
99
|
}
|
|
397
|
-
function isRejection(err) {
|
|
398
|
-
const e = err;
|
|
399
|
-
return e?.code === 4001 || /reject|denied|cancel|declined/i.test(`${String(e?.name ?? '')} ${String(e?.message ?? '')}`);
|
|
400
|
-
}
|
|
401
100
|
async function sendGroup(chain, tracker, units, options, batchSize) {
|
|
402
101
|
const { signal, dryRun = false, wait = true, retries = 2 } = options;
|
|
102
|
+
const marginRatio = Math.max(1, options.computeUnitMargin?.ratio ?? 1.1);
|
|
103
|
+
const marginExtra = Math.max(0, Math.ceil(options.computeUnitMargin?.extra ?? 1000));
|
|
403
104
|
const payer = units[0].payer;
|
|
404
105
|
/** 有浏览器钱包等外部签名者:先准备好全部交易,再让钱包一次签完 */
|
|
405
106
|
const deferred = units.some((u) => u.payer.kind === 'wallet' || u.signers.some((s) => s.kind === 'wallet'));
|
|
@@ -451,7 +152,12 @@ async function sendGroup(chain, tracker, units, options, batchSize) {
|
|
|
451
152
|
// 模拟时计算单元上限取最大值,优先费用单价按 0(否则节点按 140 万 × 单价预扣手续费,余额少的付款方会被误判为付不起);
|
|
452
153
|
// 算出真实的上限后再按真实费用复核一次
|
|
453
154
|
const simMessage = compileMessage(payer.address, [...budgetInstructions(fixed?.computeUnits ?? MAX_COMPUTE_UNITS, fixed ? price : 0n), ...instructions], blockhash.blockhash);
|
|
454
|
-
|
|
155
|
+
// 有优先费用时同时拿付款方模拟后的余额和当前余额(和模拟合并在同一个批量请求里),判断付不付得起优先费用,付得起就不再模拟第二次
|
|
156
|
+
const affordCheck = !fixed && price > 0n;
|
|
157
|
+
const [sim, payerBefore] = await Promise.all([
|
|
158
|
+
chain.simulate(simMessage, minSlot, affordCheck ? [payer.address] : undefined),
|
|
159
|
+
affordCheck ? chain.balance(payer.address, minSlot) : null,
|
|
160
|
+
]);
|
|
455
161
|
if (sim.err) {
|
|
456
162
|
if (!waited && inFlight.some((f) => f.sent)) {
|
|
457
163
|
// 可能依赖前面还没确认的交易:等它们确认后重新模拟
|
|
@@ -489,8 +195,8 @@ async function sendGroup(chain, tracker, units, options, batchSize) {
|
|
|
489
195
|
continue;
|
|
490
196
|
}
|
|
491
197
|
const consumed = sim.unitsConsumed ?? null;
|
|
492
|
-
const limit = fixed?.computeUnits ?? (consumed ? Math.min(MAX_COMPUTE_UNITS, Math.ceil(consumed *
|
|
493
|
-
if (
|
|
198
|
+
const limit = fixed?.computeUnits ?? (consumed ? Math.min(MAX_COMPUTE_UNITS, Math.ceil(consumed * marginRatio) + marginExtra) : MAX_COMPUTE_UNITS);
|
|
199
|
+
if (affordCheck && !canAffordPriority(simMessage.signers.length, limit, price, payerBefore, sim)) {
|
|
494
200
|
const check = await chain.simulate(compileMessage(payer.address, [...budgetInstructions(limit, price), ...instructions], blockhash.blockhash), minSlot);
|
|
495
201
|
if (check.err) {
|
|
496
202
|
fail(pending, transactionErrorText(check.err, check.logs));
|
|
@@ -519,7 +225,7 @@ async function sendGroup(chain, tracker, units, options, batchSize) {
|
|
|
519
225
|
// 同样的消息签名也相同、会被节点当成同一笔:调整计算单元上限让消息不同
|
|
520
226
|
let limit = p.limit;
|
|
521
227
|
let message = compileMessage(payer.address, [...budgetInstructions(limit, p.price), ...p.instructions], p.blockhash.blockhash);
|
|
522
|
-
while (!p.fixed &&
|
|
228
|
+
while (!p.fixed && isRecentMessage(message.bytes)) {
|
|
523
229
|
limit += 1;
|
|
524
230
|
message = compileMessage(payer.address, [...budgetInstructions(limit, p.price), ...p.instructions], p.blockhash.blockhash);
|
|
525
231
|
}
|
|
@@ -669,13 +375,17 @@ async function sendGroup(chain, tracker, units, options, batchSize) {
|
|
|
669
375
|
}
|
|
670
376
|
}
|
|
671
377
|
};
|
|
672
|
-
//
|
|
378
|
+
// 打包(packUnits:相邻的调用放进同一笔,不超过交易大小 / 账户数上限;固定手续费的调用单独一笔),按顺序逐笔取出
|
|
379
|
+
const packs = packUnits(payer.address, units, { batchSize, alone: (i) => Boolean(units[i].fixed) });
|
|
380
|
+
let packIndex = 0;
|
|
381
|
+
/** 下一个还没取出的调用下标 */
|
|
673
382
|
let next = 0;
|
|
674
383
|
const nextBatch = () => {
|
|
675
|
-
while (
|
|
676
|
-
const
|
|
677
|
-
next
|
|
678
|
-
if (
|
|
384
|
+
while (packIndex < packs.length) {
|
|
385
|
+
const pack = packs[packIndex++];
|
|
386
|
+
next = pack.indices[pack.indices.length - 1] + 1;
|
|
387
|
+
if (pack.tooLarge) {
|
|
388
|
+
const first = units[pack.indices[0]];
|
|
679
389
|
update(first, { status: 'failed', error: `transaction too large (over ${PACKET_DATA_SIZE} bytes or ${MAX_ACCOUNT_LOCKS} accounts)` });
|
|
680
390
|
if (!first.continueOnError)
|
|
681
391
|
stopped = true;
|
|
@@ -683,15 +393,7 @@ async function sendGroup(chain, tracker, units, options, batchSize) {
|
|
|
683
393
|
return null;
|
|
684
394
|
continue;
|
|
685
395
|
}
|
|
686
|
-
|
|
687
|
-
while (!first.fixed && next < units.length && batch.length < batchSize) {
|
|
688
|
-
const candidate = units[next];
|
|
689
|
-
if (candidate.fixed || !fits(payer.address, [...batch, candidate]))
|
|
690
|
-
break;
|
|
691
|
-
batch.push(candidate);
|
|
692
|
-
next++;
|
|
693
|
-
}
|
|
694
|
-
return batch;
|
|
396
|
+
return pack.indices.map((i) => units[i]);
|
|
695
397
|
}
|
|
696
398
|
return null;
|
|
697
399
|
};
|