@w3lib/solana-batch-call 0.2.2 → 0.4.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 +26 -0
- package/README.md +205 -2
- package/dist/address.d.ts +2 -0
- package/dist/address.d.ts.map +1 -1
- package/dist/address.js +8 -0
- package/dist/address.js.map +1 -1
- 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 +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- 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/liquidity/amm.d.ts +7 -0
- package/dist/liquidity/amm.d.ts.map +1 -0
- package/dist/liquidity/amm.js +307 -0
- package/dist/liquidity/amm.js.map +1 -0
- package/dist/liquidity/clmm.d.ts +18 -0
- package/dist/liquidity/clmm.d.ts.map +1 -0
- package/dist/liquidity/clmm.js +85 -0
- package/dist/liquidity/clmm.js.map +1 -0
- package/dist/liquidity/common.d.ts +94 -0
- package/dist/liquidity/common.d.ts.map +1 -0
- package/dist/liquidity/common.js +228 -0
- package/dist/liquidity/common.js.map +1 -0
- package/dist/liquidity/index.d.ts +34 -0
- package/dist/liquidity/index.d.ts.map +1 -0
- package/dist/liquidity/index.js +157 -0
- package/dist/liquidity/index.js.map +1 -0
- package/dist/liquidity/orca.d.ts +6 -0
- package/dist/liquidity/orca.d.ts.map +1 -0
- package/dist/liquidity/orca.js +303 -0
- package/dist/liquidity/orca.js.map +1 -0
- package/dist/liquidity/raydiumClmm.d.ts +6 -0
- package/dist/liquidity/raydiumClmm.d.ts.map +1 -0
- package/dist/liquidity/raydiumClmm.js +307 -0
- package/dist/liquidity/raydiumClmm.js.map +1 -0
- package/dist/liquidity/types.d.ts +150 -0
- package/dist/liquidity/types.d.ts.map +1 -0
- package/dist/liquidity/types.js +2 -0
- package/dist/liquidity/types.js.map +1 -0
- package/dist/lp.d.ts +168 -0
- package/dist/lp.d.ts.map +1 -0
- package/dist/lp.js +632 -0
- package/dist/lp.js.map +1 -0
- package/dist/lpMath.d.ts +82 -0
- package/dist/lpMath.d.ts.map +1 -0
- package/dist/lpMath.js +365 -0
- package/dist/lpMath.js.map +1 -0
- 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/liquidity.d.ts +23 -0
- package/dist/subpaths/liquidity.d.ts.map +1 -0
- package/dist/subpaths/liquidity.js +19 -0
- package/dist/subpaths/liquidity.js.map +1 -0
- 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 -5
- package/src/address.ts +9 -0
- package/src/client.ts +10 -0
- package/src/index.ts +2 -1
- package/src/layout.ts +17 -0
- package/src/liquidity/amm.ts +353 -0
- package/src/liquidity/clmm.ts +108 -0
- package/src/liquidity/common.ts +285 -0
- package/src/liquidity/index.ts +177 -0
- package/src/liquidity/orca.ts +344 -0
- package/src/liquidity/raydiumClmm.ts +348 -0
- package/src/liquidity/types.ts +158 -0
- package/src/lp.ts +850 -0
- package/src/lpMath.ts +420 -0
- package/src/shortcuts.ts +1 -1
- package/src/subpaths/liquidity.ts +45 -0
- package/src/subpaths/lp.ts +71 -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,420 @@
|
|
|
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
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// 添加 / 移除流动性用到的计算(两个程序公式相同,tick ↔ 价格按各自的换算)
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
/** 两个程序允许的 sqrt 价格范围(Raydium:MIN / MAX_SQRT_PRICE_X64;Orca:MIN / MAX_SQRT_PRICE) */
|
|
212
|
+
export const RAYDIUM_MIN_SQRT_PRICE = 4295048016n
|
|
213
|
+
export const RAYDIUM_MAX_SQRT_PRICE = 79226673521066979257578248091n
|
|
214
|
+
export const ORCA_MIN_SQRT_PRICE = 4295048016n
|
|
215
|
+
export const ORCA_MAX_SQRT_PRICE = 79226673515401279992447579055n
|
|
216
|
+
|
|
217
|
+
export type ClmmProgram = 'raydium' | 'orca'
|
|
218
|
+
|
|
219
|
+
const sqrtAtTick = (program: ClmmProgram) => (program === 'raydium' ? raydiumSqrtPriceAtTick : orcaSqrtPriceAtTick)
|
|
220
|
+
const sqrtRange = (program: ClmmProgram): [bigint, bigint] => (program === 'raydium' ? [RAYDIUM_MIN_SQRT_PRICE, RAYDIUM_MAX_SQRT_PRICE] : [ORCA_MIN_SQRT_PRICE, ORCA_MAX_SQRT_PRICE])
|
|
221
|
+
|
|
222
|
+
/** tick → sqrt 价格(按程序) */
|
|
223
|
+
export function sqrtPriceAtTick(program: ClmmProgram, tick: number): bigint {
|
|
224
|
+
return sqrtAtTick(program)(tick)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** sqrt 价格 → tick:满足 sqrtPriceAtTick(tick) <= sqrtPrice 的最大 tick(二分查找,与程序的 get_tick_at_sqrt_price 一致),限制在 [MIN_TICK, MAX_TICK] 内 */
|
|
228
|
+
export function tickAtSqrtPrice(program: ClmmProgram, sqrtPrice: bigint): number {
|
|
229
|
+
const at = sqrtAtTick(program)
|
|
230
|
+
if (sqrtPrice < at(MIN_TICK)) {
|
|
231
|
+
return MIN_TICK
|
|
232
|
+
}
|
|
233
|
+
let lo = MIN_TICK
|
|
234
|
+
let hi = MAX_TICK
|
|
235
|
+
while (lo < hi) {
|
|
236
|
+
const mid = Math.floor((lo + hi + 1) / 2)
|
|
237
|
+
if (at(mid) <= sqrtPrice) {
|
|
238
|
+
lo = mid
|
|
239
|
+
} else {
|
|
240
|
+
hi = mid - 1
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return lo
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** 向上取整的整数除法 */
|
|
247
|
+
export const ceilDiv = (a: bigint, b: bigint) => (a + b - 1n) / b
|
|
248
|
+
|
|
249
|
+
/** 添加流动性 L 时程序收取的 token0(向上取整):ceil(L · 2^64 · (b − a) / (a · b)) */
|
|
250
|
+
function amount0Up(a: bigint, b: bigint, liquidity: bigint): bigint {
|
|
251
|
+
const [lo, hi] = a < b ? [a, b] : [b, a]
|
|
252
|
+
return ceilDiv((liquidity << 64n) * (hi - lo), lo * hi)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** 添加流动性 L 时程序收取的 token1(向上取整):ceil(L · (b − a) / 2^64) */
|
|
256
|
+
function amount1Up(a: bigint, b: bigint, liquidity: bigint): bigint {
|
|
257
|
+
const [lo, hi] = a < b ? [a, b] : [b, a]
|
|
258
|
+
return ceilDiv(liquidity * (hi - lo), Q64)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** 添加流动性 L 时程序收取的两种代币数量(向上取整;按当前 tick 判断区间,与程序一致) */
|
|
262
|
+
export function getMintAmounts(tick: number, sqrtPrice: bigint, sqrtLower: bigint, sqrtUpper: bigint, liquidity: bigint, tickLower: number, tickUpper: number): [bigint, bigint] {
|
|
263
|
+
if (liquidity === 0n) {
|
|
264
|
+
return [0n, 0n]
|
|
265
|
+
}
|
|
266
|
+
if (tick < tickLower) {
|
|
267
|
+
return [amount0Up(sqrtLower, sqrtUpper, liquidity), 0n]
|
|
268
|
+
}
|
|
269
|
+
if (tick < tickUpper) {
|
|
270
|
+
return [amount0Up(sqrtPrice, sqrtUpper, liquidity), amount1Up(sqrtLower, sqrtPrice, liquidity)]
|
|
271
|
+
}
|
|
272
|
+
return [0n, amount1Up(sqrtLower, sqrtUpper, liquidity)]
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** 区间 [a, b] 内 amount0 个 token0 对应的流动性(向下取整):amount0 · (a · b / 2^64) / (b − a) */
|
|
276
|
+
export function getLiquidityForAmount0(a: bigint, b: bigint, amount0: bigint): bigint {
|
|
277
|
+
const [lo, hi] = a < b ? [a, b] : [b, a]
|
|
278
|
+
return (amount0 * ((lo * hi) / Q64)) / (hi - lo)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** 区间 [a, b] 内 amount1 个 token1 对应的流动性(向下取整):amount1 · 2^64 / (b − a) */
|
|
282
|
+
export function getLiquidityForAmount1(a: bigint, b: bigint, amount1: bigint): bigint {
|
|
283
|
+
const [lo, hi] = a < b ? [a, b] : [b, a]
|
|
284
|
+
return (amount1 * Q64) / (hi - lo)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 最多用 amount0 / amount1 时能添加的流动性:两边都给时取较小的;只给一边时只按这一边算(另一边要多少由 getMintAmounts 算)。
|
|
289
|
+
* 区间在当前价格之外时只需要一种代币,给的是另一种时返回 0
|
|
290
|
+
*/
|
|
291
|
+
export function getLiquidityForAmounts(
|
|
292
|
+
tick: number,
|
|
293
|
+
sqrtPrice: bigint,
|
|
294
|
+
sqrtLower: bigint,
|
|
295
|
+
sqrtUpper: bigint,
|
|
296
|
+
amount0: bigint | undefined,
|
|
297
|
+
amount1: bigint | undefined,
|
|
298
|
+
tickLower: number,
|
|
299
|
+
tickUpper: number,
|
|
300
|
+
): bigint {
|
|
301
|
+
let l0: bigint | null = null
|
|
302
|
+
let l1: bigint | null = null
|
|
303
|
+
// 价格正好在下限(tick == tickLower 且 sqrtPrice == sqrtLower)时也只需要 token0:按下方处理,避免除以 0
|
|
304
|
+
if (tick < tickLower || sqrtPrice <= sqrtLower) {
|
|
305
|
+
l0 = amount0 === undefined ? null : getLiquidityForAmount0(sqrtLower, sqrtUpper, amount0)
|
|
306
|
+
l1 = amount1 === undefined ? null : amount0 === undefined ? 0n : null
|
|
307
|
+
} else if (tick < tickUpper && sqrtPrice < sqrtUpper) {
|
|
308
|
+
// 价格正好在上限(向下穿过 tick 后 tick = tickUpper − 1、sqrtPrice = 上限)时只需要 token1:按上方处理
|
|
309
|
+
l0 = amount0 === undefined ? null : getLiquidityForAmount0(sqrtPrice, sqrtUpper, amount0)
|
|
310
|
+
l1 = amount1 === undefined ? null : getLiquidityForAmount1(sqrtLower, sqrtPrice, amount1)
|
|
311
|
+
} else {
|
|
312
|
+
l1 = amount1 === undefined ? null : getLiquidityForAmount1(sqrtLower, sqrtUpper, amount1)
|
|
313
|
+
l0 = amount0 === undefined ? null : amount1 === undefined ? 0n : null
|
|
314
|
+
}
|
|
315
|
+
if (l0 === null) return l1 ?? 0n
|
|
316
|
+
if (l1 === null) return l0
|
|
317
|
+
return l0 < l1 ? l0 : l1
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 整数平方根(向下取整,牛顿迭代从不小于平方根的值开始) */
|
|
321
|
+
export function sqrtBigInt(value: bigint): bigint {
|
|
322
|
+
if (value < 0n) {
|
|
323
|
+
throw new RangeError('square root of a negative number')
|
|
324
|
+
}
|
|
325
|
+
if (value < 2n) {
|
|
326
|
+
return value
|
|
327
|
+
}
|
|
328
|
+
let x = 1n << BigInt((value.toString(2).length + 1) >> 1)
|
|
329
|
+
for (;;) {
|
|
330
|
+
const y = (x + value / x) >> 1n
|
|
331
|
+
if (y >= x) {
|
|
332
|
+
return x
|
|
333
|
+
}
|
|
334
|
+
x = y
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** 十进制数('1.5'、'2e-7'、1.5)→ 分子 / 分母,精确,不经过浮点 */
|
|
339
|
+
export function parseDecimal(value: string | number): { num: bigint; den: bigint } {
|
|
340
|
+
const text = typeof value === 'number' ? (Number.isFinite(value) ? String(value) : '') : value.trim()
|
|
341
|
+
const m = /^\+?(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(text)
|
|
342
|
+
if (!m || (!m[1] && !m[2])) {
|
|
343
|
+
throw new Error(`Invalid number: ${String(value)}`)
|
|
344
|
+
}
|
|
345
|
+
const frac = m[2] ?? ''
|
|
346
|
+
const exp = Number(m[3] ?? 0) - frac.length
|
|
347
|
+
if (Math.abs(exp) > 400) {
|
|
348
|
+
throw new RangeError(`Number out of range: ${String(value)}`)
|
|
349
|
+
}
|
|
350
|
+
const digits = BigInt(`${m[1] ?? ''}${frac}` || '0')
|
|
351
|
+
return exp >= 0 ? { num: digits * 10n ** BigInt(exp), den: 1n } : { num: digits, den: 10n ** BigInt(-exp) }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** 价格(token1 / token0,按精度换算)→ Q64.64 sqrt 价格(向下取整);num / den 是价格的分子分母 */
|
|
355
|
+
export function ratioToSqrtPrice(num: bigint, den: bigint, decimals0: number, decimals1: number): bigint {
|
|
356
|
+
if (num <= 0n || den <= 0n) {
|
|
357
|
+
throw new RangeError('price must be greater than 0')
|
|
358
|
+
}
|
|
359
|
+
const scale = decimals1 - decimals0
|
|
360
|
+
return sqrtBigInt((num * (scale > 0 ? 10n ** BigInt(scale) : 1n) * 2n ** 128n) / (den * (scale < 0 ? 10n ** BigInt(-scale) : 1n)))
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** 价格(token1 / token0,按精度换算)→ tick(按程序;满足价格 >= tick 价格的最大 tick) */
|
|
364
|
+
export function priceToTick(program: ClmmProgram, price: string | number, decimals0: number, decimals1: number): number {
|
|
365
|
+
const { num, den } = parseDecimal(price)
|
|
366
|
+
return tickAtSqrtPrice(program, ratioToSqrtPrice(num, den, decimals0, decimals1))
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** 最近的可用 tick(tickSpacing 的整数倍),限制在 [MIN_TICK, MAX_TICK] 内 */
|
|
370
|
+
export function nearestUsableTick(tick: number, tickSpacing: number): number {
|
|
371
|
+
if (!Number.isInteger(tickSpacing) || tickSpacing <= 0) {
|
|
372
|
+
throw new RangeError(`invalid tickSpacing: ${tickSpacing}`)
|
|
373
|
+
}
|
|
374
|
+
const rounded = Math.round(tick / tickSpacing) * tickSpacing
|
|
375
|
+
if (rounded < MIN_TICK) {
|
|
376
|
+
return rounded + tickSpacing
|
|
377
|
+
}
|
|
378
|
+
if (rounded > MAX_TICK) {
|
|
379
|
+
return rounded - tickSpacing
|
|
380
|
+
}
|
|
381
|
+
return rounded
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** 价格按 slippage 上下浮动后的 sqrt 价格和对应的 tick,限制在程序允许的范围内(slippage 是比例,精确到百万分之一) */
|
|
385
|
+
export function sqrtPriceBounds(program: ClmmProgram, sqrtPrice: bigint, slippage: number): Array<{ sqrtPrice: bigint; tick: number }> {
|
|
386
|
+
if (!(slippage >= 0 && slippage < 1)) {
|
|
387
|
+
throw new RangeError(`slippage must be in [0, 1): ${slippage}`)
|
|
388
|
+
}
|
|
389
|
+
const [min, max] = sqrtRange(program)
|
|
390
|
+
const ppm = BigInt(Math.round(slippage * 1e6))
|
|
391
|
+
const square = sqrtPrice * sqrtPrice
|
|
392
|
+
return [1_000_000n - ppm, 1_000_000n + ppm].map((factor) => {
|
|
393
|
+
let s = sqrtBigInt((square * factor) / 1_000_000n)
|
|
394
|
+
s = s < min ? min : s >= max ? max - 1n : s
|
|
395
|
+
return { sqrtPrice: s, tick: tickAtSqrtPrice(program, s) }
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** 价格在 slippage 范围内变动时,流动性 L 至少对应多少代币(移除时的最小数量,向下取整) */
|
|
400
|
+
export function getMinAmounts(program: ClmmProgram, sqrtPrice: bigint, tickLower: number, tickUpper: number, liquidity: bigint, slippage: number): [bigint, bigint] {
|
|
401
|
+
const [lower, upper] = sqrtPriceBounds(program, sqrtPrice, slippage) as [{ sqrtPrice: bigint; tick: number }, { sqrtPrice: bigint; tick: number }]
|
|
402
|
+
const a = sqrtPriceAtTick(program, tickLower)
|
|
403
|
+
const b = sqrtPriceAtTick(program, tickUpper)
|
|
404
|
+
// token0 随价格上涨而减少,取上限价格时的;token1 取下限价格时的
|
|
405
|
+
return [
|
|
406
|
+
getAmountsForLiquidity(upper.tick, upper.sqrtPrice, a, b, liquidity, tickLower, tickUpper)[0],
|
|
407
|
+
getAmountsForLiquidity(lower.tick, lower.sqrtPrice, a, b, liquidity, tickLower, tickUpper)[1],
|
|
408
|
+
]
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** 价格在 slippage 范围内变动时,添加流动性 L 最多需要多少代币(amountMax,向上取整) */
|
|
412
|
+
export function getMaxAmounts(program: ClmmProgram, sqrtPrice: bigint, tickLower: number, tickUpper: number, liquidity: bigint, slippage: number): [bigint, bigint] {
|
|
413
|
+
const [lower, upper] = sqrtPriceBounds(program, sqrtPrice, slippage) as [{ sqrtPrice: bigint; tick: number }, { sqrtPrice: bigint; tick: number }]
|
|
414
|
+
const a = sqrtPriceAtTick(program, tickLower)
|
|
415
|
+
const b = sqrtPriceAtTick(program, tickUpper)
|
|
416
|
+
return [
|
|
417
|
+
getMintAmounts(lower.tick, lower.sqrtPrice, a, b, liquidity, tickLower, tickUpper)[0],
|
|
418
|
+
getMintAmounts(upper.tick, upper.sqrtPrice, a, b, liquidity, tickLower, tickUpper)[1],
|
|
419
|
+
]
|
|
420
|
+
}
|
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,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 添加 / 移除流动性(子路径 `@w3lib/solana-batch-call/liquidity`):Raydium CLMM、Orca Whirlpool(仓位 NFT),
|
|
3
|
+
* Raydium AMM v4、CPMM(LP 代币)。不放在主入口,不用时打包不会带上这部分代码。不依赖各 DEX 的 SDK。
|
|
4
|
+
*
|
|
5
|
+
* - build*:只读取、不签名,返回预计的数量和按顺序执行的指令,可以当作报价,也可以自己组交易发送
|
|
6
|
+
* - addLiquidity / removeLiquidity / collectFees:构建后签名发送(同 /write 的 batchCall),SOL 自动包装 / 解包
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { getLpPositions } from '@w3lib/solana-batch-call/lp'
|
|
10
|
+
* import { addLiquidity, removeLiquidity } from '@w3lib/solana-batch-call/liquidity'
|
|
11
|
+
*
|
|
12
|
+
* await addLiquidity(client, secretKey, { protocol: 'raydium-clmm', pool, priceLower: 120, priceUpper: 180, amount0Formatted: '1' })
|
|
13
|
+
* const [position] = await getLpPositions(owner, { provider: rpc })
|
|
14
|
+
* await removeLiquidity(client, secretKey, { position, percent: 50 })
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export { addLiquidity, buildAddLiquidity, buildCollectFees, buildRemoveLiquidity, collectFees, removeLiquidity } from '../liquidity/index.js'
|
|
18
|
+
export type {
|
|
19
|
+
SolanaAddLiquidityParams,
|
|
20
|
+
SolanaCollectFeesParams,
|
|
21
|
+
SolanaIncreaseLiquidityParams,
|
|
22
|
+
SolanaLiquidityOptions,
|
|
23
|
+
SolanaLiquidityPlan,
|
|
24
|
+
SolanaLiquidityResult,
|
|
25
|
+
SolanaLiquiditySendOptions,
|
|
26
|
+
SolanaLiquidityStep,
|
|
27
|
+
SolanaLiquidityStepResult,
|
|
28
|
+
SolanaNewPositionParams,
|
|
29
|
+
SolanaPositionRef,
|
|
30
|
+
SolanaRemoveLiquidityParams,
|
|
31
|
+
} from '../liquidity/types.js'
|
|
32
|
+
export {
|
|
33
|
+
getLiquidityForAmounts,
|
|
34
|
+
getMaxAmounts,
|
|
35
|
+
getMinAmounts,
|
|
36
|
+
getMintAmounts,
|
|
37
|
+
nearestUsableTick,
|
|
38
|
+
priceToTick,
|
|
39
|
+
sqrtPriceAtTick,
|
|
40
|
+
tickAtSqrtPrice,
|
|
41
|
+
type ClmmProgram,
|
|
42
|
+
} from '../lpMath.js'
|
|
43
|
+
export type { SolanaLpPosition, SolanaLpProtocol, SolanaLpToken } from '../lp.js'
|
|
44
|
+
export type { Instruction } from '../write/transaction.js'
|
|
45
|
+
export type { SignerInput, WriteResult, WriteStatus } from '../subpaths/write.js'
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
getLiquidityForAmounts,
|
|
38
|
+
getMaxAmounts,
|
|
39
|
+
getMinAmounts,
|
|
40
|
+
getMintAmounts,
|
|
41
|
+
nearestUsableTick,
|
|
42
|
+
orcaSqrtPriceAtTick,
|
|
43
|
+
priceToTick,
|
|
44
|
+
raydiumSqrtPriceAtTick,
|
|
45
|
+
sqrtPriceAtTick,
|
|
46
|
+
sqrtPriceToPrice,
|
|
47
|
+
tickAtSqrtPrice,
|
|
48
|
+
tickToPrice,
|
|
49
|
+
type ClmmProgram,
|
|
50
|
+
type TickFeeData,
|
|
51
|
+
} from '../lpMath.js'
|
|
52
|
+
|
|
53
|
+
export interface GetLpPositionsOptions extends LpPositionsOptions, ShortcutOptions {}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 查一个地址的 LP 仓位。
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* await getLpPositions(owner, { provider: 'https://my-rpc…' })
|
|
60
|
+
* // [{ type: 'clmm', protocol: 'raydium-clmm', pool, positionMint, token0: { symbol: 'SOL', … }, token1: { symbol: 'USDC', … },
|
|
61
|
+
* // amount0Formatted: '140.26', amount1Formatted: '6852.2', fees0Formatted: '0.546', fees1Formatted: '59.5', inRange: true, … },
|
|
62
|
+
* // { type: 'amm', protocol: 'raydium-amm', pool, lpMint, balance, share: 0.18, amount0Formatted, amount1Formatted, … }]
|
|
63
|
+
* await getLpPositions(owner, { mints: [nftMint, lpMint], pools: [ammPool] }) // 免费公共节点:指定代币和池子
|
|
64
|
+
* await getLpPositions(owner, { provider, prices: true }) // 带美元价值(DefiLlama),按价值排序
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function getLpPositions(owner: string, options: GetLpPositionsOptions = {}): Promise<SolanaLpPosition[]> {
|
|
68
|
+
// fetch 同时给节点请求(客户端配置)和查价用,与其他快捷函数一致
|
|
69
|
+
const { client, own } = resolve(options, ['protocols', 'mints', 'pools', 'includeClosed', 'prices', 'onPriceError', 'signal'])
|
|
70
|
+
return lpPositions(client, owner, { ...own, ...(options.fetch ? { fetch: options.fetch } : {}) })
|
|
71
|
+
}
|
|
@@ -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
|
+
|