pinpet-sdk 0.1.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.
@@ -0,0 +1,884 @@
1
+ const Decimal = require('decimal.js');
2
+
3
+ // Configure Decimal.js to use 28-bit precision to match rust_decimal
4
+ Decimal.set({ precision: 28 });
5
+
6
+
7
+ /**
8
+ * Denominator used for fee calculations (10^5)
9
+ * @type {bigint}
10
+ */
11
+ const FEE_DENOMINATOR = 100_000n;
12
+
13
+ /**
14
+ * Maximum fee rate (10%)
15
+ * @type {bigint}
16
+ */
17
+ const MAX_FEE_RATE = 10_000n;
18
+
19
+
20
+
21
+
22
+ /**
23
+ * Traditional AMM trading model class
24
+ * Implements constant product (xy=k) algorithm for automated market maker functionality
25
+ * Usage when importing: const CurveAMM = require("../tools/curve_amm");
26
+ */
27
+ class CurveAMM {
28
+ /**
29
+ * Initial SOL reserve amount, represented as Decimal
30
+ * @type {Decimal}
31
+ */
32
+ static INITIAL_SOL_RESERVE_DECIMAL = new Decimal('30');
33
+
34
+ /**
35
+ * Initial Token reserve amount, represented as Decimal
36
+ * @type {Decimal}
37
+ */
38
+ static INITIAL_TOKEN_RESERVE_DECIMAL = new Decimal('1073000000');
39
+
40
+ /**
41
+ * Initial constant K value, represented as Decimal
42
+ * @type {Decimal}
43
+ */
44
+ static INITIAL_K_DECIMAL = new Decimal('32190000000');
45
+
46
+ /**
47
+ * Minimum price that can appear, below this price may cause overflow
48
+ * @type {Decimal}
49
+ */
50
+ static INITIAL_MIN_PRICE_DECIMAL = new Decimal('0.000000001');
51
+
52
+ /**
53
+ * Decimal representation of precision factor = 10000000000000000000000000000
54
+ * @type {Decimal}
55
+ */
56
+ //static PRICE_PRECISION_FACTOR_DECIMAL = new Decimal('10000000000000000000000000000');
57
+ static PRICE_PRECISION_FACTOR_DECIMAL = new Decimal('100000000000000000000000000');
58
+
59
+ /**
60
+ * Decimal representation of Token precision factor = 1000000
61
+ * @type {Decimal}
62
+ */
63
+ static TOKEN_PRECISION_FACTOR_DECIMAL = new Decimal('1000000');
64
+
65
+ /**
66
+ * Decimal representation of SOL precision factor = 1000000000
67
+ * @type {Decimal}
68
+ */
69
+ static SOL_PRECISION_FACTOR_DECIMAL = new Decimal('1000000000');
70
+
71
+
72
+ /**
73
+ * Maximum price for u128
74
+ * @type {bigint}
75
+ */
76
+ static MAX_U128_PRICE = 50000000000000000000000000000n;
77
+
78
+
79
+ /**
80
+ * Minimum price for u128
81
+ * @type {bigint}
82
+ */
83
+ static MIN_U128_PRICE = 11958993476234855500n;
84
+
85
+
86
+
87
+
88
+ /**
89
+ * Convert u128 price to Decimal
90
+ *
91
+ * @param {bigint|string|number} price - u128 price to be converted
92
+ * @returns {Decimal} Converted Decimal price
93
+ */
94
+ static u128ToDecimal(price) {
95
+ if (typeof price === 'bigint') {
96
+ price = price.toString();
97
+ }
98
+ const priceDecimal = new Decimal(price);
99
+ return priceDecimal.div(this.PRICE_PRECISION_FACTOR_DECIMAL);
100
+ }
101
+
102
+ /**
103
+ * Convert Decimal price to u128, rounded down
104
+ *
105
+ * @param {Decimal} price - Decimal price to be converted
106
+ * @returns {bigint|null} Converted u128 price, returns null if overflow
107
+ */
108
+ static decimalToU128(price) {
109
+ const scaled = price.mul(this.PRICE_PRECISION_FACTOR_DECIMAL);
110
+ const floored = scaled.floor();
111
+ if (floored.isNaN() || floored.isNegative() || floored.gt(this.MAX_U128_PRICE.toString())) {
112
+ return null;
113
+ }
114
+ // Use toFixed() to avoid scientific notation
115
+ const flooredStr = floored.toFixed(0);
116
+ return BigInt(flooredStr);
117
+ }
118
+
119
+ /**
120
+ * Convert Decimal price to u128, rounded up
121
+ *
122
+ * @param {Decimal} price - Decimal price to be converted
123
+ * @returns {bigint|null} Converted u128 price, returns null if overflow
124
+ */
125
+ static decimalToU128Ceil(price) {
126
+ const scaled = price.mul(this.PRICE_PRECISION_FACTOR_DECIMAL);
127
+ const ceiled = scaled.ceil();
128
+ if (ceiled.isNaN() || ceiled.isNegative() || ceiled.gt(this.MAX_U128_PRICE.toString())) {
129
+ return null;
130
+ }
131
+ // Use toFixed() to avoid scientific notation
132
+ const ceiledStr = ceiled.toFixed(0);
133
+ return BigInt(ceiledStr);
134
+ }
135
+
136
+ /**
137
+ * Convert u64 price to Decimal (legacy compatibility)
138
+ *
139
+ * @param {bigint|string|number} price - u64 price to be converted
140
+ * @returns {Decimal} Converted Decimal price
141
+ * @deprecated Please use u128ToDecimal instead
142
+ */
143
+ static u64ToDecimal(price) {
144
+ // Directly use old precision factor for conversion
145
+ if (typeof price === 'bigint') {
146
+ price = price.toString();
147
+ }
148
+ const priceDecimal = new Decimal(price);
149
+ // Use old precision factor 10^15
150
+ const oldPrecisionFactor = new Decimal('1000000000000000');
151
+ return priceDecimal.div(oldPrecisionFactor);
152
+ }
153
+
154
+ /**
155
+ * Convert Decimal price to u64, rounded down (legacy compatibility)
156
+ *
157
+ * @param {Decimal} price - Decimal price to be converted
158
+ * @returns {bigint|null} Converted u64 price, returns null if overflow
159
+ * @deprecated Please use decimalToU128 instead
160
+ */
161
+ static decimalToU64(price) {
162
+ // Directly use old precision factor for conversion
163
+ const oldPrecisionFactor = new Decimal('1000000000000000');
164
+ const scaled = price.mul(oldPrecisionFactor);
165
+ const floored = scaled.floor();
166
+ if (floored.isNaN() || floored.isNegative() || floored.gt('18446744073709551615')) {
167
+ return null;
168
+ }
169
+ const flooredStr = floored.toFixed(0);
170
+ return BigInt(flooredStr);
171
+ }
172
+
173
+ /**
174
+ * Convert Decimal price to u64, rounded up (legacy compatibility)
175
+ *
176
+ * @param {Decimal} price - Decimal price to be converted
177
+ * @returns {bigint|null} Converted u64 price, returns null if overflow
178
+ * @deprecated Please use decimalToU128Ceil instead
179
+ */
180
+ static decimalToU64Ceil(price) {
181
+ // Directly use old precision factor for conversion
182
+ const oldPrecisionFactor = new Decimal('1000000000000000');
183
+ const scaled = price.mul(oldPrecisionFactor);
184
+ const ceiled = scaled.ceil();
185
+ if (ceiled.isNaN() || ceiled.isNegative() || ceiled.gt('18446744073709551615')) {
186
+ return null;
187
+ }
188
+ const ceiledStr = ceiled.toFixed(0);
189
+ return BigInt(ceiledStr);
190
+ }
191
+
192
+ /**
193
+ * Convert Decimal token amount to u64, using 6-digit precision, rounded down
194
+ *
195
+ * @param {Decimal} amount - Decimal token amount to be converted
196
+ * @returns {bigint|null} Converted u64 token amount, returns null if overflow
197
+ */
198
+ static tokenDecimalToU64(amount) {
199
+ const scaled = amount.mul(this.TOKEN_PRECISION_FACTOR_DECIMAL);
200
+ const floored = scaled.floor();
201
+ if (floored.isNaN() || floored.isNegative() || floored.gt('18446744073709551615')) {
202
+ return null;
203
+ }
204
+ const flooredStr = floored.toFixed(0);
205
+ return BigInt(flooredStr);
206
+ }
207
+
208
+ /**
209
+ * Convert Decimal token amount to u64, using 6-digit precision, rounded up
210
+ *
211
+ * @param {Decimal} amount - Decimal token amount to be converted
212
+ * @returns {bigint|null} Converted u64 token amount, returns null if overflow
213
+ */
214
+ static tokenDecimalToU64Ceil(amount) {
215
+ const scaled = amount.mul(this.TOKEN_PRECISION_FACTOR_DECIMAL);
216
+ const ceiled = scaled.ceil();
217
+ if (ceiled.isNaN() || ceiled.isNegative() || ceiled.gt('18446744073709551615')) {
218
+ return null;
219
+ }
220
+ const ceiledStr = ceiled.toFixed(0);
221
+ return BigInt(ceiledStr);
222
+ }
223
+
224
+ /**
225
+ * Convert Decimal SOL amount to u64, using 9-digit precision, rounded down
226
+ *
227
+ * @param {Decimal} amount - Decimal SOL amount to be converted
228
+ * @returns {bigint|null} Converted u64 SOL amount, returns null if overflow
229
+ */
230
+ static solDecimalToU64(amount) {
231
+ const scaled = amount.mul(this.SOL_PRECISION_FACTOR_DECIMAL);
232
+ const floored = scaled.floor();
233
+ if (floored.isNaN() || floored.isNegative() || floored.gt('18446744073709551615')) {
234
+ return null;
235
+ }
236
+ const flooredStr = floored.toFixed(0);
237
+ return BigInt(flooredStr);
238
+ }
239
+
240
+ /**
241
+ * Convert Decimal SOL amount to u64, using 9-digit precision, rounded up
242
+ *
243
+ * @param {Decimal} amount - Decimal SOL amount to be converted
244
+ * @returns {bigint|null} Converted u64 SOL amount, returns null if overflow
245
+ */
246
+ static solDecimalToU64Ceil(amount) {
247
+ const scaled = amount.mul(this.SOL_PRECISION_FACTOR_DECIMAL);
248
+ const ceiled = scaled.ceil();
249
+ if (ceiled.isNaN() || ceiled.isNegative() || ceiled.gt('18446744073709551615')) {
250
+ return null;
251
+ }
252
+ const ceiledStr = ceiled.toFixed(0);
253
+ return BigInt(ceiledStr);
254
+ }
255
+
256
+ /**
257
+ * Convert u64 token amount to Decimal, using 6-digit precision
258
+ *
259
+ * @param {bigint|string|number} amount - u64 token amount to be converted
260
+ * @returns {Decimal} Converted Decimal token amount
261
+ */
262
+ static u64ToTokenDecimal(amount) {
263
+ if (typeof amount === 'bigint') {
264
+ amount = amount.toString();
265
+ }
266
+ const amountDecimal = new Decimal(amount);
267
+ return amountDecimal.div(this.TOKEN_PRECISION_FACTOR_DECIMAL);
268
+ }
269
+
270
+ /**
271
+ * Convert u64 SOL amount to Decimal, using 9-digit precision
272
+ *
273
+ * @param {bigint|string|number} amount - u64 SOL amount to be converted
274
+ * @returns {Decimal} Converted Decimal SOL amount
275
+ */
276
+ static u64ToSolDecimal(amount) {
277
+ if (typeof amount === 'bigint') {
278
+ amount = amount.toString();
279
+ }
280
+ const amountDecimal = new Decimal(amount);
281
+ return amountDecimal.div(this.SOL_PRECISION_FACTOR_DECIMAL);
282
+ }
283
+
284
+ /**
285
+ * Calculate initial k value
286
+ *
287
+ * @returns {Decimal} Product k value of initial reserves
288
+ */
289
+ static calculateInitialK() {
290
+ return this.INITIAL_SOL_RESERVE_DECIMAL.mul(this.INITIAL_TOKEN_RESERVE_DECIMAL);
291
+ }
292
+
293
+ /**
294
+ * Get initial price (SOL amount for 1 token)
295
+ *
296
+ * @returns {bigint|null} Initial price in u128 format, returns null if calculation fails
297
+ */
298
+ static getInitialPrice() {
299
+ // Calculate initial price = initial SOL reserve / initial Token reserve
300
+ const initialPrice = this.INITIAL_SOL_RESERVE_DECIMAL.div(this.INITIAL_TOKEN_RESERVE_DECIMAL);
301
+
302
+ // Convert to u128 format
303
+ return this.decimalToU128(initialPrice);
304
+ }
305
+
306
+ /**
307
+ * Calculate SOL required and token amount obtained when buying tokens from low to high price
308
+ *
309
+ * @param {bigint|string|number} startLowPrice - Starting price (lower)
310
+ * @param {bigint|string|number} endHighPrice - Target price (higher)
311
+ * @returns {[bigint, bigint]|null} Returns [SOL amount to invest, token amount to obtain] on success, null on failure
312
+ * SOL amount in 9-digit precision rounded up; token amount in 6-digit precision rounded down
313
+ */
314
+ static buyFromPriceToPrice(startLowPrice, endHighPrice) {
315
+ // Convert to Decimal for calculation
316
+ const startPriceDec = this.u128ToDecimal(startLowPrice);
317
+ const endPriceDec = this.u128ToDecimal(endHighPrice);
318
+
319
+ // Ensure starting price is lower than ending price
320
+ if (startPriceDec.gte(endPriceDec)) {
321
+ return null;
322
+ }
323
+
324
+ // Use initial k value
325
+ const k = this.calculateInitialK();
326
+
327
+ // Calculate reserves for starting and ending states
328
+ const startReserves = this.calculateReservesByPrice(startPriceDec, k);
329
+ const endReserves = this.calculateReservesByPrice(endPriceDec, k);
330
+
331
+ if (!startReserves || !endReserves) {
332
+ return null;
333
+ }
334
+
335
+ const [startSolReserve, startTokenReserve] = startReserves;
336
+ const [endSolReserve, endTokenReserve] = endReserves;
337
+
338
+ // Calculate SOL amount to invest (increase in SOL reserves)
339
+ const solInputAmount = endSolReserve.sub(startSolReserve);
340
+
341
+ // Calculate token amount to obtain (decrease in token reserves)
342
+ const tokenOutputAmount = startTokenReserve.sub(endTokenReserve);
343
+
344
+ // Check if calculation results are valid
345
+ if (solInputAmount.lte(0) || tokenOutputAmount.lte(0)) {
346
+ return null;
347
+ }
348
+
349
+ // Convert back to u64
350
+ // SOL uses 9-digit precision rounded up, token uses 6-digit precision rounded down
351
+ const solAmountU64 = this.solDecimalToU64Ceil(solInputAmount);
352
+ const tokenAmountU64 = this.tokenDecimalToU64(tokenOutputAmount);
353
+
354
+ if (solAmountU64 === null || tokenAmountU64 === null) {
355
+ return null;
356
+ }
357
+
358
+ return [solAmountU64, tokenAmountU64];
359
+ }
360
+
361
+ /**
362
+ * Calculate SOL amount obtained when selling tokens from high to low price
363
+ *
364
+ * @param {bigint|string|number} startHighPrice - Starting price (higher)
365
+ * @param {bigint|string|number} endLowPrice - Target price (lower)
366
+ * @returns {[bigint, bigint]|null} Returns [token amount to sell, SOL amount to obtain] on success, null on failure
367
+ * token amount in 6-digit precision rounded up; SOL amount in 9-digit precision rounded down
368
+ */
369
+ static sellFromPriceToPrice(startHighPrice, endLowPrice) {
370
+ // console.log('\n=== sellFromPriceToPrice debug info ===');
371
+ // console.log('Input parameters:');
372
+ // console.log(' startHighPrice:', startHighPrice);
373
+ // console.log(' endLowPrice:', endLowPrice);
374
+
375
+ // Convert to Decimal for calculation
376
+ const startPriceDec = this.u128ToDecimal(startHighPrice);
377
+ const endPriceDec = this.u128ToDecimal(endLowPrice);
378
+
379
+ // console.log('Price conversion results:');
380
+ // console.log(' startPriceDec:', startPriceDec.toString());
381
+ // console.log(' endPriceDec:', endPriceDec.toString());
382
+
383
+ // Ensure starting price is higher than ending price
384
+ if (startPriceDec.lte(endPriceDec)) {
385
+ // console.log('❌ Failure reason: starting price is lower than or equal to ending price');
386
+ // console.log(' startPriceDec.lte(endPriceDec):', startPriceDec.lte(endPriceDec));
387
+ return null;
388
+ }
389
+
390
+ // Use initial k value
391
+ const k = this.calculateInitialK();
392
+ //console.log('k value:', k.toString());
393
+
394
+ // Calculate reserves for starting and ending states
395
+ const startReserves = this.calculateReservesByPrice(startPriceDec, k);
396
+ const endReserves = this.calculateReservesByPrice(endPriceDec, k);
397
+
398
+ // console.log('Reserve calculation results:');
399
+ // console.log(' startReserves:', startReserves ? [startReserves[0].toString(), startReserves[1].toString()] : null);
400
+ // console.log(' endReserves:', endReserves ? [endReserves[0].toString(), endReserves[1].toString()] : null);
401
+
402
+ // if (!startReserves || !endReserves) {
403
+ // console.log('❌ Failure reason: reserve calculation failed');
404
+ // console.log(' startReserves:', startReserves);
405
+ // console.log(' endReserves:', endReserves);
406
+ // return null;
407
+ // }
408
+
409
+ const [startSolReserve, startTokenReserve] = startReserves;
410
+ const [endSolReserve, endTokenReserve] = endReserves;
411
+
412
+ // console.log('Detailed reserves:');
413
+ // console.log(' Starting state - SOL reserve:', startSolReserve.toString());
414
+ // console.log(' Starting state - Token reserve:', startTokenReserve.toString());
415
+ // console.log(' Ending state - SOL reserve:', endSolReserve.toString());
416
+ // console.log(' Ending state - Token reserve:', endTokenReserve.toString());
417
+
418
+ // Calculate token amount to sell (increase in token reserves)
419
+ const tokenInputAmount = endTokenReserve.sub(startTokenReserve);
420
+
421
+ // Calculate SOL amount to obtain (decrease in SOL reserves)
422
+ const solOutputAmount = startSolReserve.sub(endSolReserve);
423
+
424
+ // console.log('Transaction calculation results:');
425
+ // console.log(' Token amount to sell (tokenInputAmount):', tokenInputAmount.toString());
426
+ // console.log(' SOL amount to obtain (solOutputAmount):', solOutputAmount.toString());
427
+
428
+ // Check if calculation results are valid
429
+ if (tokenInputAmount.lte(0) || solOutputAmount.lte(0)) {
430
+ // console.log('❌ Failure reason: invalid transaction amount calculation results');
431
+ // console.log(' tokenInputAmount.lte(0):', tokenInputAmount.lte(0));
432
+ // console.log(' solOutputAmount.lte(0):', solOutputAmount.lte(0));
433
+ return null;
434
+ }
435
+
436
+ // Convert back to u64
437
+ // token uses 6-digit precision rounded up, SOL uses 9-digit precision rounded down
438
+ const tokenAmountU64 = this.tokenDecimalToU64Ceil(tokenInputAmount);
439
+ const solAmountU64 = this.solDecimalToU64(solOutputAmount);
440
+
441
+ // console.log('u64 conversion results:');
442
+ // console.log(' tokenAmountU64:', tokenAmountU64);
443
+ // console.log(' solAmountU64:', solAmountU64);
444
+
445
+ if (tokenAmountU64 === null || solAmountU64 === null) {
446
+ // console.log('❌ Failure reason: u64 conversion failed');
447
+ // console.log(' tokenAmountU64 === null:', tokenAmountU64 === null);
448
+ // console.log(' solAmountU64 === null:', solAmountU64 === null);
449
+ return null;
450
+ }
451
+
452
+ // console.log('✅ Success! Return results:');
453
+ // console.log(' Token amount to sell:', tokenAmountU64.toString());
454
+ // console.log(' SOL amount to obtain:', solAmountU64.toString());
455
+ // console.log('=== sellFromPriceToPrice debug end ===\n');
456
+
457
+ return [tokenAmountU64, solAmountU64];
458
+ }
459
+
460
+ /**
461
+ * Calculate reserves given a price
462
+ *
463
+ * @param {Decimal} price - Price, representing SOL amount for 1 token
464
+ * @param {Decimal} k - Constant product
465
+ * @returns {[Decimal, Decimal]|null} Returns [SOL reserve, token reserve] on success, null on failure
466
+ */
467
+ static calculateReservesByPrice(price, k) {
468
+ // Check if input parameters are valid
469
+ if (price.lte(0) || k.lte(0)) {
470
+ return null;
471
+ }
472
+
473
+ // Minimum price check to prevent overflow
474
+ if (price.lt(this.INITIAL_MIN_PRICE_DECIMAL)) {
475
+ return null;
476
+ }
477
+
478
+ // According to AMM formula: k = sol_reserve * token_reserve
479
+ // and price = sol_reserve / token_reserve
480
+ // We get: sol_reserve = price * token_reserve
481
+ // Substituting into k formula: k = price * token_reserve^2
482
+ // Therefore: token_reserve = sqrt(k / price)
483
+ // sol_reserve = sqrt(k * price)
484
+
485
+ // Calculate k / price
486
+ const kDivPrice = k.div(price);
487
+
488
+ // Calculate token_reserve = sqrt(k / price)
489
+ const tokenReserve = kDivPrice.sqrt();
490
+
491
+ // Calculate sol_reserve = price * token_reserve
492
+ const solReserve = price.mul(tokenReserve);
493
+
494
+ if (tokenReserve.isNaN() || solReserve.isNaN()) {
495
+ return null;
496
+ }
497
+
498
+ return [solReserve, tokenReserve];
499
+ }
500
+
501
+ /**
502
+ * Calculate token output amount and ending price based on starting price and SOL input amount
503
+ *
504
+ * @param {bigint|string|number} startLowPrice - Starting price
505
+ * @param {bigint|string|number} solInputAmount - SOL amount for buying
506
+ * @returns {[bigint, bigint]|null} Returns [price after transaction, token amount obtained] on success, null on failure
507
+ * Price rounded down, token amount rounded down
508
+ */
509
+ static buyFromPriceWithSolInput(startLowPrice, solInputAmount) {
510
+ // Convert to Decimal for calculation
511
+ const startPriceDec = this.u128ToDecimal(startLowPrice);
512
+ const solInputDec = this.u64ToSolDecimal(solInputAmount);
513
+
514
+ // Check if input parameters are valid
515
+ if (startPriceDec.lte(0)) {
516
+ return null;
517
+ }
518
+
519
+ // If SOL input amount is 0, return unchanged price and token output of 0
520
+ if (solInputDec.eq(0)) {
521
+ const endPriceU128 = this.decimalToU128(startPriceDec);
522
+ if (endPriceU128 === null) return null;
523
+ return [endPriceU128, 0n];
524
+ }
525
+
526
+ if (solInputDec.lt(0)) {
527
+ return null;
528
+ }
529
+
530
+ // Use initial k value
531
+ const k = this.calculateInitialK();
532
+
533
+ // Calculate reserves for starting state
534
+ const startReserves = this.calculateReservesByPrice(startPriceDec, k);
535
+ if (!startReserves) return null;
536
+
537
+ const [startSolReserve, startTokenReserve] = startReserves;
538
+
539
+ // Calculate SOL reserves for ending state
540
+ const endSolReserve = startSolReserve.add(solInputDec);
541
+
542
+ // Calculate token reserves for ending state according to AMM formula
543
+ const endTokenReserve = k.div(endSolReserve);
544
+
545
+ // Calculate token output amount
546
+ const tokenOutputAmount = startTokenReserve.sub(endTokenReserve);
547
+
548
+ // Calculate ending price
549
+ const endPrice = endSolReserve.div(endTokenReserve);
550
+
551
+ // Check if calculation results are valid
552
+ if (tokenOutputAmount.lte(0) || endPrice.lte(0)) {
553
+ return null;
554
+ }
555
+
556
+ // Convert back to appropriate types with required rounding
557
+ const endPriceU128 = this.decimalToU128(endPrice); // Price rounded down
558
+ const tokenAmountU64 = this.tokenDecimalToU64(tokenOutputAmount); // Token rounded down
559
+
560
+ if (endPriceU128 === null || tokenAmountU64 === null) {
561
+ return null;
562
+ }
563
+
564
+ return [endPriceU128, tokenAmountU64];
565
+ }
566
+
567
+ /**
568
+ * Calculate SOL output amount and ending price based on starting price and token input amount
569
+ *
570
+ * @param {bigint|string|number} startHighPrice - Starting price
571
+ * @param {bigint|string|number} tokenInputAmount - Token amount to sell
572
+ * @returns {[bigint, bigint]|null} Returns [price after transaction, SOL amount obtained] on success, null on failure
573
+ * Price rounded down, SOL amount rounded down
574
+ */
575
+ static sellFromPriceWithTokenInput(startHighPrice, tokenInputAmount) {
576
+ // Convert to Decimal for calculation
577
+ const startPriceDec = this.u128ToDecimal(startHighPrice);
578
+ const tokenInputDec = this.u64ToTokenDecimal(tokenInputAmount);
579
+
580
+ //console.log("startHighPrice, tokenInputAmount",startHighPrice, tokenInputAmount)
581
+ // Check if input parameters are valid
582
+ if (startPriceDec.lte(0)) {
583
+ return null;
584
+ }
585
+
586
+ // If token input amount is 0, return unchanged price and SOL output of 0
587
+ if (tokenInputDec.eq(0)) {
588
+ const endPriceU128 = this.decimalToU128(startPriceDec);
589
+ if (endPriceU128 === null) return null;
590
+ return [endPriceU128, 0n];
591
+ }
592
+
593
+ if (tokenInputDec.lt(0)) {
594
+ return null;
595
+ }
596
+
597
+ // Use initial k value
598
+ const k = this.calculateInitialK();
599
+
600
+ // Calculate reserves for starting state
601
+ const startReserves = this.calculateReservesByPrice(startPriceDec, k);
602
+ if (!startReserves) return null;
603
+
604
+ const [startSolReserve, startTokenReserve] = startReserves;
605
+
606
+ // Calculate token reserves for ending state
607
+ const endTokenReserve = startTokenReserve.add(tokenInputDec);
608
+
609
+ // 根据AMM公式计算结束状态的SOL储备量
610
+ const endSolReserve = k.div(endTokenReserve);
611
+
612
+ // Calculate SOL output amount
613
+ const solOutputAmount = startSolReserve.sub(endSolReserve);
614
+
615
+ // Calculate ending price
616
+ const endPrice = endSolReserve.div(endTokenReserve);
617
+
618
+ // Check if calculation results are valid
619
+ if (solOutputAmount.lte(0) || endPrice.lte(0)) {
620
+ return null;
621
+ }
622
+
623
+ // Convert back to appropriate types with required rounding
624
+ const endPriceU128 = this.decimalToU128(endPrice); // Price rounded down
625
+ const solAmountU64 = this.solDecimalToU64(solOutputAmount); // SOL rounded down
626
+
627
+ if (endPriceU128 === null || solAmountU64 === null) {
628
+ return null;
629
+ }
630
+
631
+ return [endPriceU128, solAmountU64];
632
+ }
633
+
634
+ /**
635
+ * Calculate required SOL input amount and ending price based on starting price and expected token output amount
636
+ *
637
+ * @param {bigint|string|number} startLowPrice - Starting price
638
+ * @param {bigint|string|number} tokenOutputAmount - Desired token amount to obtain
639
+ * @returns {[bigint, bigint]|null} Returns [price after transaction, SOL amount to pay] on success, null on failure
640
+ * Price rounded down, SOL amount rounded up
641
+ */
642
+ static buyFromPriceWithTokenOutput(startLowPrice, tokenOutputAmount) {
643
+ // Convert to Decimal for calculation
644
+ const startPriceDec = this.u128ToDecimal(startLowPrice);
645
+ const tokenOutputDec = this.u64ToTokenDecimal(tokenOutputAmount);
646
+
647
+ // Check if input parameters are valid
648
+ if (startPriceDec.lte(0)) {
649
+ return null;
650
+ }
651
+
652
+ // If token output amount is 0, return unchanged price and SOL input of 0
653
+ if (tokenOutputDec.eq(0)) {
654
+ const endPriceU128 = this.decimalToU128(startPriceDec);
655
+ if (endPriceU128 === null) return null;
656
+ return [endPriceU128, 0n];
657
+ }
658
+
659
+ if (tokenOutputDec.lt(0)) {
660
+ return null;
661
+ }
662
+
663
+ // Use initial k value
664
+ const k = this.calculateInitialK();
665
+
666
+ // Calculate reserves for starting state
667
+ const startReserves = this.calculateReservesByPrice(startPriceDec, k);
668
+ if (!startReserves) return null;
669
+
670
+ const [startSolReserve, startTokenReserve] = startReserves;
671
+
672
+ // Calculate token reserves for ending state
673
+ const endTokenReserve = startTokenReserve.sub(tokenOutputDec);
674
+
675
+ //console.log('buyFromPriceWithTokenOutput 结束token储备 = 起始token储备 - token输出量:', endTokenReserve.toString());
676
+
677
+ // Check if token reserves are sufficient
678
+ if (endTokenReserve.lte(0)) {
679
+ return null;
680
+ }
681
+
682
+ // 根据AMM公式计算结束状态的SOL储备量
683
+ const endSolReserve = k.div(endTokenReserve);
684
+
685
+ // Calculate required SOL input amount
686
+ const solInputAmount = endSolReserve.sub(startSolReserve);
687
+
688
+ // Calculate ending price
689
+ const endPrice = endSolReserve.div(endTokenReserve);
690
+
691
+ // Check if calculation results are valid
692
+ if (solInputAmount.lte(0) || endPrice.lte(0)) {
693
+ return null;
694
+ }
695
+
696
+ // Convert back to appropriate types with required rounding
697
+ const endPriceU128 = this.decimalToU128(endPrice); // Price rounded down
698
+ const solAmountU64 = this.solDecimalToU64Ceil(solInputAmount); // SOL rounded up
699
+
700
+ if (endPriceU128 === null || solAmountU64 === null) {
701
+ return null;
702
+ }
703
+
704
+ return [endPriceU128, solAmountU64];
705
+ }
706
+
707
+ /**
708
+ * Calculate required token input amount and ending price based on starting price and expected SOL output amount
709
+ *
710
+ * @param {bigint|string|number} startHighPrice - Starting price
711
+ * @param {bigint|string|number} solOutputAmount - Desired SOL amount to obtain
712
+ * @returns {[bigint, bigint]|null} Returns [price after transaction, token amount to pay] on success, null on failure
713
+ * Price rounded down, token amount rounded up
714
+ */
715
+ static sellFromPriceWithSolOutput(startHighPrice, solOutputAmount) {
716
+ // Convert to Decimal for calculation
717
+ const startPriceDec = this.u128ToDecimal(startHighPrice);
718
+ const solOutputDec = this.u64ToSolDecimal(solOutputAmount);
719
+
720
+ // Check if input parameters are valid
721
+ if (startPriceDec.lte(0)) {
722
+ return null;
723
+ }
724
+
725
+ // If SOL output amount is 0, return unchanged price and token input of 0
726
+ if (solOutputDec.eq(0)) {
727
+ const endPriceU128 = this.decimalToU128(startPriceDec);
728
+ if (endPriceU128 === null) return null;
729
+ return [endPriceU128, 0n];
730
+ }
731
+
732
+ if (solOutputDec.lt(0)) {
733
+ return null;
734
+ }
735
+
736
+ // Use initial k value
737
+ const k = this.calculateInitialK();
738
+
739
+ // Calculate reserves for starting state
740
+ const startReserves = this.calculateReservesByPrice(startPriceDec, k);
741
+ if (!startReserves) return null;
742
+
743
+ const [startSolReserve, startTokenReserve] = startReserves;
744
+
745
+ // Calculate SOL reserves for ending state
746
+ const endSolReserve = startSolReserve.sub(solOutputDec);
747
+
748
+ // Check if SOL reserves are sufficient
749
+ if (endSolReserve.lte(0)) {
750
+ return null;
751
+ }
752
+
753
+ // Calculate token reserves for ending state according to AMM formula
754
+ const endTokenReserve = k.div(endSolReserve);
755
+
756
+ // Calculate required token input amount
757
+ const tokenInputAmount = endTokenReserve.sub(startTokenReserve);
758
+
759
+ // Calculate ending price
760
+ const endPrice = endSolReserve.div(endTokenReserve);
761
+
762
+ // Check if calculation results are valid
763
+ if (tokenInputAmount.lte(0) || endPrice.lte(0)) {
764
+ return null;
765
+ }
766
+
767
+ // Convert back to appropriate types with required rounding
768
+ const endPriceU128 = this.decimalToU128(endPrice); // Price rounded down
769
+ const tokenAmountU64 = this.tokenDecimalToU64Ceil(tokenInputAmount); // Token rounded up
770
+
771
+ if (endPriceU128 === null || tokenAmountU64 === null) {
772
+ return null;
773
+ }
774
+
775
+ return [endPriceU128, tokenAmountU64];
776
+ }
777
+
778
+ /**
779
+ * Calculate remaining amount after deducting fees
780
+ *
781
+ * @param {bigint|string|number} amount - Original amount
782
+ * @param {number} fee - Fee rate, expressed with FEE_DENOMINATOR as denominator
783
+ * Example: 1000 represents 1% fee (1000/100000)
784
+ * 2000 represents 2% fee (2000/100000)
785
+ * @returns {bigint|null} Returns remaining amount after deducting fees on success, null on failure
786
+ * Fee calculation uses floor rounding, which is the most favorable calculation method for users
787
+ */
788
+ static calculateAmountAfterFee(amount, fee) {
789
+ // Convert input parameters to BigInt
790
+ try {
791
+ const amountBigInt = BigInt(amount.toString());
792
+ const feeBigInt = BigInt(fee);
793
+
794
+ // Check if fee rate is valid (must be less than or equal to 10%)
795
+ if (feeBigInt > MAX_FEE_RATE) {
796
+ return null;
797
+ }
798
+
799
+ // Calculate fee amount: amount * fee / FEE_DENOMINATOR
800
+ const feeAmount = (amountBigInt * feeBigInt) / FEE_DENOMINATOR;
801
+
802
+ // Calculate remaining amount after deducting fees
803
+ const amountAfterFee = amountBigInt - feeAmount;
804
+
805
+ return amountAfterFee;
806
+ } catch (error) {
807
+ return null;
808
+ }
809
+ }
810
+
811
+ /**
812
+ * Convert u128 price to readable decimal string format for display
813
+ *
814
+ * @param {bigint|string|number} price - u128 price to be converted
815
+ * @param {number} decimalPlaces - Number of decimal places to retain, default is 28
816
+ * @returns {string} Formatted price string
817
+ */
818
+ static formatPriceForDisplay(price, decimalPlaces = 28) {
819
+ if (typeof price === 'bigint') {
820
+ price = price.toString();
821
+ }
822
+ const priceDecimal = new Decimal(price);
823
+ const convertedPrice = priceDecimal.div(this.PRICE_PRECISION_FACTOR_DECIMAL);
824
+ return convertedPrice.toFixed(decimalPlaces);
825
+ }
826
+
827
+ /**
828
+ * Create complete price display string, including both integer and decimal formats
829
+ *
830
+ * @param {bigint|string|number} price - u128 price to be converted
831
+ * @param {number} decimalPlaces - Number of decimal places to retain, default is 28
832
+ * @returns {string} Formatted complete price string, format: "integer price (decimal price)"
833
+ */
834
+ static createPriceDisplayString(price, decimalPlaces = 28) {
835
+ const integerPrice = (typeof price === 'bigint') ? price.toString() : price.toString();
836
+ const decimalPrice = this.formatPriceForDisplay(price, decimalPlaces);
837
+ return `${integerPrice} (${decimalPrice})`;
838
+ }
839
+
840
+ /**
841
+ * Calculate price based on liquidity pool reserves (how much SOL 1 token is worth)
842
+ *
843
+ * @param {bigint|string|number|BN} lpTokenReserve - Token reserves in liquidity pool (u64 format, 6-digit precision)
844
+ * @param {bigint|string|number|BN} lpSolReserve - SOL reserves in liquidity pool (u64 format, 9-digit precision)
845
+ * @returns {string|null} Returns 28-digit decimal price string on success, null on failure
846
+ */
847
+ static calculatePoolPrice(lpTokenReserve, lpSolReserve) {
848
+ try {
849
+ // Handle BN objects, convert to string
850
+ let tokenReserveStr = lpTokenReserve;
851
+ let solReserveStr = lpSolReserve;
852
+
853
+ // If it's a BN object, use toString() method
854
+ if (lpTokenReserve && typeof lpTokenReserve === 'object' && lpTokenReserve.toString) {
855
+ tokenReserveStr = lpTokenReserve.toString();
856
+ }
857
+ if (lpSolReserve && typeof lpSolReserve === 'object' && lpSolReserve.toString) {
858
+ solReserveStr = lpSolReserve.toString();
859
+ }
860
+
861
+ // Convert to Decimal for calculation
862
+ const tokenReserveDec = this.u64ToTokenDecimal(tokenReserveStr);
863
+ const solReserveDec = this.u64ToSolDecimal(solReserveStr);
864
+
865
+ // Check if reserves are valid
866
+ if (tokenReserveDec.lte(0) || solReserveDec.lte(0)) {
867
+ return null;
868
+ }
869
+
870
+ // Calculate price: 1 token = SOL reserve / Token reserve
871
+ const price = solReserveDec.div(tokenReserveDec);
872
+
873
+ // Return 28-digit decimal string
874
+ return price.toFixed(28);
875
+ } catch (error) {
876
+ return null;
877
+ }
878
+ }
879
+ }
880
+
881
+ module.exports = CurveAMM;
882
+
883
+
884
+