@cetusprotocol/sui-clmm-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.turbo/turbo-build.log +11100 -0
  2. package/README.md +108 -0
  3. package/dist/index.d.mts +2251 -0
  4. package/dist/index.d.ts +2251 -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/docs/add_liquidity.md +145 -0
  10. package/docs/close_position.md +57 -0
  11. package/docs/collect_fees.md +37 -0
  12. package/docs/create_clmm_pool.md +228 -0
  13. package/docs/error_code.md +69 -0
  14. package/docs/get_clmm_pools.md +92 -0
  15. package/docs/get_positions.md +70 -0
  16. package/docs/get_reward.md +53 -0
  17. package/docs/get_ticks.md +39 -0
  18. package/docs/migrate_to_version_6.0.md +143 -0
  19. package/docs/open_position.md +224 -0
  20. package/docs/partner_swap.md +60 -0
  21. package/docs/pre_swap.md +136 -0
  22. package/docs/remove_liquidity.md +124 -0
  23. package/docs/swap.md +153 -0
  24. package/docs/utils.md +85 -0
  25. package/package.json +37 -0
  26. package/src/config/index.ts +2 -0
  27. package/src/config/mainnet.ts +41 -0
  28. package/src/config/testnet.ts +40 -0
  29. package/src/errors/errors.ts +93 -0
  30. package/src/errors/index.ts +1 -0
  31. package/src/index.ts +10 -0
  32. package/src/math/apr.ts +167 -0
  33. package/src/math/index.ts +1 -0
  34. package/src/modules/configModule.ts +540 -0
  35. package/src/modules/index.ts +5 -0
  36. package/src/modules/poolModule.ts +1066 -0
  37. package/src/modules/positionModule.ts +932 -0
  38. package/src/modules/rewarderModule.ts +430 -0
  39. package/src/modules/swapModule.ts +389 -0
  40. package/src/sdk.ts +131 -0
  41. package/src/types/clmm_type.ts +1002 -0
  42. package/src/types/clmmpool.ts +366 -0
  43. package/src/types/config_type.ts +241 -0
  44. package/src/types/index.ts +8 -0
  45. package/src/types/sui.ts +124 -0
  46. package/src/types/token_type.ts +189 -0
  47. package/src/utils/common.ts +426 -0
  48. package/src/utils/index.ts +3 -0
  49. package/src/utils/positionUtils.ts +434 -0
  50. package/src/utils/swapUtils.ts +499 -0
  51. package/tests/add_liquidity.test.ts +121 -0
  52. package/tests/add_liquidity_fix_token.test.ts +182 -0
  53. package/tests/apr.test.ts +71 -0
  54. package/tests/cetus_config.test.ts +26 -0
  55. package/tests/collect_fees.test.ts +11 -0
  56. package/tests/pool.test.ts +267 -0
  57. package/tests/position.test.ts +145 -0
  58. package/tests/remove_liquidity.test.ts +119 -0
  59. package/tests/rewarder.test.ts +60 -0
  60. package/tests/sdk_config.test.ts +49 -0
  61. package/tests/swap.test.ts +254 -0
  62. package/tests/tsconfig.json +26 -0
  63. package/tsconfig.json +5 -0
  64. package/tsup.config.ts +10 -0
@@ -0,0 +1,426 @@
1
+ import type { SuiObjectResponse, SuiTransactionBlockResponse } from '@mysten/sui/client'
2
+ import { Transaction, TransactionObjectArgument } from '@mysten/sui/transactions'
3
+ import BN from 'bn.js'
4
+ import {
5
+ asIntN,
6
+ buildNFT,
7
+ d,
8
+ DETAILS_KEYS,
9
+ extractStructTagFromType,
10
+ getMoveObjectType,
11
+ getObjectDeletedResponse,
12
+ getObjectFields,
13
+ getObjectId,
14
+ getObjectNotExistsResponse,
15
+ getObjectOwner,
16
+ MathUtil,
17
+ } from '@cetusprotocol/common-sdk'
18
+ import { handleMessageError, PoolErrorCode, PositionErrorCode } from '../errors/errors'
19
+ import { CetusClmmSDK } from '../sdk'
20
+ import type { Pool, PoolTransactionInfo, Position, PositionReward, PositionTransactionInfo, Rewarder } from '../types'
21
+ import { ClmmIntegrateUtilsModule, ClmmPositionStatus, poolFilterEvenTypes } from '../types'
22
+ import type { TickData } from '../types/clmmpool'
23
+ import type { NFT } from '../types/sui'
24
+
25
+ /**
26
+ * Builds a pool name based on two coin types and tick spacing.
27
+ * @param {string} coin_type_a - The type of the first coin.
28
+ * @param {string} coin_type_b - The type of the second coin.
29
+ * @param {string} tick_spacing - The tick spacing of the pool.
30
+ * @returns {string} - The name of the pool.
31
+ */
32
+ function buildPoolName(coin_type_a: string, coin_type_b: string, tick_spacing: string) {
33
+ const coinNameA = extractStructTagFromType(coin_type_a).name
34
+ const coinNameB = extractStructTagFromType(coin_type_b).name
35
+ return `${coinNameA}-${coinNameB}[${tick_spacing}]`
36
+ }
37
+
38
+ /**
39
+ * Builds a Pool object based on a SuiObjectResponse.
40
+ * @param {SuiObjectResponse} objects - The SuiObjectResponse containing information about the pool.
41
+ * @returns {Pool} - The built Pool object.
42
+ */
43
+ export function buildPool(objects: SuiObjectResponse): Pool {
44
+ const type = getMoveObjectType(objects) as string
45
+ const formatType = extractStructTagFromType(type)
46
+ const fields = getObjectFields(objects)
47
+ if (fields == null) {
48
+ handleMessageError(PoolErrorCode.InvalidPoolObject, `Pool id ${getObjectId(objects)} not exists.`, {
49
+ [DETAILS_KEYS.METHOD_NAME]: 'buildPool',
50
+ })
51
+ }
52
+
53
+ const rewarders: Rewarder[] = []
54
+ fields.rewarder_manager.fields.rewarders.forEach((item: any) => {
55
+ const { emissions_per_second } = item.fields
56
+ const emissionSeconds = MathUtil.fromX64(new BN(emissions_per_second))
57
+ const emissionsEveryDay = Math.floor(emissionSeconds.toNumber() * 60 * 60 * 24)
58
+
59
+ rewarders.push({
60
+ emissions_per_second,
61
+ coin_type: extractStructTagFromType(item.fields.reward_coin.fields.name).source_address,
62
+ growth_global: item.fields.growth_global,
63
+ emissions_every_day: emissionsEveryDay,
64
+ })
65
+ })
66
+
67
+ const pool: Pool = {
68
+ id: getObjectId(objects),
69
+ pool_type: type,
70
+ coin_type_a: formatType.type_arguments[0],
71
+ coin_type_b: formatType.type_arguments[1],
72
+ coin_amount_a: fields.coin_a,
73
+ coin_amount_b: fields.coin_b,
74
+ current_sqrt_price: fields.current_sqrt_price,
75
+ current_tick_index: asIntN(BigInt(fields.current_tick_index.fields.bits)),
76
+ fee_growth_global_a: fields.fee_growth_global_a,
77
+ fee_growth_global_b: fields.fee_growth_global_b,
78
+ fee_protocol_coin_a: fields.fee_protocol_coin_a,
79
+ fee_protocol_coin_b: fields.fee_protocol_coin_b,
80
+ fee_rate: fields.fee_rate,
81
+ is_pause: fields.is_pause,
82
+ liquidity: fields.liquidity,
83
+ position_manager: {
84
+ positions_handle: fields.position_manager.fields.positions.fields.id.id,
85
+ size: fields.position_manager.fields.positions.fields.size,
86
+ },
87
+ rewarder_infos: rewarders,
88
+ rewarder_last_updated_time: fields.rewarder_manager.fields.last_updated_time,
89
+ tick_spacing: fields.tick_spacing,
90
+ ticks_handle: fields.tick_manager.fields.ticks.fields.id.id,
91
+ uri: fields.url,
92
+ index: Number(fields.index),
93
+ name: '',
94
+ }
95
+ pool.name = buildPoolName(pool.coin_type_a, pool.coin_type_b, pool.tick_spacing)
96
+ return pool
97
+ }
98
+
99
+ /** Builds a Position object based on a SuiObjectResponse.
100
+ * @param {SuiObjectResponse} object - The SuiObjectResponse containing information about the position.
101
+ * @returns {Position} - The built Position object.
102
+ */
103
+ export function buildPosition(object: SuiObjectResponse): Position {
104
+ if (object.error != null || object.data?.content?.dataType !== 'moveObject') {
105
+ handleMessageError(PositionErrorCode.InvalidPositionObject, `Position not exists. Get Position error:${object.error}`, {
106
+ [DETAILS_KEYS.METHOD_NAME]: 'buildPosition',
107
+ })
108
+ }
109
+
110
+ let nft: NFT = {
111
+ creator: '',
112
+ description: '',
113
+ image_url: '',
114
+ link: '',
115
+ name: '',
116
+ project_url: '',
117
+ }
118
+
119
+ let position = {
120
+ ...nft,
121
+ pos_object_id: '',
122
+ owner: '',
123
+ type: '',
124
+ coin_type_a: '',
125
+ coin_type_b: '',
126
+ liquidity: '',
127
+ tick_lower_index: 0,
128
+ tick_upper_index: 0,
129
+ index: 0,
130
+ pool: '',
131
+ reward_amount_owned_0: '0',
132
+ reward_amount_owned_1: '0',
133
+ reward_amount_owned_2: '0',
134
+ reward_growth_inside_0: '0',
135
+ reward_growth_inside_1: '0',
136
+ reward_growth_inside_2: '0',
137
+ fee_growth_inside_a: '0',
138
+ fee_owned_a: '0',
139
+ fee_growth_inside_b: '0',
140
+ fee_owned_b: '0',
141
+ position_status: ClmmPositionStatus.Exists,
142
+ }
143
+ let fields = getObjectFields(object)
144
+ if (fields) {
145
+ const type = getMoveObjectType(object) as string
146
+ const ownerWarp = getObjectOwner(object) as {
147
+ AddressOwner: string
148
+ }
149
+
150
+ if ('nft' in fields) {
151
+ fields = fields.nft.fields
152
+ nft.description = fields.description as string
153
+ nft.name = fields.name
154
+ nft.link = fields.url
155
+ } else {
156
+ nft = buildNFT(object)
157
+ }
158
+
159
+ position = {
160
+ ...nft,
161
+ pos_object_id: fields.id.id,
162
+ owner: ownerWarp.AddressOwner,
163
+ type,
164
+ liquidity: fields.liquidity,
165
+ coin_type_a: fields.coin_type_a.fields.name,
166
+ coin_type_b: fields.coin_type_b.fields.name,
167
+ tick_lower_index: asIntN(BigInt(fields.tick_lower_index.fields.bits)),
168
+ tick_upper_index: asIntN(BigInt(fields.tick_upper_index.fields.bits)),
169
+ index: fields.index,
170
+ pool: fields.pool,
171
+ reward_amount_owned_0: '0',
172
+ reward_amount_owned_1: '0',
173
+ reward_amount_owned_2: '0',
174
+ reward_growth_inside_0: '0',
175
+ reward_growth_inside_1: '0',
176
+ reward_growth_inside_2: '0',
177
+ fee_growth_inside_a: '0',
178
+ fee_owned_a: '0',
179
+ fee_growth_inside_b: '0',
180
+ fee_owned_b: '0',
181
+ position_status: ClmmPositionStatus.Exists,
182
+ }
183
+ }
184
+
185
+ const deletedResponse = getObjectDeletedResponse(object)
186
+ if (deletedResponse) {
187
+ position.pos_object_id = deletedResponse.objectId
188
+ position.position_status = ClmmPositionStatus.Deleted
189
+ }
190
+ const objectNotExistsResponse = getObjectNotExistsResponse(object)
191
+ if (objectNotExistsResponse) {
192
+ position.pos_object_id = objectNotExistsResponse
193
+ position.position_status = ClmmPositionStatus.NotExists
194
+ }
195
+
196
+ return position
197
+ }
198
+
199
+ /**
200
+ * Builds a PositionReward object based on a response containing information about the reward.
201
+ * @param {any} fields - The response containing information about the reward.
202
+ * @returns {PositionReward} - The built PositionReward object.
203
+ */
204
+ export function buildPositionReward(fields: any): PositionReward {
205
+ const rewarders = {
206
+ reward_amount_owned_0: '0',
207
+ reward_amount_owned_1: '0',
208
+ reward_amount_owned_2: '0',
209
+ reward_growth_inside_0: '0',
210
+ reward_growth_inside_1: '0',
211
+ reward_growth_inside_2: '0',
212
+ }
213
+ fields = 'fields' in fields ? fields.fields : fields
214
+
215
+ fields.rewards.forEach((item: any, index: number) => {
216
+ const { amount_owned, growth_inside } = 'fields' in item ? item.fields : item
217
+ if (index === 0) {
218
+ rewarders.reward_amount_owned_0 = amount_owned
219
+ rewarders.reward_growth_inside_0 = growth_inside
220
+ } else if (index === 1) {
221
+ rewarders.reward_amount_owned_1 = amount_owned
222
+ rewarders.reward_growth_inside_1 = growth_inside
223
+ } else if (index === 2) {
224
+ rewarders.reward_amount_owned_2 = amount_owned
225
+ rewarders.reward_growth_inside_2 = growth_inside
226
+ }
227
+ })
228
+
229
+ const tick_lower_index = 'fields' in fields.tick_lower_index ? fields.tick_lower_index.fields.bits : fields.tick_lower_index.bits
230
+ const tick_upper_index = 'fields' in fields.tick_upper_index ? fields.tick_upper_index.fields.bits : fields.tick_upper_index.bits
231
+
232
+ const position: PositionReward = {
233
+ liquidity: fields.liquidity,
234
+ tick_lower_index: asIntN(BigInt(tick_lower_index)),
235
+ tick_upper_index: asIntN(BigInt(tick_upper_index)),
236
+ ...rewarders,
237
+ fee_growth_inside_a: fields.fee_growth_inside_a,
238
+ fee_owned_a: fields.fee_owned_a,
239
+ fee_growth_inside_b: fields.fee_growth_inside_b,
240
+ fee_owned_b: fields.fee_owned_b,
241
+ pos_object_id: fields.position_id,
242
+ }
243
+ return position
244
+ }
245
+
246
+ /**
247
+ * Builds a TickData object based on a response containing information about tick data.
248
+ * It must check if the response contains the required fields.
249
+ * @param {SuiObjectResponse} objects - The response containing information about tick data.
250
+ * @returns {TickData} - The built TickData object.
251
+ */
252
+ export function buildTickData(objects: SuiObjectResponse): TickData {
253
+ if (objects.error != null || objects.data?.content?.dataType !== 'moveObject') {
254
+ handleMessageError(PoolErrorCode.InvalidTickObject, `Tick not exists. Get tick data error:${objects.error}`, {
255
+ [DETAILS_KEYS.METHOD_NAME]: 'buildTickData',
256
+ })
257
+ }
258
+
259
+ const fields = getObjectFields(objects)
260
+
261
+ const valueItem = fields.value.fields.value.fields
262
+ const position: TickData = {
263
+ object_id: getObjectId(objects),
264
+ index: asIntN(BigInt(valueItem.index.fields.bits)),
265
+ sqrt_price: new BN(valueItem.sqrt_price),
266
+ liquidity_net: new BN(valueItem.liquidity_net.fields.bits),
267
+ liquidity_gross: new BN(valueItem.liquidity_gross),
268
+ fee_growth_outside_a: new BN(valueItem.fee_growth_outside_a),
269
+ fee_growth_outside_b: new BN(valueItem.fee_growth_outside_b),
270
+ rewarders_growth_outside: valueItem.rewards_growth_outside,
271
+ }
272
+
273
+ return position
274
+ }
275
+
276
+ /**
277
+ * Builds a TickData object based on a given event's fields.
278
+ * @param {any} fields - The fields of an event.
279
+ * @returns {TickData} - The built TickData object.
280
+ * @throws {Error} If any required field is missing.
281
+ */
282
+ export function buildTickDataByEvent(fields: any): TickData {
283
+ if (
284
+ !fields ||
285
+ !fields.index ||
286
+ !fields.sqrt_price ||
287
+ !fields.liquidity_net ||
288
+ !fields.liquidity_gross ||
289
+ !fields.fee_growth_outside_a ||
290
+ !fields.fee_growth_outside_b
291
+ ) {
292
+ handleMessageError(PoolErrorCode.InvalidTickFields, `Invalid tick fields.`, {
293
+ [DETAILS_KEYS.METHOD_NAME]: 'buildTickDataByEvent',
294
+ })
295
+ }
296
+
297
+ // It's assumed that asIntN is a function that converts a BigInt to an integer.
298
+ const index = asIntN(BigInt(fields.index.bits))
299
+ const sqrt_price = new BN(fields.sqrt_price)
300
+ const liquidity_net = new BN(fields.liquidity_net.bits)
301
+ const liquidity_gross = new BN(fields.liquidity_gross)
302
+ const fee_growth_outside_a = new BN(fields.fee_growth_outside_a)
303
+ const fee_growth_outside_b = new BN(fields.fee_growth_outside_b)
304
+ const rewarders_growth_outside = fields.rewards_growth_outside || []
305
+
306
+ const tick: TickData = {
307
+ object_id: '',
308
+ index,
309
+ sqrt_price,
310
+ liquidity_net,
311
+ liquidity_gross,
312
+ fee_growth_outside_a,
313
+ fee_growth_outside_b,
314
+ rewarders_growth_outside,
315
+ }
316
+
317
+ return tick
318
+ }
319
+
320
+ export function buildClmmPositionName(pool_index: number, position_index: number): string {
321
+ return `Cetus LP | Pool${pool_index}-${position_index}`
322
+ }
323
+
324
+ export function buildPositionTransactionInfo(data: SuiTransactionBlockResponse, txIndex: number, filterIds: string[]) {
325
+ const list: PositionTransactionInfo[] = []
326
+ const { timestampMs, events } = data
327
+
328
+ const filterEvenTypes = [
329
+ 'AddLiquidityEvent',
330
+ 'RemoveLiquidityEvent',
331
+ 'CollectFeeEvent',
332
+ 'CollectRewardEvent',
333
+ 'CollectRewardV2Event',
334
+ 'HarvestEvent',
335
+ ]
336
+
337
+ events?.forEach((event, index) => {
338
+ const type = extractStructTagFromType(event.type).name
339
+ if (filterEvenTypes.includes(type)) {
340
+ const info: PositionTransactionInfo = {
341
+ tx_digest: event.id.txDigest,
342
+ package_id: event.packageId,
343
+ transaction_module: event.transactionModule,
344
+ sender: event.sender,
345
+ type: event.type,
346
+ timestamp_ms: timestampMs || '0',
347
+ parsed_json: event.parsedJson,
348
+ index: `${txIndex}_${index}`,
349
+ }
350
+
351
+ switch (type) {
352
+ case 'CollectFeeEvent':
353
+ if (filterIds.includes(info.parsed_json.position) && (d(info.parsed_json.amount_a).gt(0) || d(info.parsed_json.amount_b).gt(0))) {
354
+ list.push(info)
355
+ }
356
+ break
357
+ case 'RemoveLiquidityEvent':
358
+ case 'AddLiquidityEvent':
359
+ if (d(info.parsed_json.amount_a).gt(0) || d(info.parsed_json.amount_b).gt(0)) {
360
+ list.push(info)
361
+ }
362
+ break
363
+ case 'CollectRewardEvent':
364
+ case 'HarvestEvent':
365
+ case 'CollectRewardV2Event':
366
+ if (
367
+ (filterIds.includes(info.parsed_json.position) || filterIds.includes(info.parsed_json.wrapped_position_id)) &&
368
+ d(info.parsed_json.amount).gt(0)
369
+ ) {
370
+ list.push(info)
371
+ }
372
+ break
373
+
374
+ default:
375
+ break
376
+ }
377
+ }
378
+ })
379
+
380
+ return list
381
+ }
382
+
383
+ export function buildPoolTransactionInfo(data: SuiTransactionBlockResponse, txIndex: number, package_id: string, pool_id: string) {
384
+ const list: PoolTransactionInfo[] = []
385
+ const { timestampMs, events } = data
386
+
387
+ events?.forEach((event: any, index) => {
388
+ const { name: type, address: package_address } = extractStructTagFromType(event.type)
389
+ if (poolFilterEvenTypes.includes(type) && package_address === package_id && pool_id === event.parsedJson.pool) {
390
+ const info: PoolTransactionInfo = {
391
+ tx: event.id.txDigest,
392
+ sender: event.sender,
393
+ type: event.type,
394
+ block_time: timestampMs || '0',
395
+ index: `${txIndex}_${index}`,
396
+ parsed_json: event.parsedJson,
397
+ }
398
+ list.push(info)
399
+ }
400
+ })
401
+
402
+ return list
403
+ }
404
+
405
+ export function buildTransferCoinToSender(sdk: CetusClmmSDK, tx: Transaction, coin: TransactionObjectArgument, coinType: string) {
406
+ tx.moveCall({
407
+ target: `${sdk.sdkOptions.integrate.published_at}::${ClmmIntegrateUtilsModule}::transfer_coin_to_sender`,
408
+ typeArguments: [coinType],
409
+ arguments: [coin],
410
+ })
411
+ }
412
+
413
+ // If recipient not set, transfer objects move call will use ctx sender
414
+ export function buildTransferCoin(
415
+ sdk: CetusClmmSDK,
416
+ tx: Transaction,
417
+ coin: TransactionObjectArgument,
418
+ coinType: string,
419
+ recipient?: string
420
+ ) {
421
+ if (recipient != null) {
422
+ tx.transferObjects([coin], tx.pure.address(recipient))
423
+ } else {
424
+ buildTransferCoinToSender(sdk, tx, coin, coinType)
425
+ }
426
+ }
@@ -0,0 +1,3 @@
1
+ export * from './common'
2
+ export * from './positionUtils'
3
+ export * from './swapUtils'