@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,510 @@
|
|
|
1
|
+
import { d, DETAILS_KEYS } from '@cetusprotocol/common-sdk'
|
|
2
|
+
import { BinAmount, BinLiquidityInfo, BinWeight, StrategyType } from '../types/dlmm'
|
|
3
|
+
import Decimal from 'decimal.js'
|
|
4
|
+
import { DlmmErrorCode, handleError } from '../errors/errors'
|
|
5
|
+
import { DEFAULT_MAX_WEIGHT, DEFAULT_MIN_WEIGHT } from '../types/constants'
|
|
6
|
+
import { BinUtils } from './binUtils'
|
|
7
|
+
import { safeMulAmount } from './parseData'
|
|
8
|
+
|
|
9
|
+
export class WeightUtils {
|
|
10
|
+
static toWeightSpotBalanced(min_bin_id: number, max_bin_id: number): BinWeight[] {
|
|
11
|
+
let distributions = []
|
|
12
|
+
for (let i = min_bin_id; i <= max_bin_id; i++) {
|
|
13
|
+
distributions.push({
|
|
14
|
+
bin_id: i,
|
|
15
|
+
weight: 1,
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
return distributions
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static toWeightDescendingOrder(min_bin_id: number, max_bin_id: number): BinWeight[] {
|
|
22
|
+
let distributions = []
|
|
23
|
+
for (let i = min_bin_id; i <= max_bin_id; i++) {
|
|
24
|
+
distributions.push({
|
|
25
|
+
bin_id: i,
|
|
26
|
+
weight: max_bin_id - i + 1,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
return distributions
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static toWeightAscendingOrder(min_bin_id: number, max_bin_id: number): BinWeight[] {
|
|
33
|
+
let distributions = []
|
|
34
|
+
for (let i = min_bin_id; i <= max_bin_id; i++) {
|
|
35
|
+
distributions.push({
|
|
36
|
+
bin_id: i,
|
|
37
|
+
weight: i - min_bin_id + 1,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
return distributions
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static toWeightCurve(min_bin_id: number, max_bin_id: number, active_id: number): BinWeight[] {
|
|
44
|
+
if (active_id < min_bin_id) {
|
|
45
|
+
return WeightUtils.toWeightDescendingOrder(min_bin_id, max_bin_id)
|
|
46
|
+
} else if (active_id > max_bin_id) {
|
|
47
|
+
return WeightUtils.toWeightAscendingOrder(min_bin_id, max_bin_id)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let maxWeight = DEFAULT_MAX_WEIGHT
|
|
51
|
+
let minWeight = DEFAULT_MIN_WEIGHT
|
|
52
|
+
|
|
53
|
+
let diffWeight = maxWeight - minWeight
|
|
54
|
+
let diffMinWeight = active_id > min_bin_id ? Math.floor(diffWeight / (active_id - min_bin_id)) : 0
|
|
55
|
+
let diffMaxWeight = max_bin_id > active_id ? Math.floor(diffWeight / (max_bin_id - active_id)) : 0
|
|
56
|
+
|
|
57
|
+
let distributions: BinWeight[] = []
|
|
58
|
+
for (let i = min_bin_id; i <= max_bin_id; i++) {
|
|
59
|
+
if (i < active_id) {
|
|
60
|
+
distributions.push({
|
|
61
|
+
bin_id: i,
|
|
62
|
+
weight: maxWeight - (active_id - i) * diffMinWeight,
|
|
63
|
+
})
|
|
64
|
+
} else if (i > active_id) {
|
|
65
|
+
distributions.push({
|
|
66
|
+
bin_id: i,
|
|
67
|
+
weight: maxWeight - (i - active_id) * diffMaxWeight,
|
|
68
|
+
})
|
|
69
|
+
} else {
|
|
70
|
+
distributions.push({
|
|
71
|
+
bin_id: i,
|
|
72
|
+
weight: maxWeight,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return distributions
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static toWeightBidAsk(min_bin_id: number, max_bin_id: number, active_id: number): BinWeight[] {
|
|
80
|
+
if (active_id > max_bin_id) {
|
|
81
|
+
return WeightUtils.toWeightDescendingOrder(min_bin_id, max_bin_id)
|
|
82
|
+
} else if (active_id < min_bin_id) {
|
|
83
|
+
return WeightUtils.toWeightAscendingOrder(min_bin_id, max_bin_id)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let maxWeight = DEFAULT_MAX_WEIGHT
|
|
87
|
+
let minWeight = DEFAULT_MIN_WEIGHT
|
|
88
|
+
|
|
89
|
+
let diffWeight = maxWeight - minWeight
|
|
90
|
+
let diffMinWeight = active_id > min_bin_id ? Math.floor(diffWeight / (active_id - min_bin_id)) : 0
|
|
91
|
+
let diffMaxWeight = max_bin_id > active_id ? Math.floor(diffWeight / (max_bin_id - active_id)) : 0
|
|
92
|
+
|
|
93
|
+
let distributions: BinWeight[] = []
|
|
94
|
+
for (let i = min_bin_id; i <= max_bin_id; i++) {
|
|
95
|
+
if (i < active_id) {
|
|
96
|
+
distributions.push({
|
|
97
|
+
bin_id: i,
|
|
98
|
+
weight: minWeight + (active_id - i) * diffMinWeight,
|
|
99
|
+
})
|
|
100
|
+
} else if (i > active_id) {
|
|
101
|
+
distributions.push({
|
|
102
|
+
bin_id: i,
|
|
103
|
+
weight: minWeight + (i - active_id) * diffMaxWeight,
|
|
104
|
+
})
|
|
105
|
+
} else {
|
|
106
|
+
distributions.push({
|
|
107
|
+
bin_id: i,
|
|
108
|
+
weight: minWeight,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return distributions
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Distribute totalAmount to all bid side bins according to given distributions.
|
|
117
|
+
* @param active_id - active bin id
|
|
118
|
+
* @param amount_b - total amount of coin b to be distributed
|
|
119
|
+
* @param distributions - weight distribution of each bin
|
|
120
|
+
* @returns array of {binId, amount} where amount is the amount of coin b in each bin
|
|
121
|
+
*/
|
|
122
|
+
static toAmountBidSide(
|
|
123
|
+
active_id: number,
|
|
124
|
+
amount_b: string,
|
|
125
|
+
bin_step: number,
|
|
126
|
+
distributions: BinWeight[],
|
|
127
|
+
contain_active_bin = false
|
|
128
|
+
): BinLiquidityInfo {
|
|
129
|
+
// get sum of weight
|
|
130
|
+
const totalWeight = distributions
|
|
131
|
+
.filter((bin) => bin.bin_id <= active_id)
|
|
132
|
+
.reduce(function (sum, el) {
|
|
133
|
+
if (contain_active_bin) {
|
|
134
|
+
return el.bin_id > active_id ? sum : sum.add(el.weight) // skip all ask side
|
|
135
|
+
} else {
|
|
136
|
+
return el.bin_id >= active_id ? sum : sum.add(el.weight) // skip all ask side
|
|
137
|
+
}
|
|
138
|
+
}, d(0))
|
|
139
|
+
|
|
140
|
+
if (totalWeight.cmp(d(0)) != 1) {
|
|
141
|
+
throw Error('Invalid parameters')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const bin_amounts = distributions.map((bin) => {
|
|
145
|
+
let price_per_lamport = BinUtils.getPricePerLamportFromBinId(bin.bin_id, bin_step)
|
|
146
|
+
|
|
147
|
+
const isValidBin = bin.bin_id <= active_id
|
|
148
|
+
|
|
149
|
+
if (!isValidBin || (bin.bin_id >= active_id && !contain_active_bin)) {
|
|
150
|
+
return {
|
|
151
|
+
bin_id: bin.bin_id,
|
|
152
|
+
amount_a: '0',
|
|
153
|
+
amount_b: '0',
|
|
154
|
+
price_per_lamport,
|
|
155
|
+
liquidity: '0',
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
const rate = d(bin.weight).div(totalWeight)
|
|
159
|
+
const amount_b_in_bin = safeMulAmount(d(amount_b), rate).toString()
|
|
160
|
+
const amount_a = '0'
|
|
161
|
+
const qPrice = BinUtils.getQPriceFromId(bin.bin_id, bin_step)
|
|
162
|
+
const liquidity = BinUtils.getLiquidity(amount_a, amount_b_in_bin, qPrice)
|
|
163
|
+
return {
|
|
164
|
+
bin_id: bin.bin_id,
|
|
165
|
+
amount_b: amount_b_in_bin,
|
|
166
|
+
amount_a,
|
|
167
|
+
price_per_lamport,
|
|
168
|
+
liquidity,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
bins: bin_amounts,
|
|
175
|
+
amount_a: '0',
|
|
176
|
+
amount_b,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Distribute totalAmount to all ask side bins according to given distributions.
|
|
182
|
+
* @param active_id active bin id
|
|
183
|
+
* @param amount_a total amount of coin a to be distributed
|
|
184
|
+
* @param distributions weight distribution of each bin
|
|
185
|
+
* @returns array of {binId, amount} where amount is the amount of coin a in each bin
|
|
186
|
+
*/
|
|
187
|
+
static toAmountAskSide(
|
|
188
|
+
active_id: number,
|
|
189
|
+
bin_step: number,
|
|
190
|
+
amount_a: string,
|
|
191
|
+
distributions: BinWeight[],
|
|
192
|
+
contain_active_bin = false
|
|
193
|
+
): BinLiquidityInfo {
|
|
194
|
+
// get sum of weight
|
|
195
|
+
const totalWeight: Decimal = distributions
|
|
196
|
+
.filter((bin) => bin.bin_id >= active_id)
|
|
197
|
+
.reduce(function (sum, el) {
|
|
198
|
+
if (el.bin_id <= active_id && !contain_active_bin) {
|
|
199
|
+
return sum
|
|
200
|
+
} else {
|
|
201
|
+
const price_per_lamport = BinUtils.getPricePerLamportFromBinId(el.bin_id, bin_step)
|
|
202
|
+
const weightPerPrice = new Decimal(el.weight).div(price_per_lamport)
|
|
203
|
+
return sum.add(weightPerPrice)
|
|
204
|
+
}
|
|
205
|
+
}, new Decimal(0))
|
|
206
|
+
|
|
207
|
+
if (totalWeight.cmp(new Decimal(0)) != 1) {
|
|
208
|
+
throw Error('Invalid parameters')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const bin_amounts = distributions.map((bin) => {
|
|
212
|
+
let price_per_lamport = BinUtils.getPricePerLamportFromBinId(bin.bin_id, bin_step)
|
|
213
|
+
const isValidBin = bin.bin_id >= active_id
|
|
214
|
+
|
|
215
|
+
if (!isValidBin || (bin.bin_id <= active_id && !contain_active_bin)) {
|
|
216
|
+
return {
|
|
217
|
+
bin_id: bin.bin_id,
|
|
218
|
+
amount_a: '0',
|
|
219
|
+
amount_b: '0',
|
|
220
|
+
price_per_lamport,
|
|
221
|
+
liquidity: '0',
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
const weightPerPrice = new Decimal(bin.weight).div(price_per_lamport)
|
|
225
|
+
const rate = weightPerPrice.div(totalWeight)
|
|
226
|
+
const amount_a_in_bin = safeMulAmount(d(amount_a), rate).toString()
|
|
227
|
+
const amount_b = '0'
|
|
228
|
+
const qPrice = BinUtils.getQPriceFromId(bin.bin_id, bin_step)
|
|
229
|
+
const liquidity = BinUtils.getLiquidity(amount_a_in_bin, amount_b, qPrice)
|
|
230
|
+
return {
|
|
231
|
+
bin_id: bin.bin_id,
|
|
232
|
+
amount_a: amount_a_in_bin,
|
|
233
|
+
amount_b,
|
|
234
|
+
price_per_lamport,
|
|
235
|
+
liquidity,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
bins: bin_amounts,
|
|
242
|
+
amount_a,
|
|
243
|
+
amount_b: '0',
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Distributes the given amounts of tokens X and Y to both bid and ask side bins
|
|
249
|
+
* based on the provided weight distributions.
|
|
250
|
+
*
|
|
251
|
+
* @param activeId - The id of the active bin.
|
|
252
|
+
* @param binStep - The step interval between bin ids.
|
|
253
|
+
* @param amountX - Total amount of token X to distribute.
|
|
254
|
+
* @param amountY - Total amount of token Y to distribute.
|
|
255
|
+
* @param amountXInActiveBin - Amount of token X already in the active bin.
|
|
256
|
+
* @param amountYInActiveBin - Amount of token Y already in the active bin.
|
|
257
|
+
* @param distributions - Array of bins with their respective weight distributions.
|
|
258
|
+
* @param mintX - Mint information for token X. Get from DLMM instance.
|
|
259
|
+
* @param mintY - Mint information for token Y. Get from DLMM instance.
|
|
260
|
+
* @param clock - Clock instance. Get from DLMM instance.
|
|
261
|
+
* @returns An array of objects containing binId, amountX, and amountY for each bin.
|
|
262
|
+
*/
|
|
263
|
+
static toAmountBothSide(
|
|
264
|
+
active_id: number,
|
|
265
|
+
bin_step: number,
|
|
266
|
+
amount_a: string,
|
|
267
|
+
amount_b: string,
|
|
268
|
+
amount_a_in_active_bin: string,
|
|
269
|
+
amount_b_in_active_bin: string,
|
|
270
|
+
distributions: BinWeight[]
|
|
271
|
+
): BinLiquidityInfo {
|
|
272
|
+
const isOnlyAmountA = !d(amount_a).isZero() && d(amount_b).isZero()
|
|
273
|
+
const isOnlyAmountB = d(amount_a).isZero() && !d(amount_b).isZero()
|
|
274
|
+
|
|
275
|
+
// only bid side
|
|
276
|
+
if (active_id > distributions[distributions.length - 1].bin_id) {
|
|
277
|
+
return WeightUtils.toAmountBidSide(active_id, amount_b, bin_step, distributions)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (isOnlyAmountB && active_id !== distributions[distributions.length - 1].bin_id) {
|
|
281
|
+
return WeightUtils.toAmountBidSide(active_id, amount_b, bin_step, distributions, true)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// only ask side
|
|
285
|
+
if (active_id < distributions[0].bin_id) {
|
|
286
|
+
return WeightUtils.toAmountAskSide(active_id, bin_step, amount_a, distributions)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (isOnlyAmountA && active_id !== distributions[0].bin_id) {
|
|
290
|
+
return WeightUtils.toAmountAskSide(active_id, bin_step, amount_a, distributions, true)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const activeBins = distributions.filter((element) => {
|
|
294
|
+
return element.bin_id === active_id
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
if (activeBins.length === 1) {
|
|
298
|
+
const { totalWeightA, totalWeightB, activeWeightA, activeWeightB } = WeightUtils.calculateTotalWeights(
|
|
299
|
+
bin_step,
|
|
300
|
+
distributions,
|
|
301
|
+
active_id,
|
|
302
|
+
activeBins[0],
|
|
303
|
+
amount_a_in_active_bin,
|
|
304
|
+
amount_b_in_active_bin,
|
|
305
|
+
isOnlyAmountA ? 'a' : isOnlyAmountB ? 'b' : undefined
|
|
306
|
+
)
|
|
307
|
+
const kA = new Decimal(amount_a.toString()).div(totalWeightA)
|
|
308
|
+
const kB = new Decimal(amount_b.toString()).div(totalWeightB)
|
|
309
|
+
const bin_amounts = distributions.map((bin) => {
|
|
310
|
+
let price_per_lamport = BinUtils.getPricePerLamportFromBinId(bin.bin_id, bin_step)
|
|
311
|
+
if (bin.bin_id < active_id || (bin.bin_id === active_id && isOnlyAmountB)) {
|
|
312
|
+
const amount_b = safeMulAmount(kB, new Decimal(bin.weight))
|
|
313
|
+
const amount_a = '0'
|
|
314
|
+
const qPrice = BinUtils.getQPriceFromId(bin.bin_id, bin_step)
|
|
315
|
+
const liquidity = BinUtils.getLiquidity(amount_a, amount_b.toString(), qPrice)
|
|
316
|
+
return {
|
|
317
|
+
bin_id: bin.bin_id,
|
|
318
|
+
amount_a: '0',
|
|
319
|
+
amount_b: amount_b.toString(),
|
|
320
|
+
price_per_lamport,
|
|
321
|
+
liquidity,
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (bin.bin_id > active_id || (bin.bin_id === active_id && isOnlyAmountA)) {
|
|
325
|
+
const weighPerPrice = new Decimal(bin.weight).div(price_per_lamport)
|
|
326
|
+
const amount_a = safeMulAmount(kA, new Decimal(weighPerPrice))
|
|
327
|
+
const amount_b = '0'
|
|
328
|
+
const qPrice = BinUtils.getQPriceFromId(bin.bin_id, bin_step)
|
|
329
|
+
const liquidity = BinUtils.getLiquidity(amount_a.toString(), amount_b, qPrice)
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
bin_id: bin.bin_id,
|
|
333
|
+
amount_a: amount_a.toString(),
|
|
334
|
+
amount_b: '0',
|
|
335
|
+
price_per_lamport,
|
|
336
|
+
liquidity,
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const amountAActiveBin = safeMulAmount(kA, activeWeightA)
|
|
341
|
+
const amountBActiveBin = safeMulAmount(kB, activeWeightB)
|
|
342
|
+
let amount_a = amountAActiveBin.toString()
|
|
343
|
+
let amount_b = amountBActiveBin.toString()
|
|
344
|
+
|
|
345
|
+
const qPrice = BinUtils.getQPriceFromId(bin.bin_id, bin_step)
|
|
346
|
+
const liquidity = BinUtils.getLiquidity(amount_a, amount_b, qPrice)
|
|
347
|
+
return {
|
|
348
|
+
bin_id: bin.bin_id,
|
|
349
|
+
amount_a,
|
|
350
|
+
amount_b,
|
|
351
|
+
price_per_lamport,
|
|
352
|
+
liquidity,
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
const total_amount_a = bin_amounts.reduce((sum, bin) => d(sum).add(d(bin.amount_a)), d(0)).toString()
|
|
357
|
+
const total_amount_b = bin_amounts.reduce((sum, bin) => d(sum).add(d(bin.amount_b)), d(0)).toString()
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
bins: bin_amounts,
|
|
361
|
+
amount_a: total_amount_a,
|
|
362
|
+
amount_b: total_amount_b,
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
const { totalWeightA, totalWeightB } = WeightUtils.calculateTotalWeights(bin_step, distributions, active_id)
|
|
366
|
+
let kA = new Decimal(amount_a.toString()).div(totalWeightA)
|
|
367
|
+
let kB = new Decimal(amount_b.toString()).div(totalWeightB)
|
|
368
|
+
// let k = kA.lessThan(kB) ? kA : kB
|
|
369
|
+
|
|
370
|
+
const bin_amounts = distributions.map((bin) => {
|
|
371
|
+
let price_per_lamport = BinUtils.getPricePerLamportFromBinId(bin.bin_id, bin_step)
|
|
372
|
+
if (bin.bin_id < active_id) {
|
|
373
|
+
const amount = safeMulAmount(kB, new Decimal(bin.weight))
|
|
374
|
+
return {
|
|
375
|
+
bin_id: bin.bin_id,
|
|
376
|
+
amount_a: '0',
|
|
377
|
+
amount_b: amount.toString(),
|
|
378
|
+
price_per_lamport,
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
let weighPerPrice = new Decimal(bin.weight).div(price_per_lamport)
|
|
382
|
+
const amount = safeMulAmount(kA, weighPerPrice)
|
|
383
|
+
return {
|
|
384
|
+
bin_id: bin.bin_id,
|
|
385
|
+
amount_a: amount.toString(),
|
|
386
|
+
amount_b: '0',
|
|
387
|
+
price_per_lamport,
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
const total_amount_a = bin_amounts.reduce((sum, bin) => d(sum).add(d(bin.amount_a)), d(0)).toString()
|
|
393
|
+
const total_amount_b = bin_amounts.reduce((sum, bin) => d(sum).add(d(bin.amount_b)), d(0)).toString()
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
bins: bin_amounts,
|
|
397
|
+
amount_a: total_amount_a,
|
|
398
|
+
amount_b: total_amount_b,
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Distributes the given amount of coin B to both bid and ask side bins
|
|
405
|
+
* based on the provided weight distributions.
|
|
406
|
+
*
|
|
407
|
+
* @param active_id - The id of the active bin.
|
|
408
|
+
* @param bin_step - The step interval between bin ids.
|
|
409
|
+
* @param amount_a - Total amount of coin A to distribute.
|
|
410
|
+
* @param amount_a_in_active_bin - Amount of coin A already in the active bin.
|
|
411
|
+
* @param amount_b_in_active_bin - Amount of coin B already in the active bin.
|
|
412
|
+
* @param distributions - Array of bins with their respective weight distributions.
|
|
413
|
+
* @returns An array of objects containing binId, amountA, and amountB for each bin.
|
|
414
|
+
*/
|
|
415
|
+
static autoFillCoinByWeight(
|
|
416
|
+
active_id: number,
|
|
417
|
+
bin_step: number,
|
|
418
|
+
amount: string,
|
|
419
|
+
fix_amount_a: boolean,
|
|
420
|
+
amount_a_in_active_bin: string,
|
|
421
|
+
amount_b_in_active_bin: string,
|
|
422
|
+
distributions: BinWeight[]
|
|
423
|
+
): BinLiquidityInfo {
|
|
424
|
+
// only bid side
|
|
425
|
+
if (active_id > distributions[distributions.length - 1].bin_id) {
|
|
426
|
+
return WeightUtils.toAmountBidSide(active_id, amount, bin_step, distributions)
|
|
427
|
+
}
|
|
428
|
+
// only ask side
|
|
429
|
+
if (active_id < distributions[0].bin_id) {
|
|
430
|
+
return WeightUtils.toAmountAskSide(active_id, bin_step, amount, distributions)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const activeBins = distributions.filter((element) => {
|
|
434
|
+
return element.bin_id === active_id
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
const { totalWeightA, totalWeightB } = WeightUtils.calculateTotalWeights(
|
|
438
|
+
bin_step,
|
|
439
|
+
distributions,
|
|
440
|
+
active_id,
|
|
441
|
+
activeBins.length === 1 ? activeBins[0] : undefined,
|
|
442
|
+
activeBins.length === 1 ? amount_a_in_active_bin : undefined,
|
|
443
|
+
activeBins.length === 1 ? amount_b_in_active_bin : undefined
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
let k = d(0)
|
|
447
|
+
|
|
448
|
+
if (fix_amount_a) {
|
|
449
|
+
k = totalWeightA.isZero() ? new Decimal(1) : new Decimal(amount).div(totalWeightA)
|
|
450
|
+
} else {
|
|
451
|
+
k = totalWeightB.isZero() ? new Decimal(1) : new Decimal(amount).div(totalWeightB)
|
|
452
|
+
}
|
|
453
|
+
const other_amount = safeMulAmount(k, fix_amount_a ? totalWeightB : totalWeightA).toString()
|
|
454
|
+
|
|
455
|
+
return WeightUtils.toAmountBothSide(
|
|
456
|
+
active_id,
|
|
457
|
+
bin_step,
|
|
458
|
+
fix_amount_a ? amount : other_amount,
|
|
459
|
+
fix_amount_a ? other_amount : amount,
|
|
460
|
+
amount_a_in_active_bin,
|
|
461
|
+
amount_b_in_active_bin,
|
|
462
|
+
distributions
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
static calculateTotalWeights(
|
|
467
|
+
bin_step: number,
|
|
468
|
+
distributions: BinWeight[],
|
|
469
|
+
active_id: number,
|
|
470
|
+
activeBin?: BinWeight,
|
|
471
|
+
amount_a_in_active_bin?: string,
|
|
472
|
+
amount_b_in_active_bin?: string,
|
|
473
|
+
is_only_amount?: 'a' | 'b'
|
|
474
|
+
): { totalWeightA: Decimal; totalWeightB: Decimal; activeWeightA: Decimal; activeWeightB: Decimal } {
|
|
475
|
+
const p0 = d(BinUtils.getPricePerLamportFromBinId(active_id, bin_step))
|
|
476
|
+
let activeWeightA = d(0)
|
|
477
|
+
let activeWeightB = d(0)
|
|
478
|
+
|
|
479
|
+
if (amount_a_in_active_bin && amount_b_in_active_bin && activeBin && !is_only_amount) {
|
|
480
|
+
if (d(amount_a_in_active_bin).isZero() && d(amount_b_in_active_bin).isZero()) {
|
|
481
|
+
activeWeightA = new Decimal(activeBin.weight).div(p0.mul(new Decimal(2)))
|
|
482
|
+
activeWeightB = new Decimal(activeBin.weight).div(new Decimal(2))
|
|
483
|
+
} else {
|
|
484
|
+
let amountAInActiveBinDec = new Decimal(amount_a_in_active_bin.toString())
|
|
485
|
+
let amountBInActiveBinDec = new Decimal(amount_b_in_active_bin.toString())
|
|
486
|
+
|
|
487
|
+
if (!d(amount_a_in_active_bin).isZero()) {
|
|
488
|
+
activeWeightA = new Decimal(activeBin.weight).div(p0.add(amountBInActiveBinDec.div(amountAInActiveBinDec)))
|
|
489
|
+
}
|
|
490
|
+
if (!d(amount_b_in_active_bin).isZero()) {
|
|
491
|
+
activeWeightB = new Decimal(activeBin.weight).div(new Decimal(1).add(p0.mul(amountAInActiveBinDec).div(amountBInActiveBinDec)))
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
let totalWeightA = activeWeightA
|
|
497
|
+
let totalWeightB = activeWeightB
|
|
498
|
+
distributions.forEach((element) => {
|
|
499
|
+
if (element.bin_id < active_id || is_only_amount === 'b') {
|
|
500
|
+
totalWeightB = totalWeightB.add(new Decimal(element.weight))
|
|
501
|
+
}
|
|
502
|
+
if (element.bin_id > active_id || is_only_amount === 'a') {
|
|
503
|
+
let price_per_lamport = BinUtils.getPricePerLamportFromBinId(element.bin_id, bin_step)
|
|
504
|
+
let weighPerPrice = new Decimal(element.weight).div(price_per_lamport)
|
|
505
|
+
totalWeightA = totalWeightA.add(weighPerPrice)
|
|
506
|
+
}
|
|
507
|
+
})
|
|
508
|
+
return { totalWeightA, totalWeightB, activeWeightA, activeWeightB }
|
|
509
|
+
}
|
|
510
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// buildTestAccount
|
|
2
|
+
import type { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
|
|
3
|
+
import { buildTestAccount } from '@cetusprotocol/test-utils'
|
|
4
|
+
import { CetusDlmmSDK } from '../src/sdk'
|
|
5
|
+
import {
|
|
6
|
+
AddLiquidityOption,
|
|
7
|
+
CalculateAddLiquidityAutoFillOption,
|
|
8
|
+
CalculateAddLiquidityOption,
|
|
9
|
+
DlmmPool,
|
|
10
|
+
OpenAndAddLiquidityOption,
|
|
11
|
+
StrategyType,
|
|
12
|
+
} from '../src/types/dlmm'
|
|
13
|
+
import { printTransaction } from '@cetusprotocol/common-sdk'
|
|
14
|
+
|
|
15
|
+
const pool_id = '0x56d2a3270238f1347fa6fb94836da8afd2415c49ab7bb843996fe5c6a807dc5a'
|
|
16
|
+
const position_id = '0x081a3fda4c7df0fc86ff7f69809434ac096911eb7b5e81865cdd1bf8858214fa'
|
|
17
|
+
|
|
18
|
+
describe('dlmm add liquidity bid ask', () => {
|
|
19
|
+
const sdk = CetusDlmmSDK.createSDK({ env: 'testnet', full_rpc_url: 'https://rpc-testnet.suiscan.xyz' })
|
|
20
|
+
let send_key_pair: Ed25519Keypair
|
|
21
|
+
let account: string
|
|
22
|
+
let pool: DlmmPool
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
send_key_pair = buildTestAccount()
|
|
26
|
+
account = send_key_pair.getPublicKey().toSuiAddress()
|
|
27
|
+
sdk.setSenderAddress(account)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('1 bid ask both amounts open and add liquidity', async () => {
|
|
31
|
+
pool = await sdk.Pool.getPool(pool_id)
|
|
32
|
+
console.log('🚀 ~ beforeEach ~ pool:', pool)
|
|
33
|
+
const { active_id, bin_step } = pool
|
|
34
|
+
const amount_a = '1000000'
|
|
35
|
+
const amount_b = '1200000'
|
|
36
|
+
const lower_bin_id = -10
|
|
37
|
+
const upper_bin_id = 10
|
|
38
|
+
|
|
39
|
+
const amounts_in_active_bin = await sdk.Position.getActiveBinIfInRange(
|
|
40
|
+
pool.bin_manager.bin_manager_handle,
|
|
41
|
+
lower_bin_id,
|
|
42
|
+
upper_bin_id,
|
|
43
|
+
active_id,
|
|
44
|
+
bin_step
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const calculateOption: CalculateAddLiquidityOption = {
|
|
48
|
+
amount_a,
|
|
49
|
+
amount_b,
|
|
50
|
+
active_id,
|
|
51
|
+
bin_step,
|
|
52
|
+
lower_bin_id,
|
|
53
|
+
upper_bin_id,
|
|
54
|
+
amount_a_in_active_bin: amounts_in_active_bin?.amount_a || '0',
|
|
55
|
+
amount_b_in_active_bin: amounts_in_active_bin?.amount_b || '0',
|
|
56
|
+
strategy_type: StrategyType.BidAsk,
|
|
57
|
+
}
|
|
58
|
+
const bin_infos = sdk.Position.calculateAddLiquidityInfo(calculateOption)
|
|
59
|
+
console.log('🚀 ~ test ~ bin_infos:', bin_infos)
|
|
60
|
+
|
|
61
|
+
const addOption: OpenAndAddLiquidityOption = {
|
|
62
|
+
pool_id,
|
|
63
|
+
bin_infos: bin_infos,
|
|
64
|
+
coin_type_a: pool.coin_type_a,
|
|
65
|
+
coin_type_b: pool.coin_type_b,
|
|
66
|
+
lower_bin_id,
|
|
67
|
+
upper_bin_id,
|
|
68
|
+
active_id,
|
|
69
|
+
}
|
|
70
|
+
const tx = sdk.Position.addLiquidityPayload(addOption)
|
|
71
|
+
|
|
72
|
+
printTransaction(tx)
|
|
73
|
+
|
|
74
|
+
const res = await sdk.FullClient.executeTx(send_key_pair, tx, false)
|
|
75
|
+
console.log('🚀 ~ test ~ res:', res)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('2 bid ask strategy both amounts add liquidity', async () => {
|
|
79
|
+
pool = await sdk.Pool.getPool(pool_id)
|
|
80
|
+
const { active_id, bin_step, bin_manager, coin_type_a, coin_type_b } = pool
|
|
81
|
+
console.log('🚀 ~ pool:', pool)
|
|
82
|
+
|
|
83
|
+
const position = await sdk.Position.getPosition(position_id)
|
|
84
|
+
const { lower_bin_id, upper_bin_id } = position
|
|
85
|
+
console.log('🚀 ~ position:', position)
|
|
86
|
+
|
|
87
|
+
const amounts_in_active_bin = await sdk.Position.getActiveBinIfInRange(
|
|
88
|
+
bin_manager.bin_manager_handle,
|
|
89
|
+
lower_bin_id,
|
|
90
|
+
upper_bin_id,
|
|
91
|
+
active_id,
|
|
92
|
+
bin_step
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const amount_a = '1000000'
|
|
96
|
+
const amount_b = '1200000'
|
|
97
|
+
|
|
98
|
+
const calculateOption: CalculateAddLiquidityOption = {
|
|
99
|
+
amount_a,
|
|
100
|
+
amount_b,
|
|
101
|
+
active_id,
|
|
102
|
+
bin_step,
|
|
103
|
+
lower_bin_id,
|
|
104
|
+
upper_bin_id,
|
|
105
|
+
amount_a_in_active_bin: amounts_in_active_bin?.amount_a || '0',
|
|
106
|
+
amount_b_in_active_bin: amounts_in_active_bin?.amount_b || '0',
|
|
107
|
+
strategy_type: StrategyType.BidAsk,
|
|
108
|
+
}
|
|
109
|
+
const bin_infos = sdk.Position.calculateAddLiquidityInfo(calculateOption)
|
|
110
|
+
console.log('🚀 ~ test ~ bin_infos:', bin_infos)
|
|
111
|
+
|
|
112
|
+
const addOption: AddLiquidityOption = {
|
|
113
|
+
pool_id,
|
|
114
|
+
bin_infos: bin_infos,
|
|
115
|
+
coin_type_a,
|
|
116
|
+
coin_type_b,
|
|
117
|
+
active_id,
|
|
118
|
+
position_id,
|
|
119
|
+
collect_fee: true,
|
|
120
|
+
reward_coins: [],
|
|
121
|
+
}
|
|
122
|
+
const tx = sdk.Position.addLiquidityPayload(addOption)
|
|
123
|
+
|
|
124
|
+
printTransaction(tx)
|
|
125
|
+
|
|
126
|
+
const res = await sdk.FullClient.executeTx(send_key_pair, tx, false)
|
|
127
|
+
console.log('🚀 ~ test ~ res:', res)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('3 bid ask strategy fix coin a add liquidity', async () => {
|
|
131
|
+
pool = await sdk.Pool.getPool(pool_id)
|
|
132
|
+
const { active_id, bin_step, bin_manager, coin_type_a, coin_type_b } = pool
|
|
133
|
+
console.log('🚀 ~ pool:', pool)
|
|
134
|
+
|
|
135
|
+
const position = await sdk.Position.getPosition(position_id)
|
|
136
|
+
const { lower_bin_id, upper_bin_id } = position
|
|
137
|
+
console.log('🚀 ~ position:', position)
|
|
138
|
+
|
|
139
|
+
const amounts_in_active_bin = await sdk.Position.getActiveBinIfInRange(
|
|
140
|
+
bin_manager.bin_manager_handle,
|
|
141
|
+
lower_bin_id,
|
|
142
|
+
upper_bin_id,
|
|
143
|
+
active_id,
|
|
144
|
+
bin_step
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
const coin_amount = '2000000'
|
|
148
|
+
|
|
149
|
+
const calculateOption: CalculateAddLiquidityAutoFillOption = {
|
|
150
|
+
coin_amount,
|
|
151
|
+
fix_amount_a: true,
|
|
152
|
+
active_id,
|
|
153
|
+
bin_step,
|
|
154
|
+
lower_bin_id,
|
|
155
|
+
upper_bin_id,
|
|
156
|
+
amount_a_in_active_bin: amounts_in_active_bin?.amount_a || '0',
|
|
157
|
+
amount_b_in_active_bin: amounts_in_active_bin?.amount_b || '0',
|
|
158
|
+
strategy_type: StrategyType.BidAsk,
|
|
159
|
+
}
|
|
160
|
+
const bin_infos = sdk.Position.calculateAddLiquidityInfo(calculateOption)
|
|
161
|
+
console.log('🚀 ~ test ~ bin_infos:', bin_infos)
|
|
162
|
+
|
|
163
|
+
const addOption: AddLiquidityOption = {
|
|
164
|
+
pool_id,
|
|
165
|
+
bin_infos: bin_infos,
|
|
166
|
+
coin_type_a,
|
|
167
|
+
coin_type_b,
|
|
168
|
+
active_id,
|
|
169
|
+
position_id,
|
|
170
|
+
collect_fee: true,
|
|
171
|
+
reward_coins: [],
|
|
172
|
+
}
|
|
173
|
+
const tx = sdk.Position.addLiquidityPayload(addOption)
|
|
174
|
+
|
|
175
|
+
printTransaction(tx)
|
|
176
|
+
|
|
177
|
+
const res = await sdk.FullClient.executeTx(send_key_pair, tx, false)
|
|
178
|
+
console.log('🚀 ~ test ~ res:', res)
|
|
179
|
+
})
|
|
180
|
+
})
|