@cetusprotocol/dlmm-sdk 0.0.0-experimental-20250925173459

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