@w3lib/solana-batch-call 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +231 -3
  3. package/dist/approvals.d.ts +129 -0
  4. package/dist/approvals.d.ts.map +1 -0
  5. package/dist/approvals.js +221 -0
  6. package/dist/approvals.js.map +1 -0
  7. package/dist/fees.d.ts +89 -0
  8. package/dist/fees.d.ts.map +1 -0
  9. package/dist/fees.js +166 -0
  10. package/dist/fees.js.map +1 -0
  11. package/dist/layout.d.ts +2 -0
  12. package/dist/layout.d.ts.map +1 -1
  13. package/dist/layout.js +2 -2
  14. package/dist/layout.js.map +1 -1
  15. package/dist/lp.d.ts +8 -15
  16. package/dist/lp.d.ts.map +1 -1
  17. package/dist/lp.js +7 -31
  18. package/dist/lp.js.map +1 -1
  19. package/dist/names.d.ts +68 -0
  20. package/dist/names.d.ts.map +1 -0
  21. package/dist/names.js +257 -0
  22. package/dist/names.js.map +1 -0
  23. package/dist/pools.d.ts +9 -0
  24. package/dist/pools.d.ts.map +1 -0
  25. package/dist/pools.js +45 -0
  26. package/dist/pools.js.map +1 -0
  27. package/dist/portfolio.d.ts +64 -0
  28. package/dist/portfolio.d.ts.map +1 -0
  29. package/dist/portfolio.js +57 -0
  30. package/dist/portfolio.js.map +1 -0
  31. package/dist/price.d.ts +90 -0
  32. package/dist/price.d.ts.map +1 -0
  33. package/dist/price.js +287 -0
  34. package/dist/price.js.map +1 -0
  35. package/dist/risk.d.ts +84 -0
  36. package/dist/risk.d.ts.map +1 -0
  37. package/dist/risk.js +211 -0
  38. package/dist/risk.js.map +1 -0
  39. package/dist/rpc.d.ts +2 -0
  40. package/dist/rpc.d.ts.map +1 -1
  41. package/dist/rpc.js +33 -9
  42. package/dist/rpc.js.map +1 -1
  43. package/dist/subpaths/approvals.d.ts +29 -0
  44. package/dist/subpaths/approvals.d.ts.map +1 -0
  45. package/dist/subpaths/approvals.js +30 -0
  46. package/dist/subpaths/approvals.js.map +1 -0
  47. package/dist/subpaths/fees.d.ts +23 -0
  48. package/dist/subpaths/fees.d.ts.map +1 -0
  49. package/dist/subpaths/fees.js +25 -0
  50. package/dist/subpaths/fees.js.map +1 -0
  51. package/dist/subpaths/names.d.ts +37 -0
  52. package/dist/subpaths/names.d.ts.map +1 -0
  53. package/dist/subpaths/names.js +27 -0
  54. package/dist/subpaths/names.js.map +1 -0
  55. package/dist/subpaths/price.d.ts +40 -0
  56. package/dist/subpaths/price.d.ts.map +1 -0
  57. package/dist/subpaths/price.js +43 -0
  58. package/dist/subpaths/price.js.map +1 -0
  59. package/dist/subpaths/risk.d.ts +28 -0
  60. package/dist/subpaths/risk.d.ts.map +1 -0
  61. package/dist/subpaths/risk.js +30 -0
  62. package/dist/subpaths/risk.js.map +1 -0
  63. package/dist/subpaths/swap.d.ts +22 -0
  64. package/dist/subpaths/swap.d.ts.map +1 -0
  65. package/dist/subpaths/swap.js +21 -0
  66. package/dist/subpaths/swap.js.map +1 -0
  67. package/dist/subpaths/write.d.ts +1 -1
  68. package/dist/subpaths/write.d.ts.map +1 -1
  69. package/dist/subpaths/write.js +1 -1
  70. package/dist/subpaths/write.js.map +1 -1
  71. package/dist/swap/amm.d.ts +20 -0
  72. package/dist/swap/amm.d.ts.map +1 -0
  73. package/dist/swap/amm.js +77 -0
  74. package/dist/swap/amm.js.map +1 -0
  75. package/dist/swap/clmmMath.d.ts +24 -0
  76. package/dist/swap/clmmMath.d.ts.map +1 -0
  77. package/dist/swap/clmmMath.js +82 -0
  78. package/dist/swap/clmmMath.js.map +1 -0
  79. package/dist/swap/common.d.ts +68 -0
  80. package/dist/swap/common.d.ts.map +1 -0
  81. package/dist/swap/common.js +65 -0
  82. package/dist/swap/common.js.map +1 -0
  83. package/dist/swap/cpmm.d.ts +28 -0
  84. package/dist/swap/cpmm.d.ts.map +1 -0
  85. package/dist/swap/cpmm.js +113 -0
  86. package/dist/swap/cpmm.js.map +1 -0
  87. package/dist/swap/index.d.ts +145 -0
  88. package/dist/swap/index.d.ts.map +1 -0
  89. package/dist/swap/index.js +349 -0
  90. package/dist/swap/index.js.map +1 -0
  91. package/dist/swap/orca.d.ts +50 -0
  92. package/dist/swap/orca.d.ts.map +1 -0
  93. package/dist/swap/orca.js +382 -0
  94. package/dist/swap/orca.js.map +1 -0
  95. package/dist/swap/raydiumClmm.d.ts +62 -0
  96. package/dist/swap/raydiumClmm.d.ts.map +1 -0
  97. package/dist/swap/raydiumClmm.js +447 -0
  98. package/dist/swap/raydiumClmm.js.map +1 -0
  99. package/dist/tradeCheck.d.ts +45 -0
  100. package/dist/tradeCheck.d.ts.map +1 -0
  101. package/dist/tradeCheck.js +131 -0
  102. package/dist/tradeCheck.js.map +1 -0
  103. package/dist/write/instructions.d.ts +7 -0
  104. package/dist/write/instructions.d.ts.map +1 -1
  105. package/dist/write/instructions.js +24 -1
  106. package/dist/write/instructions.js.map +1 -1
  107. package/dist/write/transaction.d.ts +23 -0
  108. package/dist/write/transaction.d.ts.map +1 -1
  109. package/dist/write/transaction.js +50 -0
  110. package/dist/write/transaction.js.map +1 -1
  111. package/package.json +46 -14
  112. package/src/approvals.ts +358 -0
  113. package/src/fees.ts +259 -0
  114. package/src/layout.ts +2 -2
  115. package/src/lp.ts +8 -44
  116. package/src/names.ts +329 -0
  117. package/src/pools.ts +43 -0
  118. package/src/portfolio.ts +124 -0
  119. package/src/price.ts +366 -0
  120. package/src/risk.ts +243 -0
  121. package/src/rpc.ts +36 -9
  122. package/src/subpaths/approvals.ts +47 -0
  123. package/src/subpaths/fees.ts +41 -0
  124. package/src/subpaths/names.ts +45 -0
  125. package/src/subpaths/price.ts +62 -0
  126. package/src/subpaths/risk.ts +36 -0
  127. package/src/subpaths/swap.ts +41 -0
  128. package/src/subpaths/write.ts +2 -0
  129. package/src/swap/amm.ts +88 -0
  130. package/src/swap/clmmMath.ts +96 -0
  131. package/src/swap/common.ts +113 -0
  132. package/src/swap/cpmm.ts +132 -0
  133. package/src/swap/index.ts +467 -0
  134. package/src/swap/orca.ts +397 -0
  135. package/src/swap/raydiumClmm.ts +464 -0
  136. package/src/tradeCheck.ts +166 -0
  137. package/src/write/instructions.ts +26 -1
  138. package/src/write/transaction.ts +62 -0
@@ -0,0 +1,397 @@
1
+ /**
2
+ * Orca Whirlpool 的兑换:swap_v2(按输入数量)。报价照搬程序(manager/swap_manager.rs、fee_rate_manager.rs、util/sparse_swap.rs):
3
+ * 当前价格起最多 3 个 tick 数组(不存在的按全空处理),逐个区间计算;包括自适应费率(Oracle 账户里的波动累计)和两边的 Token-2022 转账手续费。
4
+ * 兑换超出这 3 个 tick 数组时程序会失败,报价同样不可用
5
+ */
6
+ import { sha256 } from '@noble/hashes/sha2.js'
7
+
8
+ import { decodeAddress, findProgramAddress } from '../address.js'
9
+ import { SYSTEM_PROGRAM_ID } from '../constants.js'
10
+ import type { AccountInfo } from '../layout.js'
11
+ import { MEMO_PROGRAM_ID, Writer, anchorIx, r, w } from '../liquidity/common.js'
12
+ import { ORCA_WHIRLPOOL_PROGRAM_ID, orcaTickArray, utf8 } from '../lp.js'
13
+ import { MAX_TICK, MIN_TICK, ORCA_MAX_SQRT_PRICE, ORCA_MIN_SQRT_PRICE, orcaSqrtPriceAtTick, tickAtSqrtPrice } from '../lpMath.js'
14
+ import type { Instruction } from '../write/transaction.js'
15
+ import { SwapMathError, computeSwapStep } from './clmmMath.js'
16
+ import { Bytes, UnsupportedPool, need, swapMint, transferFee, type AccountGetter, type HopQuote, type SwapEnv, type SwapMint, type SwapPool } from './common.js'
17
+
18
+ const PROGRAM = ORCA_WHIRLPOOL_PROGRAM_ID
19
+ const POOL_SIZE = 653
20
+ const TICK_ARRAY_SIZE = 88
21
+ const FIXED_TICK_SIZE = 113
22
+ const FEE_RATE_HARD_LIMIT = 100_000n
23
+ const VOLATILITY_SCALE = 10_000n
24
+ const REDUCTION_FACTOR_DENOMINATOR = 10_000n
25
+ const CONTROL_FACTOR_DENOMINATOR = 100_000n
26
+ const MAX_REFERENCE_AGE = 3_600n
27
+
28
+ export interface WhirlpoolState {
29
+ address: string
30
+ tickSpacing: number
31
+ feeRate: bigint
32
+ liquidity: bigint
33
+ sqrtPrice: bigint
34
+ tick: number
35
+ mint0: string
36
+ vault0: string
37
+ mint1: string
38
+ vault1: string
39
+ }
40
+
41
+ /** Whirlpool:tick_spacing 41、fee_rate 45、liquidity 49、sqrt_price 65、tick 81、mint_a 101、vault_a 133、mint_b 181、vault_b 213 */
42
+ export function parseWhirlpool(account: AccountInfo): WhirlpoolState | null {
43
+ if (account.owner !== PROGRAM || account.data.length !== POOL_SIZE) return null
44
+ const b = new Bytes(account.data)
45
+ return {
46
+ address: account.address,
47
+ tickSpacing: b.u16(41),
48
+ feeRate: BigInt(b.u16(45)),
49
+ liquidity: b.u128(49),
50
+ sqrtPrice: b.u128(65),
51
+ tick: b.i32(81),
52
+ mint0: b.key(101),
53
+ vault0: b.key(133),
54
+ mint1: b.key(181),
55
+ vault1: b.key(213),
56
+ }
57
+ }
58
+
59
+ const disc = (name: string) => sha256(utf8(`account:${name}`)).subarray(0, 8)
60
+ let discriminators: { fixed: Uint8Array; dynamic: Uint8Array; oracle: Uint8Array } | undefined
61
+ const starts = (data: Uint8Array, prefix: Uint8Array) => data.length >= 8 && prefix.every((x, i) => data[i] === x)
62
+ const i128 = (v: bigint) => (v >= 1n << 127n ? v - (1n << 128n) : v)
63
+ const floorDiv = (a: number, b: number) => Math.floor(a / b)
64
+
65
+ export const oracleAddress = (pool: string) => findProgramAddress([utf8('oracle'), decodeAddress(pool)], PROGRAM)
66
+
67
+ /** 与程序 get_start_tick_indexes 一致:当前所在的数组和方向上接下来的两个(向右时 tick 靠近数组末尾会整体右移一个) */
68
+ export function whirlpoolTickArrayStarts(state: WhirlpoolState, aToB: boolean): number[] {
69
+ const size = TICK_ARRAY_SIZE * state.tickSpacing
70
+ const base = floorDiv(state.tick, size) * size
71
+ const offsets = aToB ? [0, -1, -2] : state.tick + state.tickSpacing >= base + size ? [1, 2, 3] : [0, 1, 2]
72
+ const minStart = MIN_TICK - ((MIN_TICK % size) + size)
73
+ return offsets
74
+ .map((o) => base + o * size)
75
+ .filter((s) => (s < MIN_TICK || s > MAX_TICK ? s <= MIN_TICK && s === minStart : s % size === 0))
76
+ }
77
+
78
+ interface Tick {
79
+ initialized: boolean
80
+ liquidityNet: bigint
81
+ }
82
+
83
+ interface TickArray {
84
+ start: number
85
+ /** 按偏移(0~87)的 tick;全空的数组为 null */
86
+ ticks: Array<Tick | null>
87
+ }
88
+
89
+ /** 固定大小(TickArray)或可变大小(DynamicTickArray)的 tick 数组;账户不存在时按全空处理(与程序的 ZeroedTickArray 一致) */
90
+ function readTickArray(pool: string, start: number, account: AccountInfo | null): TickArray {
91
+ const empty: TickArray = { start, ticks: new Array<Tick | null>(TICK_ARRAY_SIZE).fill(null) }
92
+ if (!account || (account.owner === SYSTEM_PROGRAM_ID && account.data.length === 0)) return empty
93
+ if (account.owner !== PROGRAM) throw new UnsupportedPool(`Tick array owned by another program: ${account.address}`)
94
+ discriminators ??= { fixed: disc('TickArray'), dynamic: disc('DynamicTickArray'), oracle: disc('Oracle') }
95
+ const b = new Bytes(account.data)
96
+ const data = account.data
97
+ if (starts(data, discriminators.fixed) && data.length >= 12 + TICK_ARRAY_SIZE * FIXED_TICK_SIZE + 32) {
98
+ if (b.key(12 + TICK_ARRAY_SIZE * FIXED_TICK_SIZE) !== pool) throw new UnsupportedPool('Tick array of another whirlpool')
99
+ return {
100
+ start: b.i32(8),
101
+ ticks: Array.from({ length: TICK_ARRAY_SIZE }, (_, i) => {
102
+ const o = 12 + i * FIXED_TICK_SIZE
103
+ return { initialized: b.u8(o) === 1, liquidityNet: i128(b.u128(o + 1)) }
104
+ }),
105
+ }
106
+ }
107
+ if (starts(data, discriminators.dynamic) && data.length >= 60) {
108
+ if (b.key(12) !== pool) throw new UnsupportedPool('Tick array of another whirlpool')
109
+ // 可变大小:未初始化的 tick 1 字节(0),初始化的 1 字节(1)+ 112 字节数据
110
+ const ticks: Array<Tick | null> = []
111
+ let o = 60
112
+ for (let i = 0; i < TICK_ARRAY_SIZE; i++) {
113
+ if (o >= data.length) throw new UnsupportedPool('Truncated dynamic tick array')
114
+ if (b.u8(o) === 1) {
115
+ if (o + 113 > data.length) throw new UnsupportedPool('Truncated dynamic tick array')
116
+ ticks.push({ initialized: true, liquidityNet: i128(b.u128(o + 1)) })
117
+ o += 113
118
+ } else {
119
+ ticks.push({ initialized: false, liquidityNet: 0n })
120
+ o += 1
121
+ }
122
+ }
123
+ return { start: b.i32(8), ticks }
124
+ }
125
+ throw new UnsupportedPool(`Unexpected tick array account: ${account.address}`)
126
+ }
127
+
128
+ interface AdaptiveFee {
129
+ tradeEnableTimestamp: bigint
130
+ filterPeriod: bigint
131
+ decayPeriod: bigint
132
+ reductionFactor: bigint
133
+ controlFactor: bigint
134
+ maxVolatility: bigint
135
+ tickGroupSize: number
136
+ lastReferenceUpdate: bigint
137
+ lastMajorSwap: bigint
138
+ volatilityReference: bigint
139
+ indexReference: number
140
+ volatility: bigint
141
+ }
142
+
143
+ /** Oracle:whirlpool 8、trade_enable_timestamp 40、常量 48(filter 48、decay 50、reduction 52、control 54、max_va 58、tick_group_size 62)、变量 82(last_ref 82、last_major 90、vr 98、ref 102、va 106) */
144
+ function readOracle(pool: string, account: AccountInfo | null): AdaptiveFee | null {
145
+ if (!account || (account.owner === SYSTEM_PROGRAM_ID && account.data.length === 0)) return null
146
+ discriminators ??= { fixed: disc('TickArray'), dynamic: disc('DynamicTickArray'), oracle: disc('Oracle') }
147
+ if (account.owner !== PROGRAM || !starts(account.data, discriminators.oracle) || account.data.length < 126) throw new UnsupportedPool('Unexpected oracle account')
148
+ const b = new Bytes(account.data)
149
+ if (b.key(8) !== pool) throw new UnsupportedPool('Oracle of another whirlpool')
150
+ return {
151
+ tradeEnableTimestamp: b.u64(40),
152
+ filterPeriod: BigInt(b.u16(48)),
153
+ decayPeriod: BigInt(b.u16(50)),
154
+ reductionFactor: BigInt(b.u16(52)),
155
+ controlFactor: BigInt(b.u32(54)),
156
+ maxVolatility: BigInt(b.u32(58)),
157
+ tickGroupSize: b.u16(62),
158
+ lastReferenceUpdate: b.u64(82),
159
+ lastMajorSwap: b.u64(90),
160
+ volatilityReference: BigInt(b.u32(98)),
161
+ indexReference: b.i32(102),
162
+ volatility: BigInt(b.u32(106)),
163
+ }
164
+ }
165
+
166
+ /** 自适应费率(FeeRateManager::Adaptive);静态费率的池子 fee 为 null */
167
+ class FeeManager {
168
+ index = 0
169
+ lower: { index: number; sqrt: bigint } | null = null
170
+ upper: { index: number; sqrt: bigint } | null = null
171
+
172
+ constructor(
173
+ readonly aToB: boolean,
174
+ readonly staticFee: bigint,
175
+ readonly fee: AdaptiveFee | null,
176
+ tick: number,
177
+ timestamp: bigint,
178
+ ) {
179
+ if (!fee) return
180
+ const size = fee.tickGroupSize
181
+ this.index = floorDiv(tick, size)
182
+ // update_reference
183
+ const max = fee.lastReferenceUpdate > fee.lastMajorSwap ? fee.lastReferenceUpdate : fee.lastMajorSwap
184
+ if (timestamp < max) throw new SwapMathError('invalid timestamp')
185
+ if (timestamp - fee.lastReferenceUpdate > MAX_REFERENCE_AGE) {
186
+ fee.indexReference = this.index
187
+ fee.volatilityReference = 0n
188
+ fee.lastReferenceUpdate = timestamp
189
+ } else {
190
+ const elapsed = timestamp - max
191
+ if (elapsed >= fee.filterPeriod) {
192
+ fee.indexReference = this.index
193
+ fee.volatilityReference = elapsed < fee.decayPeriod ? (fee.volatility * fee.reductionFactor) / REDUCTION_FACTOR_DENOMINATOR : 0n
194
+ fee.lastReferenceUpdate = timestamp
195
+ }
196
+ }
197
+ const delta = Number((fee.maxVolatility - fee.volatilityReference + VOLATILITY_SCALE - 1n) / VOLATILITY_SCALE)
198
+ const lowerIndex = fee.indexReference - delta
199
+ const upperIndex = fee.indexReference + delta
200
+ const lowerTick = lowerIndex * size
201
+ const upperTick = upperIndex * size + size
202
+ this.lower = lowerTick > MIN_TICK ? { index: lowerIndex, sqrt: orcaSqrtPriceAtTick(lowerTick) } : null
203
+ this.upper = upperTick < MAX_TICK ? { index: upperIndex, sqrt: orcaSqrtPriceAtTick(upperTick) } : null
204
+ }
205
+
206
+ updateVolatility(): void {
207
+ const f = this.fee
208
+ if (!f) return
209
+ const v = f.volatilityReference + BigInt(Math.abs(f.indexReference - this.index)) * VOLATILITY_SCALE
210
+ f.volatility = v < f.maxVolatility ? v : f.maxVolatility
211
+ }
212
+
213
+ totalFee(): bigint {
214
+ const f = this.fee
215
+ if (!f) return this.staticFee
216
+ const crossed = f.volatility * BigInt(f.tickGroupSize)
217
+ const den = CONTROL_FACTOR_DENOMINATOR * VOLATILITY_SCALE * VOLATILITY_SCALE
218
+ let adaptive = (f.controlFactor * crossed * crossed + den - 1n) / den
219
+ if (adaptive > FEE_RATE_HARD_LIMIT) adaptive = FEE_RATE_HARD_LIMIT
220
+ const total = this.staticFee + adaptive
221
+ return total > FEE_RATE_HARD_LIMIT ? FEE_RATE_HARD_LIMIT : total
222
+ }
223
+
224
+ /** 这一步最多走到的价格和是否跳过了 tick 组的逐个计算 */
225
+ bounded(target: bigint, liquidity: bigint): [bigint, boolean] {
226
+ const f = this.fee
227
+ if (!f) return [target, false]
228
+ if (f.controlFactor === 0n || liquidity === 0n) return [target, true]
229
+ if (this.lower && this.index < this.lower.index) return this.aToB ? [target, true] : [target < this.lower.sqrt ? target : this.lower.sqrt, true]
230
+ if (this.upper && this.index > this.upper.index) return this.aToB ? [target > this.upper.sqrt ? target : this.upper.sqrt, true] : [target, true]
231
+ const size = f.tickGroupSize
232
+ const boundary = Math.min(Math.max(this.aToB ? this.index * size : this.index * size + size, MIN_TICK), MAX_TICK)
233
+ const s = orcaSqrtPriceAtTick(boundary)
234
+ return this.aToB ? [target > s ? target : s, false] : [target < s ? target : s, false]
235
+ }
236
+
237
+ advance(): void {
238
+ if (this.fee) this.index += this.aToB ? -1 : 1
239
+ }
240
+
241
+ advanceAfterSkip(sqrtPrice: bigint, nextTickSqrt: bigint, nextTick: number): void {
242
+ const f = this.fee as AdaptiveFee
243
+ const size = f.tickGroupSize
244
+ let tick: number
245
+ let onBoundary: boolean
246
+ if (sqrtPrice === nextTickSqrt) {
247
+ tick = nextTick
248
+ onBoundary = nextTick % size === 0
249
+ } else {
250
+ tick = tickAtSqrtPrice('orca', sqrtPrice)
251
+ onBoundary = tick % size === 0 && sqrtPrice === orcaSqrtPriceAtTick(tick)
252
+ }
253
+ const last = onBoundary && !this.aToB ? Math.trunc(tick / size) - 1 : floorDiv(tick, size)
254
+ if ((this.aToB && last < this.index) || (!this.aToB && last > this.index)) {
255
+ this.index = last
256
+ this.updateVolatility()
257
+ }
258
+ this.index += this.aToB ? -1 : 1
259
+ }
260
+ }
261
+
262
+ /** 本地报价(swap_manager::swap 的按输入数量部分):amount 是扣掉输入转账手续费后的数量,返回池子付出的输出 */
263
+ export function whirlpoolSwap(state: WhirlpoolState, arrays: readonly TickArray[], oracle: AdaptiveFee | null, amount: bigint, aToB: boolean, timestamp: bigint): { amountOut: bigint; crossed: number } {
264
+ if (amount === 0n) throw new SwapMathError('zero amount')
265
+ const spacing = state.tickSpacing
266
+ const limit = aToB ? ORCA_MIN_SQRT_PRICE : ORCA_MAX_SQRT_PRICE
267
+ if (aToB ? limit >= state.sqrtPrice : limit <= state.sqrtPrice) throw new SwapMathError('price is at the limit')
268
+ if (oracle && oracle.tradeEnableTimestamp > timestamp) throw new UnsupportedPool('Trading is not enabled yet')
269
+ const size = TICK_ARRAY_SIZE * spacing
270
+ const fees = new FeeManager(aToB, state.feeRate, oracle ? { ...oracle } : null, state.tick, timestamp)
271
+
272
+ const nextInit = (arrayIndex: number, tickIndex: number): [number, number] => {
273
+ let search = tickIndex
274
+ let i = arrayIndex
275
+ for (;;) {
276
+ const array = arrays[i]
277
+ if (!array) throw new UnsupportedPool('Swap goes beyond the 3 tick arrays the program accepts (amount too large for this pool)')
278
+ // in_search_range:向右时整体左移一个 tickSpacing
279
+ const lo = array.start - (aToB ? 0 : spacing)
280
+ if (search < lo || search >= lo + size) throw new SwapMathError('invalid tick array sequence')
281
+ let offset = floorDiv(search - array.start, spacing)
282
+ if (!aToB) offset += 1
283
+ for (; offset >= 0 && offset < TICK_ARRAY_SIZE; offset += aToB ? -1 : 1) {
284
+ if (array.ticks[offset]?.initialized) return [i, array.start + offset * spacing]
285
+ }
286
+ if (aToB && array.start <= MIN_TICK) return [i, MIN_TICK]
287
+ if (!aToB && array.start + size > MAX_TICK) return [i, MAX_TICK]
288
+ if (i + 1 === arrays.length) return [i, aToB ? array.start : array.start + size - 1]
289
+ search = aToB ? array.start - 1 : array.start + size - 1
290
+ i += 1
291
+ }
292
+ }
293
+
294
+ let remaining = amount
295
+ let calculated = 0n
296
+ let sqrt = state.sqrtPrice
297
+ let tick = state.tick
298
+ let liquidity = state.liquidity
299
+ let arrayIndex = 0
300
+ let crossed = 0
301
+ while (remaining > 0n && limit !== sqrt) {
302
+ const [nextArray, nextTick] = nextInit(arrayIndex, tick)
303
+ const nextTickSqrt = orcaSqrtPriceAtTick(nextTick)
304
+ const target = aToB ? (nextTickSqrt > limit ? nextTickSqrt : limit) : nextTickSqrt < limit ? nextTickSqrt : limit
305
+ for (;;) {
306
+ fees.updateVolatility()
307
+ const totalFee = fees.totalFee()
308
+ const [bounded, skipped] = fees.bounded(target, liquidity)
309
+ const step = computeSwapStep(sqrt, bounded, liquidity, remaining, totalFee, aToB, true, 'orca')
310
+ remaining -= step.amountIn + step.feeAmount
311
+ if (remaining < 0n) throw new SwapMathError('amount remaining overflow')
312
+ calculated += step.amountOut
313
+ if (step.sqrtPrice === nextTickSqrt) {
314
+ const array = arrays[nextArray] as TickArray
315
+ const usable = nextTick >= MIN_TICK && nextTick <= MAX_TICK && nextTick % spacing === 0 && nextTick >= array.start && nextTick < array.start + size
316
+ const t = usable ? array.ticks[floorDiv(nextTick - array.start, spacing)] : null
317
+ if (t?.initialized) {
318
+ liquidity += aToB ? -t.liquidityNet : t.liquidityNet
319
+ crossed++
320
+ if (liquidity < 0n || liquidity >= 1n << 128n) throw new SwapMathError('liquidity overflow')
321
+ }
322
+ const offset = floorDiv(nextTick - array.start, spacing)
323
+ arrayIndex = (aToB && offset === 0) || (!aToB && offset === TICK_ARRAY_SIZE - 1) ? nextArray + 1 : nextArray
324
+ tick = aToB ? nextTick - 1 : nextTick
325
+ } else if (step.sqrtPrice !== sqrt) {
326
+ tick = tickAtSqrtPrice('orca', step.sqrtPrice)
327
+ }
328
+ sqrt = step.sqrtPrice
329
+ if (!skipped) fees.advance()
330
+ else fees.advanceAfterSkip(sqrt, nextTickSqrt, nextTick)
331
+ if (remaining === 0n || sqrt === target) break
332
+ }
333
+ }
334
+ // 不给价格上限时只有走到最低 / 最高价格才会部分成交:当作流动性不足
335
+ if (remaining > 0n) throw new UnsupportedPool('Insufficient liquidity')
336
+ return { amountOut: calculated, crossed }
337
+ }
338
+
339
+ export function whirlpoolPool(state: WhirlpoolState): SwapPool {
340
+ const mints = (get: AccountGetter): [SwapMint, SwapMint] => [
341
+ swapMint(state.mint0, need(get, state.mint0, 'Token mint')),
342
+ swapMint(state.mint1, need(get, state.mint1, 'Token mint')),
343
+ ]
344
+ const oracle = oracleAddress(state.address)
345
+ const arrayAddresses = (aToB: boolean) => whirlpoolTickArrayStarts(state, aToB).map((s) => ({ start: s, address: orcaTickArray(state.address, s, state.tickSpacing).address }))
346
+ return {
347
+ protocol: 'orca-whirlpool',
348
+ address: state.address,
349
+ mint0: state.mint0,
350
+ mint1: state.mint1,
351
+ mints,
352
+ accounts: (aToB) => [state.mint0, state.mint1, oracle, ...arrayAddresses(aToB).map((a) => a.address)],
353
+ quote(get, amountIn, aToB, env: SwapEnv): HopQuote {
354
+ const [m0, m1] = mints(get)
355
+ const list = arrayAddresses(aToB)
356
+ const arrays = list.map((a) => {
357
+ const account = get(a.address)
358
+ if (account === undefined) throw new Error(`Tick array was not loaded: ${a.address}`)
359
+ return readTickArray(state.address, a.start, account)
360
+ })
361
+ const oracleAccount = get(oracle)
362
+ if (oracleAccount === undefined) throw new Error(`Oracle was not loaded: ${oracle}`)
363
+ const [mintIn, mintOut] = aToB ? [m0, m1] : [m1, m0]
364
+ const into = amountIn - transferFee(mintIn, amountIn, env.epoch)
365
+ if (into <= 0n) return { amountIn, amountOut: 0n, accounts: [] }
366
+ const { amountOut: out, crossed } = whirlpoolSwap(state, arrays, readOracle(state.address, oracleAccount), into, aToB, env.timestamp)
367
+ // 固定传 3 个 tick 数组(不足 3 个有效起点时重复最后一个,程序会去重)
368
+ const addresses = list.map((a) => a.address)
369
+ while (addresses.length < 3) addresses.push(addresses[addresses.length - 1] as string)
370
+ return { amountIn, amountOut: out - transferFee(mintOut, out, env.epoch), accounts: addresses, crossed }
371
+ },
372
+ instruction(get, owner, input, output, aToB, quote, minOut): Instruction {
373
+ const [m0, m1] = mints(get)
374
+ const [ownerA, ownerB] = aToB ? [input, output] : [output, input]
375
+ return {
376
+ programId: PROGRAM,
377
+ keys: [
378
+ r(m0.program),
379
+ r(m1.program),
380
+ r(MEMO_PROGRAM_ID),
381
+ r(owner, true),
382
+ w(state.address),
383
+ r(m0.mint),
384
+ r(m1.mint),
385
+ w(ownerA),
386
+ w(state.vault0),
387
+ w(ownerB),
388
+ w(state.vault1),
389
+ ...quote.accounts.map((a) => w(a)),
390
+ w(oracle),
391
+ ],
392
+ // sqrt_price_limit 0(不限价)、amount_specified_is_input、a_to_b、remaining_accounts_info: None
393
+ data: new Writer().bytes(anchorIx('swap_v2')).u64(quote.amountIn).u64(minOut).u128(0n).bool(true).bool(aToB).u8(0).build(),
394
+ }
395
+ },
396
+ }
397
+ }