@w3lib/solana-batch-call 0.2.1 → 0.3.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 +19 -0
- package/README.md +132 -2
- package/dist/client.d.ts +4 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +8 -0
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- 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 +5 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +16 -0
- package/dist/layout.js.map +1 -1
- package/dist/lp.d.ts +123 -0
- package/dist/lp.d.ts.map +1 -0
- package/dist/lp.js +631 -0
- package/dist/lp.js.map +1 -0
- package/dist/lpMath.d.ts +37 -0
- package/dist/lpMath.d.ts.map +1 -0
- package/dist/lpMath.js +176 -0
- package/dist/lpMath.js.map +1 -0
- package/dist/rpc.d.ts +3 -1
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +70 -10
- package/dist/rpc.js.map +1 -1
- package/dist/shortcuts.d.ts +8 -0
- package/dist/shortcuts.d.ts.map +1 -1
- package/dist/shortcuts.js +1 -1
- package/dist/shortcuts.js.map +1 -1
- package/dist/subpaths/lp.d.ts +31 -0
- package/dist/subpaths/lp.d.ts.map +1 -0
- package/dist/subpaths/lp.js +33 -0
- package/dist/subpaths/lp.js.map +1 -0
- package/dist/subpaths/write.d.ts +22 -0
- package/dist/subpaths/write.d.ts.map +1 -0
- package/dist/subpaths/write.js +19 -0
- package/dist/subpaths/write.js.map +1 -0
- package/dist/write/batch.d.ts +41 -0
- package/dist/write/batch.d.ts.map +1 -0
- package/dist/write/batch.js +46 -0
- package/dist/write/batch.js.map +1 -0
- package/dist/write/collect.d.ts +50 -0
- package/dist/write/collect.d.ts.map +1 -0
- package/dist/write/collect.js +220 -0
- package/dist/write/collect.js.map +1 -0
- package/dist/write/instructions.d.ts +16 -0
- package/dist/write/instructions.d.ts.map +1 -0
- package/dist/write/instructions.js +89 -0
- package/dist/write/instructions.js.map +1 -0
- package/dist/write/send.d.ts +115 -0
- package/dist/write/send.d.ts.map +1 -0
- package/dist/write/send.js +720 -0
- package/dist/write/send.js.map +1 -0
- package/dist/write/signer.d.ts +94 -0
- package/dist/write/signer.d.ts.map +1 -0
- package/dist/write/signer.js +197 -0
- package/dist/write/signer.js.map +1 -0
- package/dist/write/transaction.d.ts +75 -0
- package/dist/write/transaction.d.ts.map +1 -0
- package/dist/write/transaction.js +133 -0
- package/dist/write/transaction.js.map +1 -0
- package/dist/write/transfer.d.ts +72 -0
- package/dist/write/transfer.d.ts.map +1 -0
- package/dist/write/transfer.js +127 -0
- package/dist/write/transfer.js.map +1 -0
- package/package.json +23 -12
- package/src/client.ts +10 -0
- package/src/index.ts +1 -0
- package/src/layout.ts +17 -0
- package/src/lp.ts +849 -0
- package/src/lpMath.ts +205 -0
- package/src/rpc.ts +77 -11
- package/src/shortcuts.ts +1 -1
- package/src/subpaths/lp.ts +62 -0
- package/src/subpaths/write.ts +29 -0
- package/src/write/batch.ts +78 -0
- package/src/write/collect.ts +286 -0
- package/src/write/instructions.ts +98 -0
- package/src/write/send.ts +876 -0
- package/src/write/signer.ts +314 -0
- package/src/write/transaction.ts +181 -0
- package/src/write/transfer.ts +189 -0
package/src/lpMath.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 集中流动性(Raydium CLMM / Orca Whirlpool)的定点数计算,与链上程序逐位一致(Q64.64,bigint)。
|
|
3
|
+
* 两个程序的 tick → 价格换算用的常量和步骤不同(结果在最后几位可能不同),各自照搬;
|
|
4
|
+
* 数量和手续费的公式两边相同:撤出流动性时向下取整,手续费增长按 u128 回绕相减。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const Q64 = 1n << 64n
|
|
8
|
+
const U128_MAX = (1n << 128n) - 1n
|
|
9
|
+
const U64_MAX = (1n << 64n) - 1n
|
|
10
|
+
|
|
11
|
+
export const MIN_TICK = -443636
|
|
12
|
+
export const MAX_TICK = 443636
|
|
13
|
+
|
|
14
|
+
function checkTick(tick: number): void {
|
|
15
|
+
if (!Number.isInteger(tick) || tick < MIN_TICK || tick > MAX_TICK) {
|
|
16
|
+
throw new RangeError(`Tick out of range: ${tick}`)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// raydium-clmm programs/amm/src/libraries/tick_math.rs(第 0 项对应 abs_tick & 0x1)
|
|
21
|
+
const RAYDIUM_RATIOS = [
|
|
22
|
+
0xfffcb933bd6fb800n,
|
|
23
|
+
0xfff97272373d4000n,
|
|
24
|
+
0xfff2e50f5f657000n,
|
|
25
|
+
0xffe5caca7e10f000n,
|
|
26
|
+
0xffcb9843d60f7000n,
|
|
27
|
+
0xff973b41fa98e800n,
|
|
28
|
+
0xff2ea16466c9b000n,
|
|
29
|
+
0xfe5dee046a9a3800n,
|
|
30
|
+
0xfcbe86c7900bb000n,
|
|
31
|
+
0xf987a7253ac65800n,
|
|
32
|
+
0xf3392b0822bb6000n,
|
|
33
|
+
0xe7159475a2caf000n,
|
|
34
|
+
0xd097f3bdfd2f2000n,
|
|
35
|
+
0xa9f746462d9f8000n,
|
|
36
|
+
0x70d869a156f31c00n,
|
|
37
|
+
0x31be135f97ed3200n,
|
|
38
|
+
0x9aa508b5b85a500n,
|
|
39
|
+
0x5d6af8dedc582cn,
|
|
40
|
+
0x2216e584f5fan,
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
/** Raydium CLMM:tick → sqrt 价格(Q64.64) */
|
|
44
|
+
export function raydiumSqrtPriceAtTick(tick: number): bigint {
|
|
45
|
+
checkTick(tick)
|
|
46
|
+
const abs = Math.abs(tick)
|
|
47
|
+
let ratio = abs & 1 ? (RAYDIUM_RATIOS[0] as bigint) : Q64
|
|
48
|
+
for (let i = 1; i < RAYDIUM_RATIOS.length; i++) {
|
|
49
|
+
if (abs & (1 << i)) {
|
|
50
|
+
ratio = (ratio * (RAYDIUM_RATIOS[i] as bigint)) >> 64n
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return tick > 0 ? U128_MAX / ratio : ratio
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// orca whirlpools programs/whirlpool/src/math/tick_math.rs:正 tick 用 Q96 常量,负 tick 用 Q64 常量
|
|
57
|
+
const ORCA_POSITIVE = [
|
|
58
|
+
79232123823359799118286999567n,
|
|
59
|
+
79236085330515764027303304731n,
|
|
60
|
+
79244008939048815603706035061n,
|
|
61
|
+
79259858533276714757314932305n,
|
|
62
|
+
79291567232598584799939703904n,
|
|
63
|
+
79355022692464371645785046466n,
|
|
64
|
+
79482085999252804386437311141n,
|
|
65
|
+
79736823300114093921829183326n,
|
|
66
|
+
80248749790819932309965073892n,
|
|
67
|
+
81282483887344747381513967011n,
|
|
68
|
+
83390072131320151908154831281n,
|
|
69
|
+
87770609709833776024991924138n,
|
|
70
|
+
97234110755111693312479820773n,
|
|
71
|
+
119332217159966728226237229890n,
|
|
72
|
+
179736315981702064433883588727n,
|
|
73
|
+
407748233172238350107850275304n,
|
|
74
|
+
2098478828474011932436660412517n,
|
|
75
|
+
55581415166113811149459800483533n,
|
|
76
|
+
38992368544603139932233054999993551n,
|
|
77
|
+
]
|
|
78
|
+
const ORCA_NEGATIVE = [
|
|
79
|
+
18445821805675392311n,
|
|
80
|
+
18444899583751176498n,
|
|
81
|
+
18443055278223354162n,
|
|
82
|
+
18439367220385604838n,
|
|
83
|
+
18431993317065449817n,
|
|
84
|
+
18417254355718160513n,
|
|
85
|
+
18387811781193591352n,
|
|
86
|
+
18329067761203520168n,
|
|
87
|
+
18212142134806087854n,
|
|
88
|
+
17980523815641551639n,
|
|
89
|
+
17526086738831147013n,
|
|
90
|
+
16651378430235024244n,
|
|
91
|
+
15030750278693429944n,
|
|
92
|
+
12247334978882834399n,
|
|
93
|
+
8131365268884726200n,
|
|
94
|
+
3584323654723342297n,
|
|
95
|
+
696457651847595233n,
|
|
96
|
+
26294789957452057n,
|
|
97
|
+
37481735321082n,
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
/** Orca Whirlpool:tick → sqrt 价格(Q64.64) */
|
|
101
|
+
export function orcaSqrtPriceAtTick(tick: number): bigint {
|
|
102
|
+
checkTick(tick)
|
|
103
|
+
if (tick >= 0) {
|
|
104
|
+
let ratio = tick & 1 ? (ORCA_POSITIVE[0] as bigint) : 1n << 96n
|
|
105
|
+
for (let i = 1; i < ORCA_POSITIVE.length; i++) {
|
|
106
|
+
if (tick & (1 << i)) {
|
|
107
|
+
ratio = (ratio * (ORCA_POSITIVE[i] as bigint)) >> 96n
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return ratio >> 32n
|
|
111
|
+
}
|
|
112
|
+
const abs = -tick
|
|
113
|
+
let ratio = abs & 1 ? (ORCA_NEGATIVE[0] as bigint) : Q64
|
|
114
|
+
for (let i = 1; i < ORCA_NEGATIVE.length; i++) {
|
|
115
|
+
if (abs & (1 << i)) {
|
|
116
|
+
ratio = (ratio * (ORCA_NEGATIVE[i] as bigint)) >> 64n
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return ratio
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 撤出全部流动性能拿到的两种代币数量(向下取整)。两个程序都按当前 tick(不是价格)判断区间:
|
|
124
|
+
* tick < 下限只有 token0;tick ≥ 上限只有 token1;否则两种都有
|
|
125
|
+
*/
|
|
126
|
+
export function getAmountsForLiquidity(tick: number, sqrtPrice: bigint, sqrtLower: bigint, sqrtUpper: bigint, liquidity: bigint, tickLower: number, tickUpper: number): [bigint, bigint] {
|
|
127
|
+
if (liquidity === 0n) {
|
|
128
|
+
return [0n, 0n]
|
|
129
|
+
}
|
|
130
|
+
if (tick < tickLower) {
|
|
131
|
+
return [amount0Delta(sqrtLower, sqrtUpper, liquidity), 0n]
|
|
132
|
+
}
|
|
133
|
+
if (tick < tickUpper) {
|
|
134
|
+
return [amount0Delta(sqrtPrice, sqrtUpper, liquidity), amount1Delta(sqrtLower, sqrtPrice, liquidity)]
|
|
135
|
+
}
|
|
136
|
+
return [0n, amount1Delta(sqrtLower, sqrtUpper, liquidity)]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** floor(L · 2^64 · (b − a) / (a · b)) */
|
|
140
|
+
function amount0Delta(a: bigint, b: bigint, liquidity: bigint): bigint {
|
|
141
|
+
const [lo, hi] = a < b ? [a, b] : [b, a]
|
|
142
|
+
return lo === 0n ? 0n : ((liquidity << 64n) * (hi - lo)) / (lo * hi)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** floor(L · (b − a) / 2^64) */
|
|
146
|
+
function amount1Delta(a: bigint, b: bigint, liquidity: bigint): bigint {
|
|
147
|
+
const [lo, hi] = a < b ? [a, b] : [b, a]
|
|
148
|
+
return (liquidity * (hi - lo)) >> 64n
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 一个 tick 上用于计算手续费的数据;Orca 的 tick 未初始化时 initialized 为 false */
|
|
152
|
+
export interface TickFeeData {
|
|
153
|
+
initialized: boolean
|
|
154
|
+
feeGrowthOutside0: bigint
|
|
155
|
+
feeGrowthOutside1: bigint
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const wrap = (x: bigint) => ((x % (1n << 128n)) + (1n << 128n)) % (1n << 128n)
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 区间内的手续费增长(u128 回绕)。Raydium 不区分是否初始化(未初始化的 tick 各项为 0,传 initialized: true 即可);
|
|
162
|
+
* Orca 未初始化的 tick:下限按“下方增长 = 全局”、上限按“上方增长 = 0”处理(与程序一致)
|
|
163
|
+
*/
|
|
164
|
+
export function getFeeGrowthInside(
|
|
165
|
+
tick: number,
|
|
166
|
+
tickLower: number,
|
|
167
|
+
lower: TickFeeData,
|
|
168
|
+
tickUpper: number,
|
|
169
|
+
upper: TickFeeData,
|
|
170
|
+
global0: bigint,
|
|
171
|
+
global1: bigint,
|
|
172
|
+
): [bigint, bigint] {
|
|
173
|
+
const below = !lower.initialized
|
|
174
|
+
? [global0, global1]
|
|
175
|
+
: tick < tickLower
|
|
176
|
+
? [wrap(global0 - lower.feeGrowthOutside0), wrap(global1 - lower.feeGrowthOutside1)]
|
|
177
|
+
: [lower.feeGrowthOutside0, lower.feeGrowthOutside1]
|
|
178
|
+
const above = !upper.initialized
|
|
179
|
+
? [0n, 0n]
|
|
180
|
+
: tick < tickUpper
|
|
181
|
+
? [upper.feeGrowthOutside0, upper.feeGrowthOutside1]
|
|
182
|
+
: [wrap(global0 - upper.feeGrowthOutside0), wrap(global1 - upper.feeGrowthOutside1)]
|
|
183
|
+
return [wrap(global0 - (below[0] as bigint) - (above[0] as bigint)), wrap(global1 - (below[1] as bigint) - (above[1] as bigint))]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 未领取的手续费:已记账的 owed + L · (区间内增长 − 上次记录) / 2^64(向下取整)。
|
|
188
|
+
* 新增部分超过 u64 时:Orca 程序记为 0(checked_mul_shift_right 失败),这里同样记为 0(正常仓位不会碰到)
|
|
189
|
+
*/
|
|
190
|
+
export function getFeesOwed(owed: bigint, liquidity: bigint, growthInside: bigint, growthLast: bigint): bigint {
|
|
191
|
+
const delta = (liquidity * wrap(growthInside - growthLast)) >> 64n
|
|
192
|
+
return owed + (delta > U64_MAX ? 0n : delta)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Q64.64 sqrt 价格 → token1 / token0 的价格(按精度换算,浮点数,只用于展示) */
|
|
196
|
+
export function sqrtPriceToPrice(sqrtPrice: bigint, decimals0: number, decimals1: number): number {
|
|
197
|
+
// 先取高位再转浮点,避免 2^128 级别的整数转换丢精度后再平方
|
|
198
|
+
const ratio = Number(sqrtPrice) / 2 ** 64
|
|
199
|
+
return ratio * ratio * 10 ** (decimals0 - decimals1)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** tick → token1 / token0 的价格(1.0001^tick,按精度换算,只用于展示) */
|
|
203
|
+
export function tickToPrice(tick: number, decimals0: number, decimals1: number): number {
|
|
204
|
+
return 1.0001 ** tick * 10 ** (decimals0 - decimals1)
|
|
205
|
+
}
|
package/src/rpc.ts
CHANGED
|
@@ -17,18 +17,49 @@ export class RpcError extends Error {
|
|
|
17
17
|
this.name = 'RpcError'
|
|
18
18
|
this.code = code
|
|
19
19
|
this.data = data
|
|
20
|
+
const kind = classifyRpcError(code, message)
|
|
20
21
|
this.nodeFault =
|
|
21
22
|
code === 403 ||
|
|
22
|
-
code === 429 ||
|
|
23
23
|
code === -32005 ||
|
|
24
|
-
// 节点高度没到 minContextSlot
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
// 节点高度没到 minContextSlot、限频、服务端临时故障(繁忙、过载、暂不可用、超时):换节点可能成功;
|
|
25
|
+
// 临时故障也不应据此关闭批量(降级只会加重负载)
|
|
26
|
+
kind === 'min-slot' ||
|
|
27
|
+
kind === 'rate-limited' ||
|
|
28
|
+
kind === 'transient' ||
|
|
29
|
+
(/forbidden|blocked|personal token|api.?key|too many|not allowed|unauthori[sz]ed|limit exceeded|disabled|not available|not supported/i.test(message) &&
|
|
30
|
+
kind !== 'too-many-accounts')
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* JSON-RPC 错误的分类(RpcError.nodeFault、HTTP 5xx 的处理共用,避免两份清单各自维护):
|
|
36
|
+
* - method-limit / too-many-accounts:节点对批量 / 单次账户数的限制,按限制重新分批或缩小
|
|
37
|
+
* - invalid-params:参数错误(-32602),确定性的
|
|
38
|
+
* - min-slot / rate-limited / transient:节点落后、限频、服务端临时故障,换节点
|
|
39
|
+
* - internal:-32603 internal error,含义不明确(可能是临时故障,也可能是节点拒绝批量)
|
|
40
|
+
*/
|
|
41
|
+
type RpcErrorKind = 'method-limit' | 'too-many-accounts' | 'invalid-params' | 'min-slot' | 'rate-limited' | 'transient' | 'internal' | 'other'
|
|
42
|
+
|
|
43
|
+
function classifyRpcError(code: number | undefined, message: string): RpcErrorKind {
|
|
44
|
+
if (METHOD_LIMIT.test(message)) return 'method-limit'
|
|
45
|
+
if (TOO_MANY_ACCOUNTS.test(message)) return 'too-many-accounts'
|
|
46
|
+
if (code === -32602) return 'invalid-params'
|
|
47
|
+
if (code === MIN_CONTEXT_SLOT_NOT_REACHED || /minimum context slot/i.test(message)) return 'min-slot'
|
|
48
|
+
if (code === 429 || RATE_LIMITED.test(message)) return 'rate-limited'
|
|
49
|
+
if (TRANSIENT.test(message)) return 'transient'
|
|
50
|
+
if (code === -32603) return 'internal'
|
|
51
|
+
return 'other'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 节点限制单个方法在批量里的数量(如 publicnode:“Maximum number of 'getMultipleAccounts' calls in a batch request is 1”) */
|
|
55
|
+
const METHOD_LIMIT = /maximum number of '([^']+)' calls in a batch request is (\d+)/i
|
|
56
|
+
|
|
57
|
+
/** 限频的报错 */
|
|
58
|
+
const RATE_LIMITED = /rate.?limit|too many requests/i
|
|
59
|
+
|
|
60
|
+
/** 服务端临时故障的报错(如 -32000 server is busy、-32603 + 503 overloaded) */
|
|
61
|
+
const TRANSIENT = /\bbusy\b|overload|temporar(il)?y|unavailable|try again|timed? ?out|timeout/i
|
|
62
|
+
|
|
32
63
|
/** 节点限制单次 getMultipleAccounts 的账户数(如 “Too many accounts requested”):缩小每次的账户数后重试,不是节点故障 */
|
|
33
64
|
const TOO_MANY_ACCOUNTS = /too many accounts/i
|
|
34
65
|
|
|
@@ -38,11 +69,15 @@ export class HttpError extends Error {
|
|
|
38
69
|
/** 请求超时(HttpRpcOptions.timeout) */
|
|
39
70
|
readonly timeout: boolean
|
|
40
71
|
|
|
41
|
-
|
|
72
|
+
/** 5xx 的响应体是 JSON-RPC 错误(服务端故障,或节点用 5xx 拒绝批量请求) */
|
|
73
|
+
readonly rpcError: boolean
|
|
74
|
+
|
|
75
|
+
constructor(message: string, status?: number, timeout = false, rpcError = false) {
|
|
42
76
|
super(message)
|
|
43
77
|
this.name = 'HttpError'
|
|
44
78
|
this.status = status
|
|
45
79
|
this.timeout = timeout
|
|
80
|
+
this.rpcError = rpcError
|
|
46
81
|
}
|
|
47
82
|
}
|
|
48
83
|
|
|
@@ -262,7 +297,7 @@ export class HttpRpc implements RpcTransport {
|
|
|
262
297
|
* 记下限制并按限制重新分批,返回 true;限制解析不出或这批本来就没超限(避免无限重发)时返回 false
|
|
263
298
|
*/
|
|
264
299
|
async #retryWithMethodLimit(items: Pending[], message: string | undefined): Promise<boolean> {
|
|
265
|
-
const match =
|
|
300
|
+
const match = METHOD_LIMIT.exec(message ?? '')
|
|
266
301
|
const method = match?.[1]
|
|
267
302
|
const limit = Number(match?.[2])
|
|
268
303
|
if (!method || !Number.isFinite(limit) || limit < 1 || items.filter((item) => item.method === method).length <= limit) {
|
|
@@ -273,7 +308,8 @@ export class HttpRpc implements RpcTransport {
|
|
|
273
308
|
return true
|
|
274
309
|
}
|
|
275
310
|
|
|
276
|
-
|
|
311
|
+
/** retriedServerError:这一批已经因 5xx + JSON-RPC 错误重试过一次 */
|
|
312
|
+
async #dispatch(items: Pending[], retriedServerError = false): Promise<void> {
|
|
277
313
|
if (items.length === 1 || !this.#batch) {
|
|
278
314
|
await Promise.all(items.map((item) => this.#single(item)))
|
|
279
315
|
return
|
|
@@ -296,6 +332,23 @@ export class HttpRpc implements RpcTransport {
|
|
|
296
332
|
await Promise.all(items.map((item) => this.#single(item)))
|
|
297
333
|
return
|
|
298
334
|
}
|
|
335
|
+
// 5xx + -32603(含义不明确:临时故障,或节点用 5xx 拒绝批量):先原样重试一次批量(偶发故障这次就成功了,不降级);
|
|
336
|
+
// 仍然失败时只用第一条单独试探——成功说明是批量本身被拒,暂停批量(10 分钟后重试)并逐条发送其余的;
|
|
337
|
+
// 失败则是临时故障,其余的直接报错(换节点),不给出问题的节点加量
|
|
338
|
+
if (err instanceof HttpError && err.rpcError && items.length > 1) {
|
|
339
|
+
if (!retriedServerError) {
|
|
340
|
+
await this.#dispatch(items, true)
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
const [first, ...rest] = items as [Pending, ...Pending[]]
|
|
344
|
+
if (await this.#single(first)) {
|
|
345
|
+
this.#disableBatch()
|
|
346
|
+
await Promise.all(rest.map((item) => this.#single(item)))
|
|
347
|
+
} else {
|
|
348
|
+
rest.forEach((item) => item.reject(err))
|
|
349
|
+
}
|
|
350
|
+
return
|
|
351
|
+
}
|
|
299
352
|
items.forEach((item) => item.reject(err))
|
|
300
353
|
return
|
|
301
354
|
}
|
|
@@ -322,7 +375,7 @@ export class HttpRpc implements RpcTransport {
|
|
|
322
375
|
}
|
|
323
376
|
const byId = new Map((body as RpcResponse[]).map((res) => [res.id, res]))
|
|
324
377
|
// 有的节点把 “方法数量超限” 放在批量结果的某一项里
|
|
325
|
-
const methodLimited = (body as RpcResponse[]).find((res) =>
|
|
378
|
+
const methodLimited = (body as RpcResponse[]).find((res) => METHOD_LIMIT.test(res.error?.message ?? ''))
|
|
326
379
|
if (methodLimited && (await this.#retryWithMethodLimit(items, methodLimited.error?.message))) {
|
|
327
380
|
return
|
|
328
381
|
}
|
|
@@ -363,12 +416,15 @@ export class HttpRpc implements RpcTransport {
|
|
|
363
416
|
await Promise.all(this.#split(items).map((chunk) => this.#dispatch(chunk)))
|
|
364
417
|
}
|
|
365
418
|
|
|
366
|
-
|
|
419
|
+
/** 逐条发送一个调用;返回是否拿到了结果(没有出错) */
|
|
420
|
+
async #single(item: Pending, retries = this.#retries): Promise<boolean> {
|
|
367
421
|
try {
|
|
368
422
|
const body = (await this.#post({ jsonrpc: '2.0', id: nextId++, method: item.method, params: item.params }, retries)) as RpcResponse
|
|
369
423
|
settle(item, body)
|
|
424
|
+
return !body?.error
|
|
370
425
|
} catch (err) {
|
|
371
426
|
item.reject(err)
|
|
427
|
+
return false
|
|
372
428
|
}
|
|
373
429
|
}
|
|
374
430
|
|
|
@@ -414,6 +470,16 @@ export class HttpRpc implements RpcTransport {
|
|
|
414
470
|
if (!response.ok && !Array.isArray(body) && !(body as RpcResponse)?.error) {
|
|
415
471
|
throw new HttpError(`HTTP ${response.status} from ${this.#safeUrl}`, response.status)
|
|
416
472
|
}
|
|
473
|
+
// 5xx + 单个 JSON-RPC 错误:
|
|
474
|
+
// - 繁忙、过载等明确的临时故障:按 HTTP 错误处理,直接换节点(不重试、不逐条,不给过载的节点加量)
|
|
475
|
+
// - -32603 internal error:含义不明确,标记 rpcError,由 #dispatch 判断是临时故障还是节点用 5xx 拒绝批量
|
|
476
|
+
// 其他错误(方法数量超限、账户数超限、参数错误、minContextSlot、限频等)照常交给上层按类型处理
|
|
477
|
+
const bodyError = response.status >= 500 && !Array.isArray(body) ? (body as RpcResponse)?.error : undefined
|
|
478
|
+
const kind = bodyError ? classifyRpcError(bodyError.code, bodyError.message ?? '') : undefined
|
|
479
|
+
if (kind === 'transient' || kind === 'internal') {
|
|
480
|
+
const message = bodyError?.message
|
|
481
|
+
throw new HttpError(`HTTP ${response.status} from ${this.#safeUrl}${message ? `: ${message}` : ''}`, response.status, false, kind === 'internal')
|
|
482
|
+
}
|
|
417
483
|
// 有的节点限频时返回 HTTP 200 + JSON-RPC 错误码 429:同样退避重试
|
|
418
484
|
if (attempt < retries && isRateLimited(body)) {
|
|
419
485
|
await new Promise((r) => setTimeout(r, 500 * 2 ** attempt))
|
package/src/shortcuts.ts
CHANGED
|
@@ -81,7 +81,7 @@ function sourceKey(source: ShortcutOptions['provider']): string | object | null
|
|
|
81
81
|
* 拆出节点参数和函数自己的参数(ownKeys),其余作为客户端配置;值为 undefined 的参数忽略。
|
|
82
82
|
* signal 总是作为函数自己的参数(放进客户端配置会让每次调用都新建客户端)
|
|
83
83
|
*/
|
|
84
|
-
function resolve<T extends ShortcutOptions, K extends keyof T & string = never>(
|
|
84
|
+
export function resolve<T extends ShortcutOptions, K extends keyof T & string = never>(
|
|
85
85
|
options: T,
|
|
86
86
|
ownKeys: readonly K[] = [],
|
|
87
87
|
): { client: SolanaClient; own: Pick<T, K> } {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LP 仓位(子路径 `@w3lib/solana-batch-call/lp`):Raydium CLMM / Orca Whirlpool 的仓位 NFT,Raydium AMM v4 / CPMM 的 LP 代币,
|
|
3
|
+
* 代币数量、未领取的手续费、可选的美元价值。不放在主入口,不用时打包不会带上这部分代码。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { getLpPositions } from '@w3lib/solana-batch-call/lp'
|
|
7
|
+
*
|
|
8
|
+
* await getLpPositions(owner, { provider: 'https://my-rpc…' }) // 扫描全部代币账户(需要支持 getTokenAccountsByOwner 的节点)
|
|
9
|
+
* await getLpPositions(owner, { mints: [positionNftMint], prices: true }) // 只看这些代币:免费公共节点也能用
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { lpPositions, type LpPositionsOptions, type SolanaLpPosition } from '../lp.js'
|
|
13
|
+
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
ORCA_WHIRLPOOL_PROGRAM_ID,
|
|
17
|
+
RAYDIUM_AMM_PROGRAM_ID,
|
|
18
|
+
RAYDIUM_CLMM_PROGRAM_ID,
|
|
19
|
+
RAYDIUM_CPMM_PROGRAM_ID,
|
|
20
|
+
SOLANA_LP_PROTOCOLS,
|
|
21
|
+
defillamaPrices,
|
|
22
|
+
lpPositions,
|
|
23
|
+
type LpPositionsOptions,
|
|
24
|
+
type SolanaLpAmmPosition,
|
|
25
|
+
type SolanaLpClmmPosition,
|
|
26
|
+
type SolanaLpPosition,
|
|
27
|
+
type SolanaLpProtocol,
|
|
28
|
+
type SolanaLpToken,
|
|
29
|
+
type SolanaPriceSource,
|
|
30
|
+
} from '../lp.js'
|
|
31
|
+
export {
|
|
32
|
+
MAX_TICK,
|
|
33
|
+
MIN_TICK,
|
|
34
|
+
getAmountsForLiquidity,
|
|
35
|
+
getFeeGrowthInside,
|
|
36
|
+
getFeesOwed,
|
|
37
|
+
orcaSqrtPriceAtTick,
|
|
38
|
+
raydiumSqrtPriceAtTick,
|
|
39
|
+
sqrtPriceToPrice,
|
|
40
|
+
tickToPrice,
|
|
41
|
+
type TickFeeData,
|
|
42
|
+
} from '../lpMath.js'
|
|
43
|
+
|
|
44
|
+
export interface GetLpPositionsOptions extends LpPositionsOptions, ShortcutOptions {}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 查一个地址的 LP 仓位。
|
|
48
|
+
*
|
|
49
|
+
* ```ts
|
|
50
|
+
* await getLpPositions(owner, { provider: 'https://my-rpc…' })
|
|
51
|
+
* // [{ type: 'clmm', protocol: 'raydium-clmm', pool, positionMint, token0: { symbol: 'SOL', … }, token1: { symbol: 'USDC', … },
|
|
52
|
+
* // amount0Formatted: '140.26', amount1Formatted: '6852.2', fees0Formatted: '0.546', fees1Formatted: '59.5', inRange: true, … },
|
|
53
|
+
* // { type: 'amm', protocol: 'raydium-amm', pool, lpMint, balance, share: 0.18, amount0Formatted, amount1Formatted, … }]
|
|
54
|
+
* await getLpPositions(owner, { mints: [nftMint, lpMint], pools: [ammPool] }) // 免费公共节点:指定代币和池子
|
|
55
|
+
* await getLpPositions(owner, { provider, prices: true }) // 带美元价值(DefiLlama),按价值排序
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function getLpPositions(owner: string, options: GetLpPositionsOptions = {}): Promise<SolanaLpPosition[]> {
|
|
59
|
+
// fetch 同时给节点请求(客户端配置)和查价用,与其他快捷函数一致
|
|
60
|
+
const { client, own } = resolve(options, ['protocols', 'mints', 'pools', 'includeClosed', 'prices', 'onPriceError', 'signal'])
|
|
61
|
+
return lpPositions(client, owner, { ...own, ...(options.fetch ? { fetch: options.fetch } : {}) })
|
|
62
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 批量写操作(子路径 `@w3lib/solana-batch-call/write`):批量调用程序、一对多转账、多钱包归集。
|
|
3
|
+
* 不放在主入口,只做查询时打包不会带上这部分代码。不依赖 Solana SDK,签名用运行环境的 WebCrypto(Ed25519)。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { SolanaClient, NATIVE_MINT } from '@w3lib/solana-batch-call'
|
|
7
|
+
* import { batchCall, batchCollect, batchTransfer } from '@w3lib/solana-batch-call/write'
|
|
8
|
+
*
|
|
9
|
+
* const client = new SolanaClient(rpcUrl)
|
|
10
|
+
* await batchTransfer(client, secretKey, [{ to: a, formatted: '0.5' }, { to: b, token: USDC, formatted: '20' }])
|
|
11
|
+
* await batchCollect(client, [key1, key2], hotWallet, { tokens: [USDC, NATIVE_MINT], feePayer: gasKey })
|
|
12
|
+
* await batchCall(client, [{ from: key1, instructions: [ix1, ix2] }])
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export { batchCall, type BatchCallMode, type BatchCallOptions, type WriteCall } from '../write/batch.js'
|
|
16
|
+
export { batchCollect, type BatchCollectOptions, type CollectItemResult, type CollectResult } from '../write/collect.js'
|
|
17
|
+
export { batchTransfer, type BatchTransferOptions, type TransferItem, type TransferMode, type TransferResult } from '../write/transfer.js'
|
|
18
|
+
export {
|
|
19
|
+
COMPUTE_BUDGET_PROGRAM_ID,
|
|
20
|
+
closeAccount,
|
|
21
|
+
createAssociatedTokenAccountIdempotent,
|
|
22
|
+
setComputeUnitLimit,
|
|
23
|
+
setComputeUnitPrice,
|
|
24
|
+
systemTransfer,
|
|
25
|
+
transferChecked,
|
|
26
|
+
} from '../write/instructions.js'
|
|
27
|
+
export type { SendOptions, WriteResult, WriteStatus } from '../write/send.js'
|
|
28
|
+
export type { SignerInput, WriteSigner } from '../write/signer.js'
|
|
29
|
+
export type { AccountMeta, Instruction, InstructionInput } from '../write/transaction.js'
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { SolanaClient } from '../client.js'
|
|
2
|
+
import { emptyOutcome, sendUnits, toWriteResult, type SendOptions, type Unit, type WriteResult } from './send.js'
|
|
3
|
+
import { resolveSigner, type SignerInput } from './signer.js'
|
|
4
|
+
import { COMPUTE_BUDGET_PROGRAM_ID } from './instructions.js'
|
|
5
|
+
import { normalizeInstruction, type InstructionInput } from './transaction.js'
|
|
6
|
+
|
|
7
|
+
/** 一个写调用:一组要原子执行的指令 */
|
|
8
|
+
export interface WriteCall {
|
|
9
|
+
/** 发送的钱包(指令里的签名者之一);不传 feePayer 时也由它付手续费 */
|
|
10
|
+
from: SignerInput
|
|
11
|
+
/** 指令:@solana/web3.js 的 TransactionInstruction、@solana/kit 的 Instruction 或本库格式,一个或多个 */
|
|
12
|
+
instructions: InstructionInput | readonly InstructionInput[]
|
|
13
|
+
/** 指令需要的其他签名者(如新建账户的 Keypair) */
|
|
14
|
+
signers?: readonly SignerInput[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* - 'pack'(默认):同一手续费支付者的相邻调用打包进同一笔交易(不超过 1232 字节、64 个账户),
|
|
19
|
+
* 模拟时失败的调用会被剔除、其余照常发送;同一笔交易上链后才失败时一起回滚
|
|
20
|
+
* - 'tx':每个调用一笔交易
|
|
21
|
+
*/
|
|
22
|
+
export type BatchCallMode = 'pack' | 'tx'
|
|
23
|
+
|
|
24
|
+
export interface BatchCallOptions extends SendOptions {
|
|
25
|
+
mode?: BatchCallMode
|
|
26
|
+
/** 'pack' 时每笔交易最多几个调用,默认不限(只受交易大小限制) */
|
|
27
|
+
batchSize?: number
|
|
28
|
+
/**
|
|
29
|
+
* 某个调用发送前失败(模拟失败等)后,同一支付者后面的调用是否继续。默认 false(后面的标记为 skipped)。
|
|
30
|
+
* 交易不等上一笔确认就连续发出,已经发出、之后上链失败的交易拦不住后面已发出的交易
|
|
31
|
+
*/
|
|
32
|
+
continueOnError?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 批量调用程序(写操作)。结果与 calls 一一对应。
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* await batchCall(client, [
|
|
40
|
+
* { from: key1, instructions: [ix1, ix2] }, // 同一个调用里的指令原子执行
|
|
41
|
+
* { from: key2, instructions: ix3 },
|
|
42
|
+
* ], { feePayer: gasKey }) // 手续费都由 gasKey 付,不同钱包的调用也能打包进同一笔交易
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export async function batchCall(client: SolanaClient, calls: readonly WriteCall[], options: BatchCallOptions = {}): Promise<WriteResult[]> {
|
|
46
|
+
const cache = new Map()
|
|
47
|
+
const feePayer = options.feePayer === undefined ? undefined : await resolveSigner(options.feePayer, cache)
|
|
48
|
+
const prepared = await Promise.all(
|
|
49
|
+
calls.map(async (call, i) => {
|
|
50
|
+
const from = await resolveSigner(call.from, cache)
|
|
51
|
+
const signers = [from, ...(await Promise.all((call.signers ?? []).map((s) => resolveSigner(s, cache))))]
|
|
52
|
+
const list = Array.isArray(call.instructions) ? (call.instructions as readonly InstructionInput[]) : [call.instructions as InstructionInput]
|
|
53
|
+
// 库会自动加计算单元上限和优先费用指令;调用里自带的同类指令去掉(重复会让整笔交易失败),优先费用用 priorityFee 选项
|
|
54
|
+
const instructions = list.map(normalizeInstruction).filter((ix) => !(ix.programId === COMPUTE_BUDGET_PROGRAM_ID && (ix.data[0] === 2 || ix.data[0] === 3)))
|
|
55
|
+
const payer = feePayer ?? from
|
|
56
|
+
const known = new Set([payer.address, ...signers.map((s) => s.address)])
|
|
57
|
+
const missing = instructions.flatMap((ix) => ix.keys).find((k) => k.isSigner && !known.has(k.pubkey))
|
|
58
|
+
if (missing) {
|
|
59
|
+
throw new Error(`calls[${i}]: an instruction requires the signature of ${missing.pubkey}; pass it in signers`)
|
|
60
|
+
}
|
|
61
|
+
return { from, signers, instructions, payer }
|
|
62
|
+
}),
|
|
63
|
+
)
|
|
64
|
+
const results = prepared.map((p, i) => toWriteResult(i, p.from.address, emptyOutcome('skipped')))
|
|
65
|
+
const units: Unit[] = prepared.map((p, i) => ({
|
|
66
|
+
instructions: p.instructions,
|
|
67
|
+
payer: p.payer,
|
|
68
|
+
signers: p.signers.filter((s) => s.address !== p.payer.address),
|
|
69
|
+
continueOnError: options.continueOnError,
|
|
70
|
+
onUpdate: (outcome) => {
|
|
71
|
+
results[i] = toWriteResult(i, p.from.address, outcome)
|
|
72
|
+
options.onUpdate?.(results[i] as WriteResult)
|
|
73
|
+
},
|
|
74
|
+
}))
|
|
75
|
+
await sendUnits(client, units, options, { batchSize: (options.mode ?? 'pack') === 'tx' ? 1 : options.batchSize })
|
|
76
|
+
return results
|
|
77
|
+
}
|
|
78
|
+
|