@cetusprotocol/dlmm-sdk 0.0.1
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/.turbo/turbo-build.log +10423 -0
- package/README.md +646 -0
- package/dist/index.d.mts +1015 -0
- package/dist/index.d.ts +1015 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +13 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +35 -0
- package/src/config/index.ts +2 -0
- package/src/config/mainnet.ts +25 -0
- package/src/config/testnet.ts +30 -0
- package/src/errors/errors.ts +40 -0
- package/src/index.ts +8 -0
- package/src/modules/configModule.ts +184 -0
- package/src/modules/index.ts +1 -0
- package/src/modules/partnerModule.ts +302 -0
- package/src/modules/poolModule.ts +578 -0
- package/src/modules/positionModule.ts +888 -0
- package/src/modules/rewardModule.ts +175 -0
- package/src/modules/swapModule.ts +129 -0
- package/src/sdk.ts +88 -0
- package/src/types/constants.ts +23 -0
- package/src/types/dlmm.ts +445 -0
- package/src/types/index.ts +2 -0
- package/src/utils/binUtils.ts +552 -0
- package/src/utils/feeUtils.ts +92 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/parseData.ts +519 -0
- package/src/utils/strategyUtils.ts +121 -0
- package/src/utils/weightUtils.ts +510 -0
- package/tests/add_liquidity_bidask.test.ts +180 -0
- package/tests/add_liquidity_curve.test.ts +244 -0
- package/tests/add_liquidity_spot.test.ts +262 -0
- package/tests/bin.test.ts +80 -0
- package/tests/config.test.ts +51 -0
- package/tests/partner.test.ts +74 -0
- package/tests/pool.test.ts +174 -0
- package/tests/position.test.ts +76 -0
- package/tests/remove_liquidity.test.ts +137 -0
- package/tests/swap.test.ts +96 -0
- package/tests/tsconfig.json +26 -0
- package/tsconfig.json +5 -0
- package/tsup.config.ts +9 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { d, DETAILS_KEYS, MathUtil } from '@cetusprotocol/common-sdk'
|
|
2
|
+
import { BIN_BOUND, MAX_BIN_PER_POSITION } from '../types/constants'
|
|
3
|
+
import Decimal from 'decimal.js'
|
|
4
|
+
import { BASIS_POINT_MAX } from '../types/constants'
|
|
5
|
+
import BN from 'bn.js'
|
|
6
|
+
import { DlmmErrorCode, handleError } from '../errors/errors'
|
|
7
|
+
import { BinAmount, BinLiquidityInfo } from '../types/dlmm'
|
|
8
|
+
|
|
9
|
+
const MAX_EXPONENTIAL = new BN(0x80000)
|
|
10
|
+
export const SCALE_OFFSET = 64
|
|
11
|
+
export const ONE = new BN(1).shln(SCALE_OFFSET)
|
|
12
|
+
const MAX = new BN(2).pow(new BN(128)).sub(new BN(1))
|
|
13
|
+
|
|
14
|
+
export class BinUtils {
|
|
15
|
+
/**
|
|
16
|
+
* Split bins into multiple smaller positions based on MAX_BIN_PER_POSITION
|
|
17
|
+
* @param bins - The bins to split
|
|
18
|
+
* @param lower_bin_id - The lower bin id
|
|
19
|
+
* @param upper_bin_id - The upper bin id
|
|
20
|
+
* @returns Array of bin info objects for each position
|
|
21
|
+
*/
|
|
22
|
+
static splitBinLiquidityInfo(liquidity_bins: BinLiquidityInfo, lower_bin_id: number, upper_bin_id: number): BinLiquidityInfo[] {
|
|
23
|
+
const position_count = BinUtils.getPositionCount(lower_bin_id, upper_bin_id)
|
|
24
|
+
if (position_count <= 1) {
|
|
25
|
+
return [liquidity_bins]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const positions: BinLiquidityInfo[] = []
|
|
29
|
+
let current_lower = lower_bin_id
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < position_count; i++) {
|
|
32
|
+
const current_upper = Math.min(current_lower + MAX_BIN_PER_POSITION - 2, upper_bin_id)
|
|
33
|
+
const position_bins = liquidity_bins.bins.filter((bin) => bin.bin_id >= current_lower && bin.bin_id <= current_upper)
|
|
34
|
+
|
|
35
|
+
positions.push({
|
|
36
|
+
bins: position_bins,
|
|
37
|
+
amount_a: position_bins.reduce((acc, bin) => d(acc).plus(bin.amount_a), d(0)).toFixed(0),
|
|
38
|
+
amount_b: position_bins.reduce((acc, bin) => d(acc).plus(bin.amount_b), d(0)).toFixed(0),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
current_lower = current_upper + 1
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return positions
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Process bins by rate
|
|
49
|
+
* @param bins - The bins to be processed
|
|
50
|
+
* @param rate - The rate to be applied
|
|
51
|
+
* @returns The processed bins
|
|
52
|
+
*/
|
|
53
|
+
static processBinsByRate(bins: BinAmount[], rate: string): { bins: BinLiquidityInfo; has_invalid_amount: boolean } {
|
|
54
|
+
const used_bins: BinAmount[] = []
|
|
55
|
+
let used_total_amount_a = d(0)
|
|
56
|
+
let used_total_amount_b = d(0)
|
|
57
|
+
|
|
58
|
+
let has_invalid_amount = false
|
|
59
|
+
|
|
60
|
+
bins.forEach((bin) => {
|
|
61
|
+
const { amount_a, amount_b, liquidity = '0' } = bin
|
|
62
|
+
const used_liquidity = d(rate).mul(liquidity).toFixed(0)
|
|
63
|
+
const used_amount_a = d(amount_a).mul(rate)
|
|
64
|
+
const used_amount_b = d(amount_b).mul(rate)
|
|
65
|
+
|
|
66
|
+
used_total_amount_a = d(used_total_amount_a).plus(used_amount_a)
|
|
67
|
+
used_total_amount_b = d(used_total_amount_b).plus(used_amount_b)
|
|
68
|
+
|
|
69
|
+
if ((d(used_amount_a).lt(1) && d(used_amount_a).gt(0)) || (d(used_amount_b).lt(1) && d(used_amount_b).gt(0))) {
|
|
70
|
+
has_invalid_amount = true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
used_bins.push({
|
|
74
|
+
bin_id: bin.bin_id,
|
|
75
|
+
amount_a: used_amount_a.toFixed(0),
|
|
76
|
+
amount_b: used_amount_b.toFixed(0),
|
|
77
|
+
price_per_lamport: bin.price_per_lamport,
|
|
78
|
+
liquidity: used_liquidity,
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
bins: {
|
|
84
|
+
bins: used_bins,
|
|
85
|
+
amount_a: used_total_amount_a.toFixed(0),
|
|
86
|
+
amount_b: used_total_amount_b.toFixed(0),
|
|
87
|
+
},
|
|
88
|
+
has_invalid_amount,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Calculate the amount of token A and token B to be removed from a bin
|
|
94
|
+
* @param bin - The bin information
|
|
95
|
+
* @param remove_liquidity - The amount of liquidity to be removed
|
|
96
|
+
* @returns The amount of token A and token B to be removed
|
|
97
|
+
*/
|
|
98
|
+
static calculateOutByShare(bin: BinAmount, remove_liquidity: string) {
|
|
99
|
+
const { amount_a, amount_b, liquidity = '0' } = bin
|
|
100
|
+
|
|
101
|
+
if (liquidity === '0') {
|
|
102
|
+
return {
|
|
103
|
+
amount_a: '0',
|
|
104
|
+
amount_b: '0',
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (d(remove_liquidity).gte(d(liquidity))) {
|
|
109
|
+
return {
|
|
110
|
+
amount_a: amount_a,
|
|
111
|
+
amount_b: amount_b,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const amount_a_out = d(remove_liquidity).div(liquidity).mul(amount_a).toFixed(0, Decimal.ROUND_FLOOR)
|
|
116
|
+
const amount_b_out = d(remove_liquidity).div(liquidity).mul(amount_b).toFixed(0, Decimal.ROUND_FLOOR)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
amount_a: amount_a_out,
|
|
120
|
+
amount_b: amount_b_out,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get the number of positions in a range of bin ids
|
|
126
|
+
* @param lower_bin_id - The lower bin id
|
|
127
|
+
* @param upper_bin_id - The upper bin id
|
|
128
|
+
* @returns The number of positions
|
|
129
|
+
*/
|
|
130
|
+
static getPositionCount(lower_bin_id: number, upper_bin_id: number) {
|
|
131
|
+
const binDelta = d(upper_bin_id).sub(lower_bin_id).add(1)
|
|
132
|
+
const positionCount = binDelta.div(MAX_BIN_PER_POSITION)
|
|
133
|
+
return Number(positionCount.toFixed(0, Decimal.ROUND_UP))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Calculate the amount of liquidity following the constant sum formula `L = price * x + y`
|
|
138
|
+
* @param amount_a
|
|
139
|
+
* @param amount_b
|
|
140
|
+
* @param qPrice Price is in Q64x64
|
|
141
|
+
* @returns
|
|
142
|
+
*/
|
|
143
|
+
static getLiquidity(amount_a: string, amount_b: string, qPrice: string): string {
|
|
144
|
+
const px = d(qPrice).mul(amount_a)
|
|
145
|
+
const liquidity = px.add(d(amount_b).mul(d(2).pow(SCALE_OFFSET)))
|
|
146
|
+
return liquidity.toFixed(0)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Calculate amount_a from liquidity when all liquidity is in token A
|
|
151
|
+
* @param liquidity - The liquidity amount
|
|
152
|
+
* @param qPrice - Price in Q64x64 format
|
|
153
|
+
* @returns The amount of token A
|
|
154
|
+
*/
|
|
155
|
+
static getAmountAFromLiquidity(liquidity: string, qPrice: string): string {
|
|
156
|
+
return d(liquidity).div(d(qPrice)).toFixed(0)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Calculate amount_b from liquidity when all liquidity is in token B
|
|
161
|
+
* @param liquidity - The liquidity amount
|
|
162
|
+
* @returns The amount of token B
|
|
163
|
+
*/
|
|
164
|
+
static getAmountBFromLiquidity(liquidity: string): string {
|
|
165
|
+
return d(liquidity).div(d(2).pow(SCALE_OFFSET)).toFixed(0)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Calculate amounts from liquidity using the same logic as Move code
|
|
170
|
+
* @param amount_a - Current amount of token A in the bin
|
|
171
|
+
* @param amount_b - Current amount of token B in the bin
|
|
172
|
+
* @param delta_liquidity - The liquidity delta to calculate amounts for
|
|
173
|
+
* @param liquidity_supply - Total liquidity supply in the bin
|
|
174
|
+
* @returns [amount_a_out, amount_b_out]
|
|
175
|
+
*/
|
|
176
|
+
static getAmountsFromLiquidity(amount_a: string, amount_b: string, delta_liquidity: string, liquidity_supply: string): [string, string] {
|
|
177
|
+
if (d(liquidity_supply).isZero()) {
|
|
178
|
+
handleError(DlmmErrorCode.LiquiditySupplyIsZero, 'Liquidity supply is zero')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (d(delta_liquidity).gt(d(liquidity_supply))) {
|
|
182
|
+
handleError(DlmmErrorCode.InvalidDeltaLiquidity, 'Invalid delta liquidity')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (d(delta_liquidity).isZero()) {
|
|
186
|
+
return ['0', '0']
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let out_amount_a: string
|
|
190
|
+
if (d(amount_a).isZero()) {
|
|
191
|
+
out_amount_a = '0'
|
|
192
|
+
} else {
|
|
193
|
+
out_amount_a = d(amount_a).mul(d(delta_liquidity)).div(d(liquidity_supply)).toFixed(0, Decimal.ROUND_FLOOR)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let out_amount_b: string
|
|
197
|
+
if (d(amount_b).isZero()) {
|
|
198
|
+
out_amount_b = '0'
|
|
199
|
+
} else {
|
|
200
|
+
out_amount_b = d(amount_b).mul(d(delta_liquidity)).div(d(liquidity_supply)).toFixed(0, Decimal.ROUND_FLOOR)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return [out_amount_a, out_amount_b]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get the price of a bin by bin id
|
|
208
|
+
* @param bin_id - The bin id
|
|
209
|
+
* @param bin_step - The bin step
|
|
210
|
+
* @param decimal_a - The decimal of the token a
|
|
211
|
+
* @param decimal_b - The decimal of the token b
|
|
212
|
+
* @returns The price of the bin
|
|
213
|
+
*/
|
|
214
|
+
static getPriceFromBinId(bin_id: number, bin_step: number, decimal_a: number, decimal_b: number): string {
|
|
215
|
+
const pricePerLamport = BinUtils.getPricePerLamportFromBinId(bin_id, bin_step)
|
|
216
|
+
return BinUtils.getPriceFromLamport(decimal_a, decimal_b, pricePerLamport).toString()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get the price per lamport of a bin by bin id
|
|
221
|
+
* @param bin_id - The bin id
|
|
222
|
+
* @param bin_step - The bin step
|
|
223
|
+
* @returns The price per lamport of the bin
|
|
224
|
+
*/
|
|
225
|
+
static getPricePerLamportFromBinId(bin_id: number, bin_step: number): string {
|
|
226
|
+
const binStepNum = new Decimal(bin_step).div(new Decimal(BASIS_POINT_MAX))
|
|
227
|
+
return new Decimal(1).add(new Decimal(binStepNum)).pow(new Decimal(bin_id)).toString()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get the bin id from a price
|
|
232
|
+
* @param price - The price
|
|
233
|
+
* @param binStep - The bin step
|
|
234
|
+
* @param min - Whether to use the minimum or maximum bin id
|
|
235
|
+
* @param decimal_a - The decimal of the token a
|
|
236
|
+
* @param decimal_b - The decimal of the token b
|
|
237
|
+
* @returns The bin id
|
|
238
|
+
*/
|
|
239
|
+
public static getBinIdFromPrice(price: string, binStep: number, min: boolean, decimal_a: number, decimal_b: number): number {
|
|
240
|
+
const pricePerLamport = BinUtils.getPricePerLamport(decimal_a, decimal_b, price)
|
|
241
|
+
return BinUtils.getBinIdFromLamportPrice(pricePerLamport, binStep, min)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get the bin id from a price per lamport
|
|
246
|
+
* @param pricePerLamport - The price per lamport
|
|
247
|
+
* @param binStep - The bin step
|
|
248
|
+
* @param min - Whether to use the minimum or maximum bin id
|
|
249
|
+
* @returns The bin id
|
|
250
|
+
*/
|
|
251
|
+
public static getBinIdFromLamportPrice(pricePerLamport: string, binStep: number, min: boolean): number {
|
|
252
|
+
const binStepNum = new Decimal(binStep).div(new Decimal(BASIS_POINT_MAX))
|
|
253
|
+
const binId = new Decimal(pricePerLamport).log().dividedBy(new Decimal(1).add(binStepNum).log())
|
|
254
|
+
return (min ? binId.floor() : binId.ceil()).toNumber()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get the price per lamport
|
|
259
|
+
* @param decimal_a - The decimal of the token a
|
|
260
|
+
* @param decimal_b - The decimal of the token b
|
|
261
|
+
* @param price - The price
|
|
262
|
+
* @returns The price per lamport
|
|
263
|
+
*/
|
|
264
|
+
public static getPricePerLamport(decimal_a: number, decimal_b: number, price: string): string {
|
|
265
|
+
return new Decimal(price).mul(new Decimal(10 ** (decimal_b - decimal_a))).toString()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Convert price per lamport back to original price
|
|
270
|
+
* @param decimal_a - The decimal of the token a
|
|
271
|
+
* @param decimal_b - The decimal of the token b
|
|
272
|
+
* @param pricePerLamport - The price per lamport
|
|
273
|
+
* @returns The original price
|
|
274
|
+
*/
|
|
275
|
+
public static getPriceFromLamport(decimal_a: number, decimal_b: number, pricePerLamport: string): string {
|
|
276
|
+
return new Decimal(pricePerLamport).div(new Decimal(10 ** (decimal_b - decimal_a))).toString()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get the reverse price
|
|
281
|
+
* @param price - The price
|
|
282
|
+
* @returns The reverse price
|
|
283
|
+
*/
|
|
284
|
+
public static getReversePrice(price: string): string {
|
|
285
|
+
return new Decimal(1).div(price).toString()
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get the price of a bin by bin id
|
|
290
|
+
* @param binId - The bin id
|
|
291
|
+
* @param binStep - The bin step
|
|
292
|
+
* @returns The price of the bin
|
|
293
|
+
*/
|
|
294
|
+
static getQPriceFromId(binId: number, binStep: number): string {
|
|
295
|
+
const bps = new BN(binStep).shln(SCALE_OFFSET).div(new BN(BASIS_POINT_MAX))
|
|
296
|
+
const base = ONE.add(bps)
|
|
297
|
+
return BinUtils.pow(base, new BN(binId)).toString()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Convert QPrice (Q64x64 format) to actual price
|
|
302
|
+
* @param qPrice - The price in Q64x64 format
|
|
303
|
+
* @returns The actual price
|
|
304
|
+
*/
|
|
305
|
+
static getPricePerLamportFromQPrice(qPrice: string): string {
|
|
306
|
+
return MathUtil.fromX64(new BN(qPrice)).toString()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
static pow(base: BN, exp: BN): BN {
|
|
310
|
+
let invert = exp.isNeg()
|
|
311
|
+
|
|
312
|
+
if (exp.isZero()) {
|
|
313
|
+
return ONE
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
exp = invert ? exp.abs() : exp
|
|
317
|
+
|
|
318
|
+
if (exp.gt(MAX_EXPONENTIAL)) {
|
|
319
|
+
return new BN(0)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let squaredBase = base
|
|
323
|
+
let result = ONE
|
|
324
|
+
|
|
325
|
+
if (squaredBase.gte(result)) {
|
|
326
|
+
squaredBase = MAX.div(squaredBase)
|
|
327
|
+
invert = !invert
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!exp.and(new BN(0x1)).isZero()) {
|
|
331
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
335
|
+
|
|
336
|
+
if (!exp.and(new BN(0x2)).isZero()) {
|
|
337
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
341
|
+
|
|
342
|
+
if (!exp.and(new BN(0x4)).isZero()) {
|
|
343
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
347
|
+
|
|
348
|
+
if (!exp.and(new BN(0x8)).isZero()) {
|
|
349
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
353
|
+
|
|
354
|
+
if (!exp.and(new BN(0x10)).isZero()) {
|
|
355
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
359
|
+
|
|
360
|
+
if (!exp.and(new BN(0x20)).isZero()) {
|
|
361
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
365
|
+
|
|
366
|
+
if (!exp.and(new BN(0x40)).isZero()) {
|
|
367
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
371
|
+
|
|
372
|
+
if (!exp.and(new BN(0x80)).isZero()) {
|
|
373
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
377
|
+
|
|
378
|
+
if (!exp.and(new BN(0x100)).isZero()) {
|
|
379
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
383
|
+
|
|
384
|
+
if (!exp.and(new BN(0x200)).isZero()) {
|
|
385
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
389
|
+
|
|
390
|
+
if (!exp.and(new BN(0x400)).isZero()) {
|
|
391
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
395
|
+
|
|
396
|
+
if (!exp.and(new BN(0x800)).isZero()) {
|
|
397
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
401
|
+
|
|
402
|
+
if (!exp.and(new BN(0x1000)).isZero()) {
|
|
403
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
407
|
+
|
|
408
|
+
if (!exp.and(new BN(0x2000)).isZero()) {
|
|
409
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
413
|
+
|
|
414
|
+
if (!exp.and(new BN(0x4000)).isZero()) {
|
|
415
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
419
|
+
|
|
420
|
+
if (!exp.and(new BN(0x8000)).isZero()) {
|
|
421
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
425
|
+
|
|
426
|
+
if (!exp.and(new BN(0x10000)).isZero()) {
|
|
427
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
431
|
+
|
|
432
|
+
if (!exp.and(new BN(0x20000)).isZero()) {
|
|
433
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
squaredBase = squaredBase.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
437
|
+
|
|
438
|
+
if (!exp.and(new BN(0x40000)).isZero()) {
|
|
439
|
+
result = result.mul(squaredBase).shrn(SCALE_OFFSET)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (result.isZero()) {
|
|
443
|
+
return new BN(0)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (invert) {
|
|
447
|
+
result = MAX.div(result)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return result
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Converts a bin ID to a score by adding the bin bound and validating the range
|
|
455
|
+
* @param binId - The bin ID to convert
|
|
456
|
+
* @returns The calculated bin score
|
|
457
|
+
* @throws Error if the bin ID is invalid
|
|
458
|
+
*/
|
|
459
|
+
static binScore(binId: number): string {
|
|
460
|
+
const score = BigInt(binId) + BIN_BOUND
|
|
461
|
+
if (score < 0n || score > BIN_BOUND * 2n) {
|
|
462
|
+
handleError(DlmmErrorCode.InvalidBinId, new Error('Invalid bin ID'), {
|
|
463
|
+
[DETAILS_KEYS.METHOD_NAME]: 'binScore',
|
|
464
|
+
[DETAILS_KEYS.REQUEST_PARAMS]: { binId },
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return score.toString()
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Converts a score back to bin ID by subtracting the bin bound
|
|
473
|
+
* @param score - The score to convert
|
|
474
|
+
* @returns The calculated bin ID
|
|
475
|
+
* @throws Error if the score is invalid
|
|
476
|
+
*/
|
|
477
|
+
static scoreToBinId(score: string): number {
|
|
478
|
+
const binId = BigInt(score) - BIN_BOUND
|
|
479
|
+
if (binId < -BIN_BOUND || binId > BIN_BOUND) {
|
|
480
|
+
handleError(DlmmErrorCode.InvalidBinId, new Error('Invalid score'), {
|
|
481
|
+
[DETAILS_KEYS.METHOD_NAME]: 'scoreToBinId',
|
|
482
|
+
[DETAILS_KEYS.REQUEST_PARAMS]: { score },
|
|
483
|
+
})
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return Number(binId)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Resolves the bin position from a score.
|
|
491
|
+
*
|
|
492
|
+
* @param score - The score to resolve
|
|
493
|
+
* @returns Tuple of [group index, offset in group]
|
|
494
|
+
*/
|
|
495
|
+
static resolveBinPosition(score: string): [string, number] {
|
|
496
|
+
const scoreBigInt = BigInt(score)
|
|
497
|
+
const groupIndex = scoreBigInt >> 4n
|
|
498
|
+
const offsetInGroup = Number(scoreBigInt & 0xfn)
|
|
499
|
+
|
|
500
|
+
return [groupIndex.toString(), offsetInGroup]
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
static findMinMaxBinId(binStep: number) {
|
|
504
|
+
const base = 1 + binStep / BASIS_POINT_MAX
|
|
505
|
+
const maxQPriceSupported = new Decimal('18446744073709551615')
|
|
506
|
+
const n = maxQPriceSupported.log(10).div(new Decimal(base).log(10)).floor()
|
|
507
|
+
|
|
508
|
+
let minBinId = n.neg()
|
|
509
|
+
let maxBinId = n
|
|
510
|
+
|
|
511
|
+
let minQPrice = d(1)
|
|
512
|
+
let maxQPrice = d('340282366920938463463374607431768211455')
|
|
513
|
+
|
|
514
|
+
while (true) {
|
|
515
|
+
const qPrice = d(BinUtils.getQPriceFromId(minBinId.toNumber(), binStep))
|
|
516
|
+
if (qPrice.gt(minQPrice) && !qPrice.isZero()) {
|
|
517
|
+
break
|
|
518
|
+
} else {
|
|
519
|
+
minBinId = minBinId.add(1)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
while (true) {
|
|
524
|
+
const qPrice = d(BinUtils.getQPriceFromId(maxBinId.toNumber(), binStep))
|
|
525
|
+
if (qPrice.lt(maxQPrice) && !qPrice.isZero()) {
|
|
526
|
+
break
|
|
527
|
+
} else {
|
|
528
|
+
maxBinId = maxBinId.sub(1)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
minBinId: minBinId.toNumber(),
|
|
534
|
+
maxBinId: maxBinId.toNumber(),
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
static getBinShift(active_id: number, bin_step: number, max_price_slippage: number): number {
|
|
539
|
+
const price = BinUtils.getPricePerLamportFromBinId(active_id, bin_step)
|
|
540
|
+
const price_limit = d(price)
|
|
541
|
+
.mul(1 + max_price_slippage)
|
|
542
|
+
.toString()
|
|
543
|
+
const slippage_active_id = BinUtils.getBinIdFromLamportPrice(price_limit, bin_step, true)
|
|
544
|
+
const bin_shift = d(slippage_active_id).sub(active_id).abs().toFixed(0, Decimal.ROUND_UP)
|
|
545
|
+
|
|
546
|
+
console.log('getBinShift Options:', {
|
|
547
|
+
active_id,
|
|
548
|
+
bin_shift,
|
|
549
|
+
})
|
|
550
|
+
return Number(bin_shift)
|
|
551
|
+
}
|
|
552
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { d } from '@cetusprotocol/common-sdk'
|
|
2
|
+
import { BinAmount, BinStepConfig, VariableParameters } from '../types/dlmm'
|
|
3
|
+
import { BinUtils } from './binUtils'
|
|
4
|
+
import { BASIS_POINT, FEE_PRECISION, MAX_FEE_RATE } from '../types/constants'
|
|
5
|
+
|
|
6
|
+
export class FeeUtils {
|
|
7
|
+
static getVariableFee(variableParameters: VariableParameters): string {
|
|
8
|
+
const { volatility_accumulator, bin_step_config } = variableParameters
|
|
9
|
+
const { variable_fee_control, bin_step } = bin_step_config
|
|
10
|
+
|
|
11
|
+
if (d(variable_fee_control).gt(0)) {
|
|
12
|
+
const square_vfa_bin = d(volatility_accumulator).mul(bin_step).pow(2)
|
|
13
|
+
const v_fee = square_vfa_bin.mul(variable_fee_control)
|
|
14
|
+
const scaled_v_fee = v_fee.add(99_999_999_999).div(100_000_000_000)
|
|
15
|
+
return scaled_v_fee.toFixed(0)
|
|
16
|
+
}
|
|
17
|
+
return '0'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static calculateCompositionFee(amount: string, total_fee_rate: string) {
|
|
21
|
+
const fee_amount = d(amount).mul(total_fee_rate)
|
|
22
|
+
const composition_fee = d(fee_amount).mul(d(FEE_PRECISION).add(total_fee_rate))
|
|
23
|
+
return composition_fee.div(d(FEE_PRECISION).pow(2)).toFixed(0)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static calculateProtocolFee(fee_amount: string, protocol_fee_rate: string) {
|
|
27
|
+
const protocol_fee = d(fee_amount).mul(protocol_fee_rate).div(BASIS_POINT).ceil().toFixed(0)
|
|
28
|
+
return protocol_fee
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static getProtocolFees(fee_a: string, fee_b: string, protocol_fee_rate: string) {
|
|
32
|
+
const protocol_fee_a = FeeUtils.calculateProtocolFee(fee_a, protocol_fee_rate)
|
|
33
|
+
const protocol_fee_b = FeeUtils.calculateProtocolFee(fee_b, protocol_fee_rate)
|
|
34
|
+
return {
|
|
35
|
+
protocol_fee_a,
|
|
36
|
+
protocol_fee_b,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static getCompositionFees(
|
|
41
|
+
active_bin: BinAmount,
|
|
42
|
+
used_bin: BinAmount,
|
|
43
|
+
binStepConfig: BinStepConfig,
|
|
44
|
+
variableParameters: VariableParameters
|
|
45
|
+
): { fees_a: string; fees_b: string } {
|
|
46
|
+
if (d(active_bin.liquidity || '0').eq(d(0))) {
|
|
47
|
+
return {
|
|
48
|
+
fees_a: '0',
|
|
49
|
+
fees_b: '0',
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const { bin_step, base_factor } = binStepConfig
|
|
53
|
+
const qPrice = BinUtils.getQPriceFromId(active_bin.bin_id, bin_step)
|
|
54
|
+
const used_liquidity = BinUtils.getLiquidity(used_bin.amount_a, used_bin.amount_b, qPrice)
|
|
55
|
+
|
|
56
|
+
const { amount_a: amount_a_out, amount_b: amount_b_out } = BinUtils.calculateOutByShare(
|
|
57
|
+
{
|
|
58
|
+
bin_id: active_bin.bin_id,
|
|
59
|
+
liquidity: d(active_bin.liquidity).add(used_liquidity).toFixed(0),
|
|
60
|
+
amount_a: d(active_bin.amount_a).add(used_bin.amount_a).toFixed(0),
|
|
61
|
+
amount_b: d(active_bin.amount_b).add(used_bin.amount_b).toFixed(0),
|
|
62
|
+
price_per_lamport: active_bin.price_per_lamport,
|
|
63
|
+
},
|
|
64
|
+
used_liquidity
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const base_fee = d(bin_step).mul(base_factor)
|
|
68
|
+
const variable_fee = FeeUtils.getVariableFee(variableParameters)
|
|
69
|
+
|
|
70
|
+
let total_fee_rate = d(base_fee).add(variable_fee).toFixed(0)
|
|
71
|
+
|
|
72
|
+
if (d(total_fee_rate).gt(MAX_FEE_RATE)) {
|
|
73
|
+
total_fee_rate = MAX_FEE_RATE.toString()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let fees_a = '0'
|
|
77
|
+
let fees_b = '0'
|
|
78
|
+
|
|
79
|
+
if (d(amount_a_out).gt(used_bin.amount_a)) {
|
|
80
|
+
fees_a = FeeUtils.calculateCompositionFee(amount_a_out, total_fee_rate)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (d(amount_b_out).gt(used_bin.amount_b)) {
|
|
84
|
+
fees_b = FeeUtils.calculateCompositionFee(amount_b_out, total_fee_rate)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
fees_a,
|
|
89
|
+
fees_b,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|