@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.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +10423 -0
  2. package/README.md +646 -0
  3. package/dist/index.d.mts +1015 -0
  4. package/dist/index.d.ts +1015 -0
  5. package/dist/index.js +13 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +13 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +35 -0
  10. package/src/config/index.ts +2 -0
  11. package/src/config/mainnet.ts +25 -0
  12. package/src/config/testnet.ts +30 -0
  13. package/src/errors/errors.ts +40 -0
  14. package/src/index.ts +8 -0
  15. package/src/modules/configModule.ts +184 -0
  16. package/src/modules/index.ts +1 -0
  17. package/src/modules/partnerModule.ts +302 -0
  18. package/src/modules/poolModule.ts +578 -0
  19. package/src/modules/positionModule.ts +888 -0
  20. package/src/modules/rewardModule.ts +175 -0
  21. package/src/modules/swapModule.ts +129 -0
  22. package/src/sdk.ts +88 -0
  23. package/src/types/constants.ts +23 -0
  24. package/src/types/dlmm.ts +445 -0
  25. package/src/types/index.ts +2 -0
  26. package/src/utils/binUtils.ts +552 -0
  27. package/src/utils/feeUtils.ts +92 -0
  28. package/src/utils/index.ts +5 -0
  29. package/src/utils/parseData.ts +519 -0
  30. package/src/utils/strategyUtils.ts +121 -0
  31. package/src/utils/weightUtils.ts +510 -0
  32. package/tests/add_liquidity_bidask.test.ts +180 -0
  33. package/tests/add_liquidity_curve.test.ts +244 -0
  34. package/tests/add_liquidity_spot.test.ts +262 -0
  35. package/tests/bin.test.ts +80 -0
  36. package/tests/config.test.ts +51 -0
  37. package/tests/partner.test.ts +74 -0
  38. package/tests/pool.test.ts +174 -0
  39. package/tests/position.test.ts +76 -0
  40. package/tests/remove_liquidity.test.ts +137 -0
  41. package/tests/swap.test.ts +96 -0
  42. package/tests/tsconfig.json +26 -0
  43. package/tsconfig.json +5 -0
  44. package/tsup.config.ts +9 -0
@@ -0,0 +1,888 @@
1
+ import { asUintN, CLOCK_ADDRESS, CoinAssist, d, DETAILS_KEYS, getPackagerConfigs, IModule } from '@cetusprotocol/common-sdk'
2
+ import { Transaction, TransactionObjectArgument, TransactionResult } from '@mysten/sui/transactions'
3
+ import { normalizeSuiAddress } from '@mysten/sui/utils'
4
+ import { DlmmErrorCode, handleError } from '../errors/errors'
5
+ import { CetusDlmmSDK } from '../sdk'
6
+ import { BASIS_POINT, MAX_BIN_PER_POSITION } from '../types/constants'
7
+ import {
8
+ AddLiquidityOption,
9
+ BinAmount,
10
+ BinLiquidityInfo,
11
+ CalculateAddLiquidityAutoFillOption,
12
+ CalculateAddLiquidityOption,
13
+ CalculateRemoveLiquidityBothOption,
14
+ CalculateRemoveLiquidityOnlyOption,
15
+ ClosePositionOption,
16
+ CollectFeeOption,
17
+ CollectRewardAndFeeOption,
18
+ CollectRewardOption,
19
+ DlmmPosition,
20
+ OpenAndAddLiquidityOption,
21
+ OpenAndAddLiquidityWithPriceOption,
22
+ PositionFee,
23
+ PositionReward,
24
+ RemoveLiquidityOption,
25
+ StrategyType,
26
+ UpdatePositionFeeAndRewardsOption,
27
+ ValidateActiveIdSlippageOption,
28
+ } from '../types/dlmm'
29
+ import {
30
+ BinUtils,
31
+ getRouterModule,
32
+ parsedDlmmPosFeeData,
33
+ parsedDlmmPosRewardData,
34
+ parseDlmmPosition,
35
+ parseStrategyType,
36
+ StrategyUtils,
37
+ } from '../utils'
38
+ import Decimal from 'decimal.js'
39
+
40
+ export class PositionModule implements IModule<CetusDlmmSDK> {
41
+ protected _sdk: CetusDlmmSDK
42
+
43
+ constructor(sdk: CetusDlmmSDK) {
44
+ this._sdk = sdk
45
+ }
46
+
47
+ get sdk() {
48
+ return this._sdk
49
+ }
50
+
51
+ buildPositionType(): string {
52
+ const package_id = this._sdk.sdkOptions.dlmm_pool.package_id
53
+ return `${package_id}::position::Position`
54
+ }
55
+
56
+ async getOwnerPositionList(owner: string): Promise<DlmmPosition[]> {
57
+ const list: DlmmPosition[] = []
58
+ try {
59
+ const res = await this._sdk.FullClient.getOwnedObjectsByPage(owner, {
60
+ options: { showType: true, showContent: true, showOwner: true },
61
+ filter: {
62
+ StructType: this.buildPositionType(),
63
+ },
64
+ })
65
+
66
+ res.data.forEach((obj) => {
67
+ list.push(parseDlmmPosition(obj))
68
+ })
69
+ } catch (error) {
70
+ console.log('🚀 ~ PositionModule ~ getOwnerPositionList ~ error:', error)
71
+ handleError(DlmmErrorCode.GetObjectError, error as Error, {
72
+ [DETAILS_KEYS.METHOD_NAME]: 'getOwnerPositionList',
73
+ [DETAILS_KEYS.REQUEST_PARAMS]: owner,
74
+ })
75
+ }
76
+
77
+ return list
78
+ }
79
+
80
+ async getPosition(position_id: string): Promise<DlmmPosition> {
81
+ try {
82
+ const res = await this._sdk.FullClient.getObject({ id: position_id, options: { showType: true, showContent: true, showOwner: true } })
83
+ return parseDlmmPosition(res)
84
+ } catch (error) {
85
+ console.log('🚀 ~ PositionModule ~ getPosition ~ error:', error)
86
+ return handleError(DlmmErrorCode.GetObjectError, error as Error, {
87
+ [DETAILS_KEYS.METHOD_NAME]: 'getPosition',
88
+ [DETAILS_KEYS.REQUEST_PARAMS]: position_id,
89
+ })
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Collect fee
95
+ * @param option - The option for collecting fee
96
+ * @param tx - The transaction object
97
+ * @returns The transaction object
98
+ */
99
+ collectFeePayload(option: CollectFeeOption, tx?: Transaction): Transaction {
100
+ const { pool_id, position_id, coin_type_a, coin_type_b } = option
101
+ const { dlmm_pool } = this.sdk.sdkOptions
102
+ const { versioned_id, global_config_id } = getPackagerConfigs(dlmm_pool)
103
+
104
+ tx = tx || new Transaction()
105
+
106
+ const [fee_a_balance, fee_b_balance] = tx.moveCall({
107
+ target: `${dlmm_pool.published_at}::pool::collect_position_fee`,
108
+ arguments: [tx.object(pool_id), tx.object(position_id), tx.object(global_config_id), tx.object(versioned_id)],
109
+ typeArguments: [coin_type_a, coin_type_b],
110
+ })
111
+
112
+ const fee_a_obj = CoinAssist.fromBalance(fee_a_balance, coin_type_a, tx)
113
+ const fee_b_obj = CoinAssist.fromBalance(fee_b_balance, coin_type_b, tx)
114
+ tx.transferObjects([fee_a_obj, fee_b_obj], this.sdk.getSenderAddress())
115
+
116
+ return tx
117
+ }
118
+
119
+ /**
120
+ * Update the fee and rewards of the position
121
+ * @param option - The option for updating the fee and rewards of the position
122
+ * @param tx - The transaction object
123
+ * @returns The transaction object
124
+ */
125
+ updatePositionFeeAndRewards(option: UpdatePositionFeeAndRewardsOption, tx: Transaction) {
126
+ const { dlmm_pool } = this.sdk.sdkOptions
127
+ const { versioned_id, global_config_id } = getPackagerConfigs(dlmm_pool)
128
+ const { pool_id, position_id, coin_type_a, coin_type_b } = option
129
+ tx.moveCall({
130
+ target: `${dlmm_pool.published_at}::pool::update_position_fee_and_rewards`,
131
+ arguments: [
132
+ tx.object(pool_id),
133
+ tx.pure.id(position_id),
134
+ tx.object(global_config_id),
135
+ tx.object(versioned_id),
136
+ tx.object(CLOCK_ADDRESS),
137
+ ],
138
+ typeArguments: [coin_type_a, coin_type_b],
139
+ })
140
+
141
+ return tx
142
+ }
143
+
144
+ /**
145
+ * Collect reward
146
+ * @param options - The option for collecting reward
147
+ * @param tx - The transaction object
148
+ * @returns The transaction object
149
+ */
150
+ collectRewardPayload(options: CollectRewardOption[], tx?: Transaction): Transaction {
151
+ const { dlmm_pool } = this.sdk.sdkOptions
152
+ const { versioned_id, global_config_id } = getPackagerConfigs(dlmm_pool)
153
+
154
+ tx = tx || new Transaction()
155
+
156
+ options.forEach((option) => {
157
+ const { pool_id, position_id, reward_coins, coin_type_a, coin_type_b } = option
158
+
159
+ reward_coins.forEach((reward_coin) => {
160
+ const reward_coin_balance = tx.moveCall({
161
+ target: `${dlmm_pool.published_at}::pool::collect_position_reward`,
162
+ arguments: [tx.object(pool_id), tx.object(position_id), tx.object(global_config_id), tx.object(versioned_id)],
163
+ typeArguments: [coin_type_a, coin_type_b, reward_coin],
164
+ })
165
+
166
+ const reward_coin_obj = CoinAssist.fromBalance(reward_coin_balance, reward_coin, tx)
167
+ tx.transferObjects([reward_coin_obj], this.sdk.getSenderAddress())
168
+ })
169
+ })
170
+
171
+ return tx
172
+ }
173
+
174
+ collectRewardAndFeePayload(options: CollectRewardAndFeeOption[], tx?: Transaction): Transaction {
175
+ tx = tx || new Transaction()
176
+
177
+ options.forEach((option) => {
178
+ const { pool_id, position_id, reward_coins, coin_type_a, coin_type_b } = option
179
+
180
+ this.updatePositionFeeAndRewards({ pool_id, position_id, coin_type_a, coin_type_b }, tx)
181
+
182
+ this.collectFeePayload({ pool_id, position_id, coin_type_a, coin_type_b }, tx)
183
+ this.collectRewardPayload([{ pool_id, position_id, reward_coins, coin_type_a, coin_type_b }], tx)
184
+ })
185
+
186
+ return tx
187
+ }
188
+
189
+ /**
190
+ * Validate the active id slippage
191
+ * @param options - The option for validating the active id slippage
192
+ * @param tx - The transaction object
193
+ * @returns The transaction object
194
+ */
195
+ validateActiveIdSlippage(options: ValidateActiveIdSlippageOption, tx: Transaction): Transaction {
196
+ const { pool_id, active_id, max_price_slippage, bin_step, coin_type_a, coin_type_b } = options
197
+ const { dlmm_router } = this.sdk.sdkOptions
198
+
199
+ const bin_shift = BinUtils.getBinShift(active_id, bin_step, max_price_slippage)
200
+ const active_id_u32 = Number(asUintN(BigInt(active_id)))
201
+
202
+ tx.moveCall({
203
+ target: `${dlmm_router.published_at}::utils::validate_active_id_slippage`,
204
+ arguments: [
205
+ typeof pool_id === 'string' ? tx.object(pool_id) : pool_id,
206
+ tx.pure.u32(Number(active_id_u32)),
207
+ tx.pure.u32(Number(bin_shift)),
208
+ ],
209
+ typeArguments: [coin_type_a, coin_type_b],
210
+ })
211
+
212
+ return tx
213
+ }
214
+
215
+ /**
216
+ * Close a position
217
+ * @param option - The option for closing a position
218
+ * @returns The transaction object
219
+ */
220
+ closePositionPayload(option: ClosePositionOption, tx?: Transaction): Transaction {
221
+ const { pool_id, position_id, reward_coins, coin_type_a, coin_type_b } = option
222
+ const { dlmm_pool } = this.sdk.sdkOptions
223
+ const { versioned_id, global_config_id } = getPackagerConfigs(dlmm_pool)
224
+
225
+ tx = tx || new Transaction()
226
+
227
+ this.updatePositionFeeAndRewards({ pool_id, position_id, coin_type_a, coin_type_b }, tx)
228
+
229
+ this.collectRewardPayload([{ pool_id, position_id, reward_coins, coin_type_a, coin_type_b }], tx)
230
+
231
+ const [close_position_cert, coin_a_balance, coin_b_balance] = tx.moveCall({
232
+ target: `${dlmm_pool.published_at}::pool::close_position`,
233
+ arguments: [
234
+ tx.object(pool_id),
235
+ tx.object(position_id),
236
+ tx.object(global_config_id),
237
+ tx.object(versioned_id),
238
+ tx.object(CLOCK_ADDRESS),
239
+ ],
240
+ typeArguments: [coin_type_a, coin_type_b],
241
+ })
242
+
243
+ const coin_a_obj = CoinAssist.fromBalance(coin_a_balance, coin_type_a, tx)
244
+ const coin_b_obj = CoinAssist.fromBalance(coin_b_balance, coin_type_b, tx)
245
+
246
+ tx.moveCall({
247
+ target: `${dlmm_pool.published_at}::pool::destroy_close_position_cert`,
248
+ arguments: [close_position_cert, tx.object(versioned_id)],
249
+ typeArguments: [],
250
+ })
251
+
252
+ tx.transferObjects([coin_a_obj, coin_b_obj], this.sdk.getSenderAddress())
253
+
254
+ return tx
255
+ }
256
+
257
+ /**
258
+ * Get the amounts in the active bin if in range
259
+ * @param bin_manager_handle - The bin manager handle
260
+ * @param lower_bin_id - The lower bin id
261
+ * @param upper_bin_id - The upper bin id
262
+ * @param active_id - The active id
263
+ * @returns The amounts in the active bin if in range
264
+ */
265
+ async getActiveBinIfInRange(
266
+ bin_manager_handle: string,
267
+ lower_bin_id: number,
268
+ upper_bin_id: number,
269
+ active_id: number,
270
+ bin_step: number,
271
+ force_refresh = false
272
+ ): Promise<BinAmount | undefined> {
273
+ if (active_id <= upper_bin_id && active_id >= lower_bin_id) {
274
+ const bin_info = await this._sdk.Pool.getBinInfo(bin_manager_handle, active_id, bin_step, force_refresh)
275
+ return bin_info
276
+ }
277
+ return undefined
278
+ }
279
+
280
+ /**
281
+ * Calculate the result of removing liquidity
282
+ * @param option - The option for calculating the result of removing liquidity
283
+ * @returns The result of removing liquidity
284
+ */
285
+ calculateRemoveLiquidityInfo(option: CalculateRemoveLiquidityBothOption | CalculateRemoveLiquidityOnlyOption): BinLiquidityInfo {
286
+ const { bins, active_id, coin_amount } = option
287
+ const isBothSide = 'fix_amount_a' in option
288
+
289
+ const bins_b = bins.filter((bin) => bin.bin_id < active_id)
290
+ const bins_a = bins.filter((bin) => bin.bin_id > active_id)
291
+
292
+ let total_amount = d(0)
293
+ let amount_rate = d(0)
294
+ let used_bins: BinAmount[] = []
295
+
296
+ if (isBothSide) {
297
+ used_bins = [...bins]
298
+ const { fix_amount_a } = option
299
+ const active_bin = bins.find((bin) => bin.bin_id === active_id)
300
+ const total_amount_a = bins_a.reduce((acc, bin) => d(acc).plus(bin.amount_a), d(0)).add(active_bin?.amount_a || '0')
301
+ const total_amount_b = bins_b.reduce((acc, bin) => d(acc).plus(bin.amount_b), d(0)).add(active_bin?.amount_b || '0')
302
+ total_amount = fix_amount_a ? total_amount_a : total_amount_b
303
+
304
+ amount_rate = d(coin_amount).gte(total_amount) ? d(1) : d(coin_amount).div(total_amount)
305
+ } else {
306
+ const { is_only_a } = option
307
+ used_bins = is_only_a ? bins_a : bins_b
308
+ total_amount = used_bins.reduce((acc, bin) => d(acc).plus(is_only_a ? bin.amount_a : bin.amount_b), d(0))
309
+
310
+ amount_rate = d(coin_amount).gte(total_amount) ? d(1) : d(coin_amount).div(total_amount)
311
+ }
312
+
313
+ if (d(total_amount).isZero()) {
314
+ return handleError(DlmmErrorCode.InsufficientLiquidity, new Error('Insufficient liquidity'), {
315
+ [DETAILS_KEYS.METHOD_NAME]: 'calculateRemoveLiquidityResult',
316
+ [DETAILS_KEYS.REQUEST_PARAMS]: option,
317
+ })
318
+ }
319
+
320
+ const info = BinUtils.processBinsByRate([...used_bins], amount_rate.toFixed())
321
+ // If the proportional allocation results in an invalid amount, iterate through the bins and remove them one by one
322
+ if (info.has_invalid_amount) {
323
+ let remaining_amount = d(coin_amount)
324
+ const processed_bins: BinAmount[] = []
325
+ const is_amount_a = isBothSide ? option.fix_amount_a : option.is_only_a
326
+ for (const bin of used_bins) {
327
+ if (remaining_amount.lte(0)) break
328
+
329
+ const bin_amount = is_amount_a ? bin.amount_a : bin.amount_b
330
+ const bin_amount_other = is_amount_a ? bin.amount_b : bin.amount_a
331
+
332
+ let rate = d(d(remaining_amount).div(bin_amount).toFixed(9, Decimal.ROUND_UP))
333
+ rate = rate.gt(1) ? d(1) : rate
334
+ let amount_to_remove = d(bin_amount).mul(rate)
335
+ remaining_amount = remaining_amount.minus(amount_to_remove)
336
+
337
+ const amount_to_remove_other = d(bin_amount_other).mul(rate)
338
+ const liquidity_to_remove = d(bin.liquidity!).mul(rate)
339
+
340
+ processed_bins.push({
341
+ ...bin,
342
+ amount_a: is_amount_a ? amount_to_remove.toFixed(0) : amount_to_remove_other.toFixed(0),
343
+ amount_b: is_amount_a ? amount_to_remove_other.toFixed(0) : amount_to_remove.toFixed(0),
344
+ liquidity: liquidity_to_remove.toFixed(0),
345
+ })
346
+ }
347
+
348
+ return {
349
+ bins: processed_bins,
350
+ amount_a: processed_bins.reduce((acc, bin) => d(acc).plus(bin.amount_a), d(0)).toFixed(0),
351
+ amount_b: processed_bins.reduce((acc, bin) => d(acc).plus(bin.amount_b), d(0)).toFixed(0),
352
+ }
353
+ }
354
+
355
+ return info.bins
356
+ }
357
+
358
+ /**
359
+ * Calculate the result of adding liquidity
360
+ * @param option - The option for calculating the result of adding liquidity
361
+ * @returns The result of adding liquidity
362
+ */
363
+ calculateAddLiquidityInfo(option: CalculateAddLiquidityOption | CalculateAddLiquidityAutoFillOption): BinLiquidityInfo {
364
+ const isAutoFill = 'fix_amount_a' in option
365
+ const { active_id, bin_step, lower_bin_id, upper_bin_id, amount_a_in_active_bin, amount_b_in_active_bin, strategy_type } = option
366
+
367
+ if (isAutoFill) {
368
+ const { coin_amount, fix_amount_a } = option
369
+ return StrategyUtils.autoFillCoinByStrategy(
370
+ active_id,
371
+ bin_step,
372
+ coin_amount,
373
+ fix_amount_a,
374
+ amount_a_in_active_bin,
375
+ amount_b_in_active_bin,
376
+ lower_bin_id,
377
+ upper_bin_id,
378
+ strategy_type
379
+ )
380
+ } else {
381
+ return StrategyUtils.toAmountsBothSideByStrategy(
382
+ active_id,
383
+ bin_step,
384
+ lower_bin_id,
385
+ upper_bin_id,
386
+ option.amount_a,
387
+ option.amount_b,
388
+ amount_a_in_active_bin,
389
+ amount_b_in_active_bin,
390
+ strategy_type
391
+ )
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Remove liquidity
397
+ * @param option - The option for removing liquidity
398
+ * @returns The transaction
399
+ */
400
+ removeLiquidityPayload(option: RemoveLiquidityOption): Transaction {
401
+ const {
402
+ pool_id,
403
+ position_id,
404
+ bin_infos,
405
+ reward_coins,
406
+ slippage,
407
+ coin_type_a,
408
+ coin_type_b,
409
+ active_id,
410
+ collect_fee,
411
+ bin_step,
412
+ remove_percent,
413
+ } = option
414
+ const { dlmm_pool } = this.sdk.sdkOptions
415
+ const { bins } = bin_infos
416
+ const tx = new Transaction()
417
+
418
+ if (collect_fee || reward_coins.length > 0) {
419
+ this.updatePositionFeeAndRewards({ pool_id, position_id, coin_type_a, coin_type_b }, tx)
420
+ }
421
+ if (collect_fee) {
422
+ this.collectFeePayload({ pool_id, position_id, coin_type_a, coin_type_b }, tx)
423
+ }
424
+
425
+ this.collectRewardPayload([{ pool_id, position_id, reward_coins, coin_type_a, coin_type_b }], tx)
426
+
427
+ const { versioned_id, global_config_id } = getPackagerConfigs(dlmm_pool)
428
+
429
+ if (remove_percent) {
430
+ const min_bin_id_u32 = asUintN(BigInt(bins[0].bin_id))
431
+ const max_bin_id_u32 = asUintN(BigInt(bins[bins.length - 1].bin_id))
432
+ const remove_percent_fixed = Number(d(remove_percent).mul(BASIS_POINT).toFixed(0))
433
+ const [coin_a_balance, coin_b_balance] = tx.moveCall({
434
+ target: `${dlmm_pool.published_at}::pool::remove_liquidity_by_percent`,
435
+ arguments: [
436
+ tx.object(pool_id),
437
+ tx.object(position_id),
438
+ tx.pure.u32(Number(min_bin_id_u32)),
439
+ tx.pure.u32(Number(max_bin_id_u32)),
440
+ tx.pure.u16(remove_percent_fixed),
441
+ tx.object(global_config_id),
442
+ tx.object(versioned_id),
443
+ tx.object(CLOCK_ADDRESS),
444
+ ],
445
+ typeArguments: [coin_type_a, coin_type_b],
446
+ })
447
+ const coin_a_obj = CoinAssist.fromBalance(coin_a_balance, coin_type_a, tx)
448
+ const coin_b_obj = CoinAssist.fromBalance(coin_b_balance, coin_type_b, tx)
449
+ tx.transferObjects([coin_a_obj, coin_b_obj], this.sdk.getSenderAddress())
450
+ } else {
451
+ const bin_amounts = tx.pure.vector(
452
+ 'u32',
453
+ bins.map((bin) => Number(asUintN(BigInt(bin.bin_id))))
454
+ )
455
+ const remove_liquiditys = tx.pure.vector(
456
+ 'u128',
457
+ bins.map((bin) => bin.liquidity!)
458
+ )
459
+
460
+ const [coin_a_balance, coin_b_balance] = tx.moveCall({
461
+ target: `${dlmm_pool.published_at}::pool::remove_liquidity`,
462
+ arguments: [
463
+ tx.object(pool_id),
464
+ tx.object(position_id),
465
+ bin_amounts,
466
+ remove_liquiditys,
467
+ tx.object(global_config_id),
468
+ tx.object(versioned_id),
469
+ tx.object(CLOCK_ADDRESS),
470
+ ],
471
+ typeArguments: [coin_type_a, coin_type_b],
472
+ })
473
+ const coin_a_obj = CoinAssist.fromBalance(coin_a_balance, coin_type_a, tx)
474
+ const coin_b_obj = CoinAssist.fromBalance(coin_b_balance, coin_type_b, tx)
475
+ tx.transferObjects([coin_a_obj, coin_b_obj], this.sdk.getSenderAddress())
476
+ }
477
+
478
+ // validate the active id slippage
479
+ this.validateActiveIdSlippage({ pool_id, active_id, max_price_slippage: slippage, bin_step, coin_type_a, coin_type_b }, tx)
480
+
481
+ return tx
482
+ }
483
+
484
+ /**
485
+ * Add liquidity with price
486
+ * @param option - The option for adding liquidity with price
487
+ * @returns The transaction
488
+ */
489
+ addLiquidityWithPricePayload(option: OpenAndAddLiquidityWithPriceOption): Transaction {
490
+ const {
491
+ pool_id,
492
+ bin_infos,
493
+ coin_type_a,
494
+ coin_type_b,
495
+ price_base_coin,
496
+ price,
497
+ lower_price,
498
+ upper_price,
499
+ bin_step,
500
+ strategy_type,
501
+ amount_a_in_active_bin,
502
+ amount_b_in_active_bin,
503
+ decimals_a,
504
+ decimals_b,
505
+ use_bin_infos,
506
+ max_price_slippage,
507
+ } = option
508
+ let lower_bin_id
509
+ let upper_bin_id
510
+ let active_id
511
+ let new_bin_infos: BinLiquidityInfo = bin_infos
512
+
513
+ const is_coin_a_base = price_base_coin === 'coin_a'
514
+
515
+ if (is_coin_a_base) {
516
+ lower_bin_id = BinUtils.getBinIdFromPrice(lower_price, bin_step, false, decimals_a, decimals_b)
517
+ upper_bin_id = BinUtils.getBinIdFromPrice(upper_price, bin_step, true, decimals_a, decimals_b)
518
+ active_id = BinUtils.getBinIdFromPrice(price, bin_step, true, decimals_a, decimals_b)
519
+ } else {
520
+ lower_bin_id = BinUtils.getBinIdFromPrice(d(1).div(upper_price).toString(), bin_step, false, decimals_a, decimals_b)
521
+ upper_bin_id = BinUtils.getBinIdFromPrice(d(1).div(lower_price).toString(), bin_step, true, decimals_a, decimals_b)
522
+ active_id = BinUtils.getBinIdFromPrice(d(1).div(price).toString(), bin_step, false, decimals_a, decimals_b)
523
+
524
+ const calculateOption: CalculateAddLiquidityOption = {
525
+ amount_a: bin_infos.amount_b,
526
+ amount_b: bin_infos.amount_a,
527
+ active_id,
528
+ bin_step,
529
+ lower_bin_id,
530
+ upper_bin_id,
531
+ amount_a_in_active_bin,
532
+ amount_b_in_active_bin,
533
+ strategy_type: strategy_type,
534
+ }
535
+
536
+ new_bin_infos = this.sdk.Position.calculateAddLiquidityInfo(calculateOption)
537
+ }
538
+
539
+ const openAndAddLiquidityOption: OpenAndAddLiquidityOption = {
540
+ pool_id,
541
+ active_id,
542
+ bin_infos: new_bin_infos,
543
+ coin_type_a,
544
+ coin_type_b,
545
+ lower_bin_id,
546
+ upper_bin_id,
547
+ strategy_type,
548
+ use_bin_infos,
549
+ max_price_slippage,
550
+ bin_step,
551
+ }
552
+
553
+ return this.addLiquidityPayload(openAndAddLiquidityOption)
554
+ }
555
+
556
+ /**
557
+ * Add liquidity
558
+ * @param option - The option for adding liquidity
559
+ * @returns The transaction object
560
+ */
561
+ addLiquidityPayload(option: AddLiquidityOption | OpenAndAddLiquidityOption, tx?: Transaction): Transaction {
562
+ const {
563
+ pool_id,
564
+ bin_infos,
565
+ coin_type_a,
566
+ coin_type_b,
567
+ active_id,
568
+ strategy_type,
569
+ max_price_slippage,
570
+ bin_step,
571
+ use_bin_infos = false,
572
+ } = option
573
+ tx = tx || new Transaction()
574
+
575
+ const isOpenPosition = 'lower_bin_id' in option
576
+
577
+ const liquidity_bins: BinLiquidityInfo[] = []
578
+
579
+ if (isOpenPosition) {
580
+ const position_bins = BinUtils.splitBinLiquidityInfo(bin_infos, option.lower_bin_id, option.upper_bin_id)
581
+ liquidity_bins.push(...position_bins)
582
+ } else {
583
+ const position_id = option.position_id
584
+ liquidity_bins.push(bin_infos)
585
+
586
+ if (option.collect_fee || option.reward_coins.length > 0) {
587
+ this.updatePositionFeeAndRewards({ pool_id: pool_id as string, position_id, coin_type_a, coin_type_b }, tx)
588
+ }
589
+
590
+ if (option.collect_fee) {
591
+ this.collectFeePayload({ pool_id: pool_id as string, position_id, coin_type_a, coin_type_b }, tx)
592
+ }
593
+
594
+ if (option.reward_coins.length > 0) {
595
+ this.collectRewardPayload(
596
+ [{ pool_id: pool_id as string, position_id, reward_coins: option.reward_coins, coin_type_a, coin_type_b }],
597
+ tx
598
+ )
599
+ }
600
+ }
601
+
602
+ liquidity_bins.forEach((liquidity_bin, index) => {
603
+ console.log('🚀 ~ PositionModule ~ addLiquidityPayload ~ liquidity_bin:', index, liquidity_bin)
604
+ const { amount_a, amount_b, bins } = liquidity_bin
605
+
606
+ const coin_a_obj_id = CoinAssist.buildCoinWithBalance(BigInt(amount_a), coin_type_a, tx)
607
+ const coin_b_obj_id = CoinAssist.buildCoinWithBalance(BigInt(amount_b), coin_type_b, tx)
608
+
609
+ if (use_bin_infos) {
610
+ this.addLiquidityInternal({
611
+ pool_id,
612
+ coin_type_a,
613
+ coin_type_b,
614
+ active_id,
615
+ liquidity_bin,
616
+ tx,
617
+ coin_a_obj_id,
618
+ coin_b_obj_id,
619
+ position_id: isOpenPosition ? undefined : option.position_id,
620
+ max_price_slippage,
621
+ bin_step,
622
+ })
623
+ } else {
624
+ this.addLiquidityStrategyInternal({
625
+ pool_id,
626
+ coin_type_a,
627
+ coin_type_b,
628
+ active_id,
629
+ liquidity_bin,
630
+ tx,
631
+ max_price_slippage,
632
+ bin_step,
633
+ coin_a_obj_id,
634
+ coin_b_obj_id,
635
+ strategy_type,
636
+ position_id: isOpenPosition ? undefined : option.position_id,
637
+ })
638
+ }
639
+ })
640
+
641
+ return tx
642
+ }
643
+
644
+ private addLiquidityStrategyInternal(option: {
645
+ pool_id: string | TransactionObjectArgument
646
+ coin_type_a: string
647
+ coin_type_b: string
648
+ active_id: number
649
+ liquidity_bin: BinLiquidityInfo
650
+ tx: Transaction
651
+ coin_a_obj_id: TransactionObjectArgument
652
+ coin_b_obj_id: TransactionObjectArgument
653
+ position_id?: string
654
+ strategy_type: StrategyType
655
+ max_price_slippage: number
656
+ bin_step: number
657
+ }): Transaction {
658
+ const {
659
+ max_price_slippage,
660
+ bin_step,
661
+ position_id,
662
+ pool_id,
663
+ coin_type_a,
664
+ coin_type_b,
665
+ active_id,
666
+ liquidity_bin,
667
+ tx,
668
+ coin_a_obj_id,
669
+ coin_b_obj_id,
670
+ strategy_type,
671
+ } = option
672
+
673
+ const { dlmm_pool, dlmm_router } = this.sdk.sdkOptions
674
+ const { versioned_id, global_config_id } = getPackagerConfigs(dlmm_pool)
675
+
676
+ const { bins, amount_a, amount_b } = liquidity_bin
677
+
678
+ let position: string | undefined | TransactionObjectArgument = position_id
679
+
680
+ const lower_bin_id = bins[0].bin_id
681
+ const upper_bin_id = bins[bins.length - 1].bin_id
682
+ const lower_bin_id_u32 = asUintN(BigInt(lower_bin_id))
683
+
684
+ const active_id_u32 = Number(asUintN(BigInt(active_id)))
685
+ const bin_shift = BinUtils.getBinShift(active_id, bin_step, max_price_slippage)
686
+ const routerModule = getRouterModule(strategy_type)
687
+
688
+ if (position_id === undefined) {
689
+ const width = upper_bin_id - lower_bin_id + 1
690
+ if (width > MAX_BIN_PER_POSITION) {
691
+ handleError(DlmmErrorCode.InvalidBinWidth, new Error('Width is too large'), {
692
+ [DETAILS_KEYS.METHOD_NAME]: 'openPosition',
693
+ })
694
+ }
695
+
696
+ position = tx.moveCall({
697
+ target: `${dlmm_router.published_at}::${routerModule}::open_position`,
698
+ arguments: [
699
+ typeof pool_id === 'string' ? tx.object(pool_id) : pool_id,
700
+ coin_a_obj_id,
701
+ coin_b_obj_id,
702
+ tx.pure.u64(amount_a),
703
+ tx.pure.u64(amount_b),
704
+ tx.pure.u32(Number(lower_bin_id_u32)),
705
+ tx.pure.u16(Number(width)),
706
+ tx.pure.u32(Number(active_id_u32)),
707
+ tx.pure.u32(Number(bin_shift)),
708
+ tx.object(global_config_id),
709
+ tx.object(versioned_id),
710
+ tx.object(CLOCK_ADDRESS),
711
+ ],
712
+ typeArguments: [coin_type_a, coin_type_b],
713
+ })
714
+ } else {
715
+ const valid_bins = bins.filter((bin) => bin.amount_a !== '0' || bin.amount_b !== '0')
716
+ if (valid_bins.length === 0) {
717
+ return handleError(DlmmErrorCode.InvalidParams, new Error('No bins to add liquidity'), {
718
+ [DETAILS_KEYS.METHOD_NAME]: 'addLiquidityStrategyInternal',
719
+ [DETAILS_KEYS.REQUEST_PARAMS]: option,
720
+ })
721
+ }
722
+ const valid_lower_bin_id_u32 = asUintN(BigInt(valid_bins[0].bin_id))
723
+ const valid_upper_bin_id_u32 = asUintN(BigInt(valid_bins[valid_bins.length - 1].bin_id))
724
+
725
+ tx.moveCall({
726
+ target: `${dlmm_router.published_at}::${routerModule}::add_liquidity`,
727
+ arguments: [
728
+ typeof pool_id === 'string' ? tx.object(pool_id) : pool_id,
729
+ tx.object(position_id!),
730
+ coin_a_obj_id,
731
+ coin_b_obj_id,
732
+ tx.pure.u64(amount_a),
733
+ tx.pure.u64(amount_b),
734
+ tx.pure.u32(Number(valid_lower_bin_id_u32)),
735
+ tx.pure.u32(Number(valid_upper_bin_id_u32)),
736
+ tx.pure.u32(Number(active_id_u32)),
737
+ tx.pure.u32(Number(bin_shift)),
738
+ tx.object(global_config_id),
739
+ tx.object(versioned_id),
740
+ tx.object(CLOCK_ADDRESS),
741
+ ],
742
+ typeArguments: [coin_type_a, coin_type_b],
743
+ })
744
+ }
745
+
746
+ if (position) {
747
+ tx.transferObjects([position, coin_a_obj_id, coin_b_obj_id], this.sdk.getSenderAddress())
748
+ } else {
749
+ tx.transferObjects([coin_a_obj_id, coin_b_obj_id], this.sdk.getSenderAddress())
750
+ }
751
+
752
+ return tx
753
+ }
754
+
755
+ private addLiquidityInternal(option: {
756
+ pool_id: string | TransactionObjectArgument
757
+ coin_type_a: string
758
+ coin_type_b: string
759
+ active_id: number
760
+ liquidity_bin: BinLiquidityInfo
761
+ tx: Transaction
762
+ coin_a_obj_id: TransactionObjectArgument
763
+ coin_b_obj_id: TransactionObjectArgument
764
+ position_id?: string
765
+ max_price_slippage: number
766
+ bin_step: number
767
+ }): Transaction {
768
+ const {
769
+ position_id,
770
+ pool_id,
771
+ coin_type_a,
772
+ coin_type_b,
773
+ active_id,
774
+ liquidity_bin,
775
+ tx,
776
+ coin_a_obj_id,
777
+ coin_b_obj_id,
778
+ max_price_slippage,
779
+ bin_step,
780
+ } = option
781
+ const { bins } = liquidity_bin
782
+
783
+ const { dlmm_pool, dlmm_router } = this.sdk.sdkOptions
784
+ const { versioned_id, global_config_id } = getPackagerConfigs(dlmm_pool)
785
+ const amounts_a = tx.pure.vector(
786
+ 'u64',
787
+ bins.map((bin) => bin.amount_a)
788
+ )
789
+ const amounts_b = tx.pure.vector(
790
+ 'u64',
791
+ bins.map((bin) => bin.amount_b)
792
+ )
793
+
794
+ const bin_ids = tx.makeMoveVec({
795
+ elements: bins.map((bin) => tx.pure.u32(Number(asUintN(BigInt(bin.bin_id))))),
796
+ type: 'u32',
797
+ })
798
+ const lower_bin_id = liquidity_bin.bins[0].bin_id
799
+ const upper_bin_id = liquidity_bin.bins[liquidity_bin.bins.length - 1].bin_id
800
+ if (position_id === undefined) {
801
+ const width = upper_bin_id - lower_bin_id + 1
802
+ if (width > MAX_BIN_PER_POSITION) {
803
+ handleError(DlmmErrorCode.InvalidBinWidth, new Error('Width is too large'), {
804
+ [DETAILS_KEYS.METHOD_NAME]: 'openPosition',
805
+ })
806
+ }
807
+ const open_position_id = tx.moveCall({
808
+ target: `${dlmm_router.published_at}::add_liquidity::open_position`,
809
+ arguments: [
810
+ typeof pool_id === 'string' ? tx.object(pool_id) : pool_id,
811
+ coin_a_obj_id,
812
+ coin_b_obj_id,
813
+ bin_ids,
814
+ amounts_a,
815
+ amounts_b,
816
+ tx.object(global_config_id),
817
+ tx.object(versioned_id),
818
+ tx.object(CLOCK_ADDRESS),
819
+ ],
820
+ typeArguments: [coin_type_a, coin_type_b],
821
+ })
822
+ // validate the active id slippage
823
+ if (active_id >= lower_bin_id && active_id <= upper_bin_id) {
824
+ this.validateActiveIdSlippage({ pool_id, active_id, max_price_slippage, bin_step, coin_type_a, coin_type_b }, tx)
825
+ }
826
+ tx.transferObjects([coin_a_obj_id, coin_b_obj_id, open_position_id], this.sdk.getSenderAddress())
827
+ } else {
828
+ tx.moveCall({
829
+ target: `${dlmm_router.published_at}::add_liquidity::add_liquidity`,
830
+ arguments: [
831
+ typeof pool_id === 'string' ? tx.object(pool_id) : pool_id,
832
+ tx.object(position_id!),
833
+ coin_a_obj_id,
834
+ coin_b_obj_id,
835
+ bin_ids,
836
+ amounts_a,
837
+ amounts_b,
838
+ tx.object(global_config_id),
839
+ tx.object(versioned_id),
840
+ tx.object(CLOCK_ADDRESS),
841
+ ],
842
+ typeArguments: [coin_type_a, coin_type_b],
843
+ })
844
+ // validate the active id slippage
845
+ if (active_id >= lower_bin_id && active_id <= upper_bin_id) {
846
+ this.validateActiveIdSlippage({ pool_id, active_id, max_price_slippage, bin_step, coin_type_a, coin_type_b }, tx)
847
+ }
848
+ tx.transferObjects([coin_a_obj_id, coin_b_obj_id], this.sdk.getSenderAddress())
849
+ }
850
+
851
+ return tx
852
+ }
853
+
854
+ /**
855
+ * Fetch the fee and reward of the position
856
+ * @param options - The option for fetching the fee and reward of the position
857
+ * @returns The fee and reward of the position
858
+ */
859
+ async fetchPositionFeeAndReward(
860
+ options: CollectRewardAndFeeOption[]
861
+ ): Promise<{ feeData: Record<string, PositionFee>; rewardData: Record<string, PositionReward> }> {
862
+ const tx = new Transaction()
863
+ this.collectRewardAndFeePayload(options, tx)
864
+
865
+ const simulateRes = await this.sdk.FullClient.devInspectTransactionBlock({
866
+ transactionBlock: tx,
867
+ sender: normalizeSuiAddress('0x0'),
868
+ })
869
+
870
+ if (simulateRes.error != null) {
871
+ return handleError(DlmmErrorCode.FetchError, new Error(simulateRes.error), {
872
+ [DETAILS_KEYS.METHOD_NAME]: 'fetchPositionFeeAndReward',
873
+ [DETAILS_KEYS.REQUEST_PARAMS]: {
874
+ options,
875
+ totalOptions: options.length,
876
+ },
877
+ })
878
+ }
879
+
880
+ const feeData = parsedDlmmPosFeeData(simulateRes)
881
+ const rewardData = parsedDlmmPosRewardData(simulateRes)
882
+
883
+ return {
884
+ feeData,
885
+ rewardData,
886
+ }
887
+ }
888
+ }