@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.
- package/CHANGELOG.md +31 -0
- package/README.md +231 -3
- package/dist/approvals.d.ts +129 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +221 -0
- package/dist/approvals.js.map +1 -0
- package/dist/fees.d.ts +89 -0
- package/dist/fees.d.ts.map +1 -0
- package/dist/fees.js +166 -0
- package/dist/fees.js.map +1 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +2 -2
- package/dist/layout.js.map +1 -1
- package/dist/lp.d.ts +8 -15
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +7 -31
- package/dist/lp.js.map +1 -1
- package/dist/names.d.ts +68 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +257 -0
- package/dist/names.js.map +1 -0
- package/dist/pools.d.ts +9 -0
- package/dist/pools.d.ts.map +1 -0
- package/dist/pools.js +45 -0
- package/dist/pools.js.map +1 -0
- package/dist/portfolio.d.ts +64 -0
- package/dist/portfolio.d.ts.map +1 -0
- package/dist/portfolio.js +57 -0
- package/dist/portfolio.js.map +1 -0
- package/dist/price.d.ts +90 -0
- package/dist/price.d.ts.map +1 -0
- package/dist/price.js +287 -0
- package/dist/price.js.map +1 -0
- package/dist/risk.d.ts +84 -0
- package/dist/risk.d.ts.map +1 -0
- package/dist/risk.js +211 -0
- package/dist/risk.js.map +1 -0
- package/dist/rpc.d.ts +2 -0
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +33 -9
- package/dist/rpc.js.map +1 -1
- package/dist/subpaths/approvals.d.ts +29 -0
- package/dist/subpaths/approvals.d.ts.map +1 -0
- package/dist/subpaths/approvals.js +30 -0
- package/dist/subpaths/approvals.js.map +1 -0
- package/dist/subpaths/fees.d.ts +23 -0
- package/dist/subpaths/fees.d.ts.map +1 -0
- package/dist/subpaths/fees.js +25 -0
- package/dist/subpaths/fees.js.map +1 -0
- package/dist/subpaths/names.d.ts +37 -0
- package/dist/subpaths/names.d.ts.map +1 -0
- package/dist/subpaths/names.js +27 -0
- package/dist/subpaths/names.js.map +1 -0
- package/dist/subpaths/price.d.ts +40 -0
- package/dist/subpaths/price.d.ts.map +1 -0
- package/dist/subpaths/price.js +43 -0
- package/dist/subpaths/price.js.map +1 -0
- package/dist/subpaths/risk.d.ts +28 -0
- package/dist/subpaths/risk.d.ts.map +1 -0
- package/dist/subpaths/risk.js +30 -0
- package/dist/subpaths/risk.js.map +1 -0
- package/dist/subpaths/swap.d.ts +22 -0
- package/dist/subpaths/swap.d.ts.map +1 -0
- package/dist/subpaths/swap.js +21 -0
- package/dist/subpaths/swap.js.map +1 -0
- package/dist/subpaths/write.d.ts +1 -1
- package/dist/subpaths/write.d.ts.map +1 -1
- package/dist/subpaths/write.js +1 -1
- package/dist/subpaths/write.js.map +1 -1
- package/dist/swap/amm.d.ts +20 -0
- package/dist/swap/amm.d.ts.map +1 -0
- package/dist/swap/amm.js +77 -0
- package/dist/swap/amm.js.map +1 -0
- package/dist/swap/clmmMath.d.ts +24 -0
- package/dist/swap/clmmMath.d.ts.map +1 -0
- package/dist/swap/clmmMath.js +82 -0
- package/dist/swap/clmmMath.js.map +1 -0
- package/dist/swap/common.d.ts +68 -0
- package/dist/swap/common.d.ts.map +1 -0
- package/dist/swap/common.js +65 -0
- package/dist/swap/common.js.map +1 -0
- package/dist/swap/cpmm.d.ts +28 -0
- package/dist/swap/cpmm.d.ts.map +1 -0
- package/dist/swap/cpmm.js +113 -0
- package/dist/swap/cpmm.js.map +1 -0
- package/dist/swap/index.d.ts +145 -0
- package/dist/swap/index.d.ts.map +1 -0
- package/dist/swap/index.js +349 -0
- package/dist/swap/index.js.map +1 -0
- package/dist/swap/orca.d.ts +50 -0
- package/dist/swap/orca.d.ts.map +1 -0
- package/dist/swap/orca.js +382 -0
- package/dist/swap/orca.js.map +1 -0
- package/dist/swap/raydiumClmm.d.ts +62 -0
- package/dist/swap/raydiumClmm.d.ts.map +1 -0
- package/dist/swap/raydiumClmm.js +447 -0
- package/dist/swap/raydiumClmm.js.map +1 -0
- package/dist/tradeCheck.d.ts +45 -0
- package/dist/tradeCheck.d.ts.map +1 -0
- package/dist/tradeCheck.js +131 -0
- package/dist/tradeCheck.js.map +1 -0
- package/dist/write/instructions.d.ts +7 -0
- package/dist/write/instructions.d.ts.map +1 -1
- package/dist/write/instructions.js +24 -1
- package/dist/write/instructions.js.map +1 -1
- package/dist/write/transaction.d.ts +23 -0
- package/dist/write/transaction.d.ts.map +1 -1
- package/dist/write/transaction.js +50 -0
- package/dist/write/transaction.js.map +1 -1
- package/package.json +46 -14
- package/src/approvals.ts +358 -0
- package/src/fees.ts +259 -0
- package/src/layout.ts +2 -2
- package/src/lp.ts +8 -44
- package/src/names.ts +329 -0
- package/src/pools.ts +43 -0
- package/src/portfolio.ts +124 -0
- package/src/price.ts +366 -0
- package/src/risk.ts +243 -0
- package/src/rpc.ts +36 -9
- package/src/subpaths/approvals.ts +47 -0
- package/src/subpaths/fees.ts +41 -0
- package/src/subpaths/names.ts +45 -0
- package/src/subpaths/price.ts +62 -0
- package/src/subpaths/risk.ts +36 -0
- package/src/subpaths/swap.ts +41 -0
- package/src/subpaths/write.ts +2 -0
- package/src/swap/amm.ts +88 -0
- package/src/swap/clmmMath.ts +96 -0
- package/src/swap/common.ts +113 -0
- package/src/swap/cpmm.ts +132 -0
- package/src/swap/index.ts +467 -0
- package/src/swap/orca.ts +397 -0
- package/src/swap/raydiumClmm.ts +464 -0
- package/src/tradeCheck.ts +166 -0
- package/src/write/instructions.ts +26 -1
- package/src/write/transaction.ts +62 -0
package/src/swap/orca.ts
ADDED
|
@@ -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
|
+
}
|