@signals-protocol/v1-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.
@@ -0,0 +1,980 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
36
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
37
+ };
38
+ var __importDefault = (this && this.__importDefault) || function (mod) {
39
+ return (mod && mod.__esModule) ? mod : { "default": mod };
40
+ };
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.CLMSRSDK = exports.toMicroUSDC = exports.toWAD = void 0;
43
+ exports.createCLMSRSDK = createCLMSRSDK;
44
+ exports.createSignalsSDK = createSignalsSDK;
45
+ const big_js_1 = __importDefault(require("big.js"));
46
+ const types_1 = require("./types");
47
+ const MathUtils = __importStar(require("./utils/math"));
48
+ const fees_1 = require("./fees");
49
+ // Re-export types and utilities for easy access
50
+ __exportStar(require("./types"), exports);
51
+ var math_1 = require("./utils/math");
52
+ Object.defineProperty(exports, "toWAD", { enumerable: true, get: function () { return math_1.toWAD; } });
53
+ Object.defineProperty(exports, "toMicroUSDC", { enumerable: true, get: function () { return math_1.toMicroUSDC; } });
54
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
55
+ const ZERO_CONTEXT = `0x${"00".repeat(32)}`;
56
+ const INVERSE_SPEND_TOLERANCE = new big_js_1.default(1); // 1 micro USDC tolerance
57
+ const MAX_INVERSE_ITERATIONS = 64;
58
+ const OVERFLOW_GUARD_MULTIPLIER = new big_js_1.default("50e18");
59
+ function bigToBigInt(value) {
60
+ const rounded = value.round(0, big_js_1.default.roundDown);
61
+ if (!rounded.eq(value)) {
62
+ throw new types_1.CalculationError("Fee calculations require integer micro-USDC amounts");
63
+ }
64
+ return BigInt(rounded.toFixed(0, big_js_1.default.roundDown));
65
+ }
66
+ /**
67
+ * CLMSR SDK - 컨트랙트 뷰함수들과 역함수 제공
68
+ */
69
+ class CLMSRSDK {
70
+ // ============================================================================
71
+ // CONTRACT VIEW FUNCTIONS (컨트랙트 뷰함수들)
72
+ // ============================================================================
73
+ /**
74
+ * calculateOpenCost - 새 포지션 열기 비용 계산
75
+ * @param lowerTick Lower tick bound (inclusive)
76
+ * @param upperTick Upper tick bound (exclusive)
77
+ * @param quantity 매수 수량
78
+ * @param distribution Current market distribution
79
+ * @param market Market parameters
80
+ */
81
+ // Tick boundary in absolute ticks; internally maps to inclusive bin indices [loBin, hiBin]
82
+ calculateOpenCost(lowerTick, upperTick, quantity, distribution, market) {
83
+ const normalizedQuantity = MathUtils.formatUSDC(new big_js_1.default(quantity));
84
+ // Input validation
85
+ if (normalizedQuantity.lte(0)) {
86
+ throw new types_1.ValidationError("Quantity must be positive");
87
+ }
88
+ if (!distribution) {
89
+ throw new types_1.ValidationError("Distribution data is required but was undefined");
90
+ }
91
+ // Tick range 검증
92
+ this.validateTickRange(lowerTick, upperTick, market);
93
+ // 시장별 최대 수량 검증 (UX 개선)
94
+ this._assertQuantityWithinLimit(normalizedQuantity, market.liquidityParameter);
95
+ const quantityWad = MathUtils.toWad(normalizedQuantity);
96
+ const costWad = this._calculateTradeCostWad(lowerTick, upperTick, quantityWad, distribution, market);
97
+ const cost = MathUtils.formatUSDC(MathUtils.fromWadRoundUp(costWad));
98
+ // Calculate average price with proper formatting
99
+ // cost는 micro USDC, quantity도 micro USDC이므로 결과는 USDC/USDC = 비율
100
+ const averagePrice = cost.div(normalizedQuantity);
101
+ const formattedAveragePrice = new big_js_1.default(averagePrice.toFixed(6, big_js_1.default.roundDown)); // 6자리 정밀도로 충분
102
+ const feeOverlay = this.computeFeeOverlay("BUY", cost, normalizedQuantity, lowerTick, upperTick, market.feePolicyDescriptor);
103
+ const result = {
104
+ cost,
105
+ averagePrice: formattedAveragePrice,
106
+ feeAmount: feeOverlay.amount,
107
+ feeRate: feeOverlay.rate,
108
+ feeInfo: feeOverlay.info,
109
+ };
110
+ return result;
111
+ }
112
+ /**
113
+ * calculateIncreaseCost - 기존 포지션 증가 비용 계산
114
+ */
115
+ calculateIncreaseCost(position, additionalQuantity, distribution, market) {
116
+ const result = this.calculateOpenCost(position.lowerTick, position.upperTick, additionalQuantity, distribution, market);
117
+ return {
118
+ additionalCost: result.cost,
119
+ averagePrice: result.averagePrice,
120
+ feeAmount: result.feeAmount,
121
+ feeRate: result.feeRate,
122
+ feeInfo: result.feeInfo,
123
+ };
124
+ }
125
+ /**
126
+ * Decrease position 비용 계산
127
+ */
128
+ calculateDecreaseProceeds(position, sellQuantity, distribution, market) {
129
+ const normalizedSellQuantity = MathUtils.formatUSDC(new big_js_1.default(sellQuantity));
130
+ const baseResult = this._calcSellProceeds(position.lowerTick, position.upperTick, normalizedSellQuantity, position.quantity, distribution, market);
131
+ const feeOverlay = this.computeFeeOverlay("SELL", baseResult.proceeds, normalizedSellQuantity, position.lowerTick, position.upperTick, market.feePolicyDescriptor);
132
+ return {
133
+ proceeds: baseResult.proceeds,
134
+ averagePrice: baseResult.averagePrice,
135
+ feeAmount: feeOverlay.amount,
136
+ feeRate: feeOverlay.rate,
137
+ feeInfo: feeOverlay.info,
138
+ };
139
+ }
140
+ /**
141
+ * Close position 비용 계산
142
+ */
143
+ calculateCloseProceeds(position, distribution, market) {
144
+ const result = this.calculateDecreaseProceeds(position, position.quantity, distribution, market);
145
+ return {
146
+ proceeds: result.proceeds,
147
+ averagePrice: result.averagePrice,
148
+ feeAmount: result.feeAmount,
149
+ feeRate: result.feeRate,
150
+ feeInfo: result.feeInfo,
151
+ };
152
+ }
153
+ /**
154
+ * Claim amount 계산
155
+ */
156
+ calculateClaim(position, settlementTick) {
157
+ // 정산 틱이 포지션 범위 [lowerTick, upperTick)에 포함되는지 확인
158
+ const hasWinning = position.lowerTick <= settlementTick &&
159
+ position.upperTick > settlementTick;
160
+ if (!hasWinning) {
161
+ // 패배 포지션: 클레임 불가
162
+ return {
163
+ payout: new big_js_1.default(0),
164
+ };
165
+ }
166
+ // 승리 포지션: 1 USDC per unit
167
+ return {
168
+ payout: position.quantity,
169
+ };
170
+ }
171
+ /**
172
+ * 포지션의 현재 가치와 미실현 손익 계산
173
+ * @param position 포지션 정보
174
+ * @param totalCost 포지션의 총 비용 (OPEN + INCREASE 비용 합계)
175
+ * @param distribution Current market distribution
176
+ * @param market Market parameters
177
+ * @returns 포지션 현재 가치와 미실현 손익
178
+ */
179
+ calculatePositionValue(position, totalCost, distribution, market) {
180
+ // 포지션을 지금 닫았을 때 받을 수 있는 금액 계산
181
+ const closeResult = this.calculateCloseProceeds(position, distribution, market);
182
+ const normalizedTotalCost = MathUtils.formatUSDC(new big_js_1.default(totalCost));
183
+ const currentValue = closeResult.proceeds;
184
+ const unrealizedPnL = currentValue.minus(normalizedTotalCost);
185
+ return {
186
+ currentValue,
187
+ unrealizedPnL,
188
+ averagePrice: closeResult.averagePrice,
189
+ feeAmount: closeResult.feeAmount,
190
+ feeRate: closeResult.feeRate,
191
+ feeInfo: closeResult.feeInfo,
192
+ };
193
+ }
194
+ // ============================================================================
195
+ // INVERSE FUNCTION (역함수: 돈 → 수량)
196
+ // ============================================================================
197
+ /**
198
+ * Sell position의 예상 수익 계산
199
+ * @param position 포지션 정보
200
+ * @param sellQuantity 매도할 수량
201
+ * @param distribution Current market distribution
202
+ * @param market Market parameters
203
+ * @returns 예상 수익
204
+ */
205
+ calculateSellProceeds(position, sellQuantity, distribution, market) {
206
+ const base = this._calcSellProceeds(position.lowerTick, position.upperTick, sellQuantity, position.quantity, distribution, market);
207
+ return {
208
+ proceeds: base.proceeds,
209
+ averagePrice: base.averagePrice,
210
+ feeAmount: MathUtils.formatUSDC(new big_js_1.default(0)),
211
+ feeRate: new big_js_1.default(0),
212
+ feeInfo: {
213
+ policy: types_1.FeePolicyKind.Null,
214
+ name: "NullFeePolicy",
215
+ },
216
+ };
217
+ }
218
+ /**
219
+ * 주어진 총 지출(수수료 포함)으로 살 수 있는 수량 계산 (역산)
220
+ * @param lowerTick Lower tick bound (inclusive)
221
+ * @param upperTick Upper tick bound (exclusive)
222
+ * @param cost 총 지출 한도 (수수료 포함, 6 decimals)
223
+ * @param distribution Current market distribution
224
+ * @param market Market parameters
225
+ * @returns 구매 가능한 수량과 순수 베팅 비용
226
+ */
227
+ // Tick boundary in absolute ticks; internally maps to inclusive bin indices [loBin, hiBin]
228
+ calculateQuantityFromCost(lowerTick, upperTick, cost, distribution, market, includeFees = true) {
229
+ const targetSpend = MathUtils.formatUSDC(new big_js_1.default(cost));
230
+ // 0 또는 음수 입력은 기존 로직과 동일하게 처리
231
+ if (targetSpend.lte(0)) {
232
+ return this._calculateQuantityFromNetCost(lowerTick, upperTick, targetSpend, distribution, market);
233
+ }
234
+ if (!includeFees) {
235
+ return this._calculateQuantityFromNetCost(lowerTick, upperTick, targetSpend, distribution, market);
236
+ }
237
+ const descriptor = market.feePolicyDescriptor?.trim();
238
+ if (!descriptor || descriptor.length === 0) {
239
+ return this._calculateQuantityFromNetCost(lowerTick, upperTick, targetSpend, distribution, market);
240
+ }
241
+ const resolvedPolicy = (0, fees_1.resolveFeePolicyWithMetadata)(descriptor);
242
+ if (resolvedPolicy.descriptor?.policy === "null" ||
243
+ resolvedPolicy.policy === fees_1.NullFeePolicy) {
244
+ return this._calculateQuantityFromNetCost(lowerTick, upperTick, targetSpend, distribution, market);
245
+ }
246
+ const zeroBase = MathUtils.formatUSDC(new big_js_1.default(0));
247
+ const minSpend = this._computeTotalSpendWithFees(zeroBase, zeroBase, lowerTick, upperTick, descriptor);
248
+ if (targetSpend.lt(minSpend)) {
249
+ throw new types_1.ValidationError("Target cost is below the minimum spend achievable after fees");
250
+ }
251
+ let low = new big_js_1.default(0);
252
+ let high = new big_js_1.default(targetSpend);
253
+ // 퍼센트 수수료의 경우 총액을 (1+rate)로 나눠 초기 추정치를 잡아 수렴 속도 개선
254
+ let initialGuess = new big_js_1.default(targetSpend);
255
+ if (resolvedPolicy.descriptor?.policy === "percentage") {
256
+ const bps = new big_js_1.default(resolvedPolicy.descriptor.bps.toString());
257
+ const rate = bps.div(10000);
258
+ const onePlusRate = new big_js_1.default(1).plus(rate);
259
+ initialGuess = targetSpend.div(onePlusRate);
260
+ }
261
+ let netGuess = MathUtils.formatUSDC(initialGuess);
262
+ if (netGuess.lt(0)) {
263
+ netGuess = new big_js_1.default(0);
264
+ }
265
+ let bestResult = this._calculateQuantityFromNetCost(lowerTick, upperTick, netGuess, distribution, market);
266
+ let bestDiff = this._computeTotalSpendWithFees(bestResult.actualCost, bestResult.quantity, lowerTick, upperTick, descriptor).minus(targetSpend);
267
+ if (bestDiff.abs().lte(INVERSE_SPEND_TOLERANCE)) {
268
+ return bestResult;
269
+ }
270
+ if (bestDiff.gt(0)) {
271
+ high = new big_js_1.default(netGuess);
272
+ }
273
+ else {
274
+ low = new big_js_1.default(netGuess);
275
+ }
276
+ for (let i = 0; i < MAX_INVERSE_ITERATIONS; i++) {
277
+ const mid = low.plus(high).div(2);
278
+ const midFormatted = MathUtils.formatUSDC(mid);
279
+ const lowFormatted = MathUtils.formatUSDC(low);
280
+ const highFormatted = MathUtils.formatUSDC(high);
281
+ // 수렴 조건: 더 이상 변화가 없거나 잔여 구간이 tolerance 이하
282
+ if (midFormatted.eq(lowFormatted) ||
283
+ midFormatted.eq(highFormatted) ||
284
+ high.minus(low).abs().lte(INVERSE_SPEND_TOLERANCE)) {
285
+ const seen = new Set();
286
+ [lowFormatted, highFormatted].forEach((boundary) => {
287
+ const key = boundary.toString();
288
+ if (seen.has(key)) {
289
+ return;
290
+ }
291
+ seen.add(key);
292
+ const boundaryCandidate = this._calculateQuantityFromNetCost(lowerTick, upperTick, boundary, distribution, market);
293
+ const boundaryTotal = this._computeTotalSpendWithFees(boundaryCandidate.actualCost, boundaryCandidate.quantity, lowerTick, upperTick, descriptor);
294
+ const boundaryDiff = boundaryTotal.minus(targetSpend);
295
+ if (boundaryDiff.abs().lt(bestDiff.abs())) {
296
+ bestResult = boundaryCandidate;
297
+ bestDiff = boundaryDiff;
298
+ }
299
+ });
300
+ break;
301
+ }
302
+ const candidate = this._calculateQuantityFromNetCost(lowerTick, upperTick, MathUtils.formatUSDC(midFormatted), distribution, market);
303
+ const totalSpend = this._computeTotalSpendWithFees(candidate.actualCost, candidate.quantity, lowerTick, upperTick, descriptor);
304
+ const diff = totalSpend.minus(targetSpend);
305
+ if (diff.abs().lt(bestDiff.abs())) {
306
+ bestResult = candidate;
307
+ bestDiff = diff;
308
+ }
309
+ if (diff.abs().lte(INVERSE_SPEND_TOLERANCE)) {
310
+ bestResult = candidate;
311
+ break;
312
+ }
313
+ if (diff.gt(0)) {
314
+ high = mid;
315
+ }
316
+ else {
317
+ low = mid;
318
+ }
319
+ }
320
+ if (bestDiff.abs().gt(INVERSE_SPEND_TOLERANCE)) {
321
+ throw new types_1.ValidationError("Target cost cannot be achieved with current fee policy");
322
+ }
323
+ return bestResult;
324
+ }
325
+ _calculateQuantityFromNetCost(lowerTick, upperTick, netCost, distribution, market) {
326
+ const costWad = MathUtils.toWad(netCost); // 6→18 dec 변환
327
+ // Convert from input
328
+ const alpha = market.liquidityParameter;
329
+ // Get current state
330
+ const sumBefore = distribution.totalSum;
331
+ const affectedSum = this.getAffectedSum(lowerTick, upperTick, distribution, market);
332
+ if (sumBefore.eq(0)) {
333
+ throw new types_1.CalculationError("Tree not initialized");
334
+ }
335
+ // Direct mathematical inverse:
336
+ // From: C = α * ln(sumAfter / sumBefore)
337
+ // Calculate: q = α * ln(factor)
338
+ // Calculate target sum after: sumAfter = sumBefore * exp(C/α) - safe chunking 사용
339
+ const expValue = MathUtils.safeExp(costWad, alpha);
340
+ const targetSumAfter = MathUtils.wMul(sumBefore, expValue);
341
+ // Calculate required affected sum after trade
342
+ const requiredAffectedSum = targetSumAfter.minus(sumBefore.minus(affectedSum));
343
+ // Calculate factor: newAffectedSum / affectedSum
344
+ if (affectedSum.eq(0)) {
345
+ throw new types_1.CalculationError("Cannot calculate quantity from cost: affected sum is zero. This usually means the tick range is outside the market or the distribution data is empty.");
346
+ }
347
+ const factor = MathUtils.wDiv(requiredAffectedSum, affectedSum);
348
+ // Calculate quantity: q = α * ln(factor)
349
+ const quantityWad = MathUtils.wMul(alpha, MathUtils.wLn(factor));
350
+ // quantityWad는 WAD 형식이므로 WAD를 일반 수로 변환 후 micro USDC로 변환
351
+ const quantityValue = MathUtils.wadToNumber(quantityWad);
352
+ const quantity = quantityValue.mul(MathUtils.USDC_PRECISION); // 일반 수를 micro USDC로 변환
353
+ // 역산 결과 수량이 시장 한계 내에 있는지 검증 (UX 개선)
354
+ this._assertQuantityWithinLimit(quantity, market.liquidityParameter);
355
+ // Verify by calculating actual cost
356
+ // 스케일링 문제 수정으로 이제 안전하게 검증 가능
357
+ let actualCost;
358
+ try {
359
+ const verification = this.calculateOpenCost(lowerTick, upperTick, quantity, distribution, market);
360
+ actualCost = verification.cost;
361
+ }
362
+ catch (error) {
363
+ // 매우 큰 수량이나 극단적인 경우에만 예외 처리
364
+ // 입력 비용을 그대로 사용
365
+ actualCost = netCost;
366
+ console.warn("calculateQuantityFromCost: verification failed, using target cost as approximation", error);
367
+ }
368
+ // Calculate fee information for the final result
369
+ const formattedActualCost = MathUtils.formatUSDC(actualCost);
370
+ const formattedQuantity = MathUtils.formatUSDC(quantity);
371
+ const feeOverlay = this.computeFeeOverlay("BUY", formattedActualCost, formattedQuantity, lowerTick, upperTick, market.feePolicyDescriptor);
372
+ return {
373
+ quantity: formattedQuantity,
374
+ actualCost: formattedActualCost,
375
+ feeAmount: feeOverlay.amount,
376
+ feeRate: feeOverlay.rate,
377
+ feeInfo: feeOverlay.info,
378
+ };
379
+ }
380
+ _computeTotalSpendWithFees(baseAmount, quantity, lowerTick, upperTick, descriptor) {
381
+ const formattedBase = MathUtils.formatUSDC(baseAmount);
382
+ const formattedQuantity = MathUtils.formatUSDC(quantity);
383
+ const feeOverlay = this.computeFeeOverlay("BUY", formattedBase, formattedQuantity, lowerTick, upperTick, descriptor);
384
+ return MathUtils.formatUSDC(formattedBase.plus(feeOverlay.amount));
385
+ }
386
+ /**
387
+ * 주어진 목표 수익(수수료 반영)으로 필요한 매도 수량 역산
388
+ * @param position 보유 포지션 정보
389
+ * @param targetProceeds 수수료 제외 후 실제로 받고 싶은 금액 (6 decimals)
390
+ * @param distribution Current market distribution
391
+ * @param market Market parameters
392
+ * @returns 매도해야 할 수량과 검증된 실제 수익(수수료 제외 전 기준)
393
+ */
394
+ calculateQuantityFromProceeds(position, targetProceeds, distribution, market, includeFees = true) {
395
+ this.validateTickRange(position.lowerTick, position.upperTick, market);
396
+ if (!distribution) {
397
+ throw new types_1.ValidationError("Distribution data is required but was undefined");
398
+ }
399
+ if (new big_js_1.default(position.quantity).lte(0)) {
400
+ throw new types_1.ValidationError("Position quantity must be positive");
401
+ }
402
+ if (new big_js_1.default(targetProceeds).lte(0)) {
403
+ throw new types_1.ValidationError("Target proceeds must be positive");
404
+ }
405
+ const maxDecrease = this.calculateDecreaseProceeds(position, position.quantity, distribution, market);
406
+ const targetAmount = MathUtils.formatUSDC(new big_js_1.default(targetProceeds));
407
+ const maxBaseProceeds = MathUtils.formatUSDC(maxDecrease.proceeds);
408
+ if (!includeFees) {
409
+ if (targetAmount.gt(maxBaseProceeds)) {
410
+ throw new types_1.ValidationError("Target proceeds exceed the maximum proceeds available for this position");
411
+ }
412
+ return this._calculateQuantityFromBaseProceeds(position, targetAmount, distribution, market);
413
+ }
414
+ const descriptor = market.feePolicyDescriptor?.trim();
415
+ const targetNetProceeds = targetAmount;
416
+ if (!descriptor || descriptor.length === 0) {
417
+ if (targetNetProceeds.gt(maxBaseProceeds)) {
418
+ throw new types_1.ValidationError("Target proceeds exceed the maximum proceeds available for this position");
419
+ }
420
+ return this._calculateQuantityFromBaseProceeds(position, targetNetProceeds, distribution, market);
421
+ }
422
+ const maxNetProceeds = MathUtils.formatUSDC(maxDecrease.proceeds.minus(maxDecrease.feeAmount));
423
+ if (targetNetProceeds.gt(maxNetProceeds)) {
424
+ throw new types_1.ValidationError("Target proceeds exceed the maximum net proceeds available for this position");
425
+ }
426
+ const resolvedPolicy = (0, fees_1.resolveFeePolicyWithMetadata)(descriptor);
427
+ if (resolvedPolicy.descriptor?.policy === "null" ||
428
+ resolvedPolicy.policy === fees_1.NullFeePolicy) {
429
+ return this._calculateQuantityFromBaseProceeds(position, targetNetProceeds, distribution, market);
430
+ }
431
+ let lowBound = new big_js_1.default(targetNetProceeds);
432
+ let highBound = new big_js_1.default(maxBaseProceeds);
433
+ if (lowBound.gt(highBound)) {
434
+ lowBound = new big_js_1.default(highBound);
435
+ }
436
+ let initialGuess = new big_js_1.default(targetNetProceeds);
437
+ const parsedDescriptor = resolvedPolicy.descriptor;
438
+ if (parsedDescriptor?.policy === "percentage") {
439
+ const bps = new big_js_1.default(parsedDescriptor.bps.toString());
440
+ const rate = bps.div(10000);
441
+ const denominator = new big_js_1.default(1).minus(rate);
442
+ if (denominator.gt(0)) {
443
+ const derived = targetNetProceeds.div(denominator);
444
+ if (derived.gt(initialGuess)) {
445
+ initialGuess = derived;
446
+ }
447
+ }
448
+ else {
449
+ initialGuess = new big_js_1.default(highBound);
450
+ }
451
+ }
452
+ if (initialGuess.gt(highBound)) {
453
+ initialGuess = new big_js_1.default(highBound);
454
+ }
455
+ if (initialGuess.lt(lowBound)) {
456
+ initialGuess = new big_js_1.default(lowBound);
457
+ }
458
+ let baseGuess = MathUtils.formatUSDC(initialGuess);
459
+ let bestResult = this._calculateQuantityFromBaseProceeds(position, baseGuess, distribution, market);
460
+ let bestNet = this._computeNetProceedsAfterFees(bestResult.actualProceeds, bestResult.quantity, position.lowerTick, position.upperTick, descriptor);
461
+ let bestDiff = bestNet.minus(targetNetProceeds);
462
+ if (bestDiff.abs().lte(INVERSE_SPEND_TOLERANCE)) {
463
+ return bestResult;
464
+ }
465
+ const adjustBounds = (candidateBase, diff) => {
466
+ if (diff.gt(0)) {
467
+ highBound = candidateBase;
468
+ }
469
+ else {
470
+ lowBound = candidateBase;
471
+ }
472
+ };
473
+ adjustBounds(new big_js_1.default(bestResult.actualProceeds), bestDiff);
474
+ for (let i = 0; i < MAX_INVERSE_ITERATIONS; i++) {
475
+ const mid = lowBound.plus(highBound).div(2);
476
+ const midFormatted = MathUtils.formatUSDC(mid);
477
+ const lowFormatted = MathUtils.formatUSDC(lowBound);
478
+ const highFormatted = MathUtils.formatUSDC(highBound);
479
+ if (midFormatted.eq(lowFormatted) ||
480
+ midFormatted.eq(highFormatted) ||
481
+ highBound.minus(lowBound).abs().lte(INVERSE_SPEND_TOLERANCE)) {
482
+ const seen = new Set();
483
+ [lowFormatted, highFormatted].forEach((boundary) => {
484
+ const key = boundary.toString();
485
+ if (seen.has(key)) {
486
+ return;
487
+ }
488
+ seen.add(key);
489
+ const boundaryCandidate = this._calculateQuantityFromBaseProceeds(position, boundary, distribution, market);
490
+ const boundaryNet = this._computeNetProceedsAfterFees(boundaryCandidate.actualProceeds, boundaryCandidate.quantity, position.lowerTick, position.upperTick, descriptor);
491
+ const boundaryDiff = boundaryNet.minus(targetNetProceeds);
492
+ if (boundaryDiff.abs().lt(bestDiff.abs())) {
493
+ bestResult = boundaryCandidate;
494
+ bestDiff = boundaryDiff;
495
+ }
496
+ });
497
+ break;
498
+ }
499
+ const candidate = this._calculateQuantityFromBaseProceeds(position, MathUtils.formatUSDC(midFormatted), distribution, market);
500
+ const candidateNet = this._computeNetProceedsAfterFees(candidate.actualProceeds, candidate.quantity, position.lowerTick, position.upperTick, descriptor);
501
+ const diff = candidateNet.minus(targetNetProceeds);
502
+ if (diff.abs().lt(bestDiff.abs())) {
503
+ bestResult = candidate;
504
+ bestDiff = diff;
505
+ }
506
+ if (diff.abs().lte(INVERSE_SPEND_TOLERANCE)) {
507
+ bestResult = candidate;
508
+ break;
509
+ }
510
+ adjustBounds(new big_js_1.default(candidate.actualProceeds), diff);
511
+ }
512
+ if (bestDiff.abs().gt(INVERSE_SPEND_TOLERANCE)) {
513
+ throw new types_1.ValidationError("Target proceeds cannot be achieved with current fee policy");
514
+ }
515
+ return bestResult;
516
+ }
517
+ // ============================================================================
518
+ // HELPER FUNCTIONS
519
+ // ============================================================================
520
+ /**
521
+ * 시장별 최대 수량 한계 검증 (컨트랙트와 동일한 제한)
522
+ * @param quantity 검증할 수량 (6 decimals)
523
+ * @param alpha 유동성 파라미터 α (18 decimals WAD)
524
+ * @throws Error if quantity exceeds market limit
525
+ */
526
+ _assertQuantityWithinLimit(quantity, alpha) {
527
+ const maxChunk = MathUtils.maxSafeChunkQuantity(alpha);
528
+ const maxQtyWad = maxChunk.mul(MathUtils.MAX_CHUNKS_PER_TX);
529
+ // quantity는 이미 micro-USDC(6 decimals) 정수이므로 바로 WAD로 변환
530
+ const qtyWad = MathUtils.toWad(quantity);
531
+ if (qtyWad.gt(maxQtyWad)) {
532
+ const maxQtyFormatted = MathUtils.wadToNumber(maxQtyWad);
533
+ throw new types_1.ValidationError(`Quantity too large. Max per trade = ${maxQtyFormatted.toString()} USDC (market limit: maxSafeChunk × ${MathUtils.MAX_CHUNKS_PER_TX})`);
534
+ }
535
+ }
536
+ _calculateTradeCostWad(lowerTick, upperTick, quantityWad, distribution, market) {
537
+ const alpha = market.liquidityParameter;
538
+ const sumBefore = distribution.totalSum;
539
+ const affectedSum = this.getAffectedSum(lowerTick, upperTick, distribution, market);
540
+ if (quantityWad.eq(0)) {
541
+ return new big_js_1.default(0);
542
+ }
543
+ if (sumBefore.eq(0)) {
544
+ throw new types_1.CalculationError("Tree not initialized");
545
+ }
546
+ const maxSafeChunk = MathUtils.maxSafeChunkQuantity(alpha);
547
+ if (quantityWad.lte(maxSafeChunk)) {
548
+ return this._calculateSingleTradeCostWad(sumBefore, affectedSum, quantityWad, alpha);
549
+ }
550
+ return this._calculateTradeCostChunked(sumBefore, affectedSum, quantityWad, alpha);
551
+ }
552
+ _calculateSingleTradeCostWad(sumBefore, affectedSum, quantityWad, alpha) {
553
+ const factor = MathUtils.safeExp(quantityWad, alpha);
554
+ const factorBI = MathUtils.toBigInt(factor, "factor");
555
+ const affectedBI = MathUtils.toBigInt(affectedSum, "affectedSum");
556
+ if (factorBI !== 0n && affectedBI > MathUtils.MAX_UINT256 / factorBI) {
557
+ return this._calculateTradeCostChunked(sumBefore, affectedSum, quantityWad, alpha);
558
+ }
559
+ const sumAfter = sumBefore
560
+ .minus(affectedSum)
561
+ .plus(MathUtils.wMulNearest(affectedSum, factor));
562
+ if (sumAfter.lte(sumBefore)) {
563
+ return new big_js_1.default(0);
564
+ }
565
+ const ratio = MathUtils.wDivUp(sumAfter, sumBefore);
566
+ return MathUtils.wMul(alpha, MathUtils.wLn(ratio));
567
+ }
568
+ _calculateTradeCostChunked(sumBefore, affectedSum, quantityWad, alpha) {
569
+ const maxSafeChunk = MathUtils.maxSafeChunkQuantity(alpha);
570
+ const totalBI = MathUtils.toBigInt(quantityWad, "quantityWad");
571
+ const maxSafeBI = MathUtils.toBigInt(maxSafeChunk, "maxSafeChunk");
572
+ if (maxSafeBI === 0n) {
573
+ throw new types_1.CalculationError("Max safe chunk is zero");
574
+ }
575
+ const requiredChunks = (totalBI + maxSafeBI - 1n) / maxSafeBI;
576
+ if (requiredChunks > BigInt(MathUtils.MAX_CHUNKS_PER_TX)) {
577
+ throw new types_1.ValidationError(`Chunk limit exceeded: ${requiredChunks.toString()} > ${MathUtils.MAX_CHUNKS_PER_TX}`);
578
+ }
579
+ let cumulativeCost = new big_js_1.default(0);
580
+ let remaining = new big_js_1.default(quantityWad.toString());
581
+ let currentSumBefore = new big_js_1.default(sumBefore.toString());
582
+ let currentAffectedSum = new big_js_1.default(affectedSum.toString());
583
+ let chunkCount = 0;
584
+ while (remaining.gt(0) && chunkCount < MathUtils.MAX_CHUNKS_PER_TX) {
585
+ let chunkQuantity = remaining.gt(maxSafeChunk)
586
+ ? maxSafeChunk
587
+ : remaining;
588
+ let factor = MathUtils.safeExp(chunkQuantity, alpha);
589
+ let factorBI = MathUtils.toBigInt(factor, "factor");
590
+ const affectedBI = MathUtils.toBigInt(currentAffectedSum, "affectedSum");
591
+ if (factorBI !== 0n && affectedBI > MathUtils.MAX_UINT256 / factorBI) {
592
+ chunkQuantity = this._computeSafeChunk(currentAffectedSum, alpha, remaining, BigInt(MathUtils.MAX_CHUNKS_PER_TX - chunkCount));
593
+ if (chunkQuantity.gt(remaining)) {
594
+ chunkQuantity = remaining;
595
+ }
596
+ factor = MathUtils.safeExp(chunkQuantity, alpha);
597
+ factorBI = MathUtils.toBigInt(factor, "factor");
598
+ }
599
+ if (!currentAffectedSum.eq(0) &&
600
+ factorBI > MathUtils.MAX_UINT256 / affectedBI) {
601
+ throw new types_1.CalculationError("MathMulOverflow");
602
+ }
603
+ const newAffectedSum = MathUtils.wMulNearest(currentAffectedSum, factor);
604
+ const sumAfter = currentSumBefore
605
+ .minus(currentAffectedSum)
606
+ .plus(newAffectedSum);
607
+ if (sumAfter.lte(currentSumBefore)) {
608
+ throw new types_1.CalculationError("NonIncreasingSum");
609
+ }
610
+ const ratio = MathUtils.wDivUp(sumAfter, currentSumBefore);
611
+ const chunkCost = MathUtils.wMul(alpha, MathUtils.wLn(ratio));
612
+ cumulativeCost = cumulativeCost.plus(chunkCost);
613
+ if (chunkQuantity.eq(0)) {
614
+ throw new types_1.CalculationError("NoChunkProgress");
615
+ }
616
+ currentSumBefore = sumAfter;
617
+ currentAffectedSum = newAffectedSum;
618
+ remaining = remaining.minus(chunkQuantity);
619
+ chunkCount++;
620
+ }
621
+ if (!remaining.eq(0)) {
622
+ throw new types_1.CalculationError(`Residual quantity: ${remaining.toFixed(0, big_js_1.default.roundDown)}`);
623
+ }
624
+ return cumulativeCost;
625
+ }
626
+ _calculateSellProceedsWad(lowerTick, upperTick, quantityWad, distribution, market) {
627
+ const alpha = market.liquidityParameter;
628
+ const sumBefore = distribution.totalSum;
629
+ const affectedSum = this.getAffectedSum(lowerTick, upperTick, distribution, market);
630
+ if (quantityWad.eq(0)) {
631
+ return new big_js_1.default(0);
632
+ }
633
+ if (sumBefore.eq(0)) {
634
+ throw new types_1.CalculationError("Tree not initialized");
635
+ }
636
+ const maxSafeChunk = MathUtils.maxSafeChunkQuantity(alpha);
637
+ if (quantityWad.lte(maxSafeChunk)) {
638
+ return this._calculateSingleSellProceedsWad(sumBefore, affectedSum, quantityWad, alpha);
639
+ }
640
+ return this._calculateSellProceedsChunked(sumBefore, affectedSum, quantityWad, alpha);
641
+ }
642
+ _calculateSingleSellProceedsWad(sumBefore, affectedSum, quantityWad, alpha) {
643
+ const factor = MathUtils.safeExp(quantityWad, alpha);
644
+ const inverseFactor = MathUtils.wDivUp(MathUtils.WAD, factor);
645
+ const inverseBI = MathUtils.toBigInt(inverseFactor, "inverseFactor");
646
+ const affectedBI = MathUtils.toBigInt(affectedSum, "affectedSum");
647
+ if (inverseBI !== 0n && affectedBI > MathUtils.MAX_UINT256 / inverseBI) {
648
+ return this._calculateSellProceedsChunked(sumBefore, affectedSum, quantityWad, alpha);
649
+ }
650
+ const sumAfter = sumBefore
651
+ .minus(affectedSum)
652
+ .plus(MathUtils.wMulNearest(affectedSum, inverseFactor));
653
+ if (sumAfter.eq(0)) {
654
+ throw new types_1.CalculationError("Sum after sell is zero");
655
+ }
656
+ if (sumBefore.lte(sumAfter)) {
657
+ return new big_js_1.default(0);
658
+ }
659
+ const ratio = MathUtils.wDivUp(sumBefore, sumAfter);
660
+ return MathUtils.wMul(alpha, MathUtils.wLn(ratio));
661
+ }
662
+ _calculateSellProceedsChunked(sumBefore, affectedSum, quantityWad, alpha) {
663
+ const maxSafeChunk = MathUtils.maxSafeChunkQuantity(alpha);
664
+ const totalBI = MathUtils.toBigInt(quantityWad, "quantityWad");
665
+ const maxSafeBI = MathUtils.toBigInt(maxSafeChunk, "maxSafeChunk");
666
+ if (maxSafeBI === 0n) {
667
+ throw new types_1.CalculationError("Max safe chunk is zero");
668
+ }
669
+ const requiredChunks = (totalBI + maxSafeBI - 1n) / maxSafeBI;
670
+ if (requiredChunks > BigInt(MathUtils.MAX_CHUNKS_PER_TX)) {
671
+ throw new types_1.ValidationError(`Chunk limit exceeded: ${requiredChunks.toString()} > ${MathUtils.MAX_CHUNKS_PER_TX}`);
672
+ }
673
+ let cumulativeProceeds = new big_js_1.default(0);
674
+ let remaining = new big_js_1.default(quantityWad.toString());
675
+ let currentSumBefore = new big_js_1.default(sumBefore.toString());
676
+ let currentAffectedSum = new big_js_1.default(affectedSum.toString());
677
+ let chunkCount = 0;
678
+ while (remaining.gt(0) && chunkCount < MathUtils.MAX_CHUNKS_PER_TX) {
679
+ let chunkQuantity = remaining.gt(maxSafeChunk)
680
+ ? maxSafeChunk
681
+ : remaining;
682
+ let factor = MathUtils.safeExp(chunkQuantity, alpha);
683
+ let inverseFactor = MathUtils.wDivUp(MathUtils.WAD, factor);
684
+ let inverseBI = MathUtils.toBigInt(inverseFactor, "inverseFactor");
685
+ const affectedBI = MathUtils.toBigInt(currentAffectedSum, "affectedSum");
686
+ if (inverseBI !== 0n && affectedBI > MathUtils.MAX_UINT256 / inverseBI) {
687
+ chunkQuantity = this._computeSafeChunk(currentAffectedSum, alpha, remaining, BigInt(MathUtils.MAX_CHUNKS_PER_TX - chunkCount));
688
+ if (chunkQuantity.gt(remaining)) {
689
+ chunkQuantity = remaining;
690
+ }
691
+ factor = MathUtils.safeExp(chunkQuantity, alpha);
692
+ inverseFactor = MathUtils.wDivUp(MathUtils.WAD, factor);
693
+ inverseBI = MathUtils.toBigInt(inverseFactor, "inverseFactor");
694
+ }
695
+ if (!currentAffectedSum.eq(0) &&
696
+ inverseBI > MathUtils.MAX_UINT256 / affectedBI) {
697
+ throw new types_1.CalculationError("MathMulOverflow");
698
+ }
699
+ const newAffectedSum = MathUtils.wMulNearest(currentAffectedSum, inverseFactor);
700
+ const sumAfter = currentSumBefore
701
+ .minus(currentAffectedSum)
702
+ .plus(newAffectedSum);
703
+ if (sumAfter.eq(0)) {
704
+ throw new types_1.CalculationError("Sum after sell is zero");
705
+ }
706
+ if (sumBefore.lte(sumAfter)) {
707
+ return new big_js_1.default(0);
708
+ }
709
+ const ratio = MathUtils.wDivUp(currentSumBefore, sumAfter);
710
+ const chunkProceeds = MathUtils.wMul(alpha, MathUtils.wLn(ratio));
711
+ cumulativeProceeds = cumulativeProceeds.plus(chunkProceeds);
712
+ if (chunkQuantity.eq(0)) {
713
+ throw new types_1.CalculationError("NoChunkProgress");
714
+ }
715
+ currentSumBefore = sumAfter;
716
+ currentAffectedSum = newAffectedSum;
717
+ remaining = remaining.minus(chunkQuantity);
718
+ chunkCount++;
719
+ }
720
+ if (!remaining.eq(0)) {
721
+ throw new types_1.CalculationError(`Residual quantity: ${remaining.toFixed(0, big_js_1.default.roundDown)}`);
722
+ }
723
+ return cumulativeProceeds;
724
+ }
725
+ _computeSafeChunk(currentSum, alpha, remainingQty, chunksLeft) {
726
+ if (chunksLeft === 0n) {
727
+ return remainingQty;
728
+ }
729
+ const remainingBI = MathUtils.toBigInt(remainingQty, "remainingQty");
730
+ const minProgress = (remainingBI + chunksLeft - 1n) / chunksLeft;
731
+ let maxSafeQuantity = MathUtils.wMul(alpha, MathUtils.MAX_EXP_INPUT_WAD);
732
+ const guard = MathUtils.wMul(alpha, OVERFLOW_GUARD_MULTIPLIER);
733
+ if (MathUtils.toBigInt(currentSum, "currentSum") >
734
+ MathUtils.toBigInt(guard, "overflowGuard")) {
735
+ maxSafeQuantity = MathUtils.fromBigInt(MathUtils.toBigInt(alpha, "alpha") / 10n);
736
+ }
737
+ const maxSafeBI = MathUtils.toBigInt(maxSafeQuantity, "maxSafeQuantity");
738
+ let safeChunk = minProgress < maxSafeBI ? minProgress : maxSafeBI;
739
+ if (safeChunk > remainingBI) {
740
+ safeChunk = remainingBI;
741
+ }
742
+ return MathUtils.fromBigInt(safeChunk);
743
+ }
744
+ /**
745
+ * 내부 헬퍼: 매도 수익 계산 (코드 중복 제거)
746
+ * @param lowerTick Lower tick bound (inclusive)
747
+ * @param upperTick Upper tick bound (exclusive)
748
+ * @param sellQuantity 매도할 수량
749
+ * @param positionQuantity 현재 포지션 수량 (검증용)
750
+ * @param distribution Current market distribution
751
+ * @param market Market parameters
752
+ * @returns 매도 수익
753
+ */
754
+ // Tick boundary in absolute ticks; internally maps to inclusive bin indices [loBin, hiBin]
755
+ _calculateQuantityFromBaseProceeds(position, baseProceeds, distribution, market) {
756
+ const alpha = market.liquidityParameter;
757
+ const proceedsWad = MathUtils.toWad(baseProceeds);
758
+ const sumBefore = distribution.totalSum;
759
+ const affectedSum = this.getAffectedSum(position.lowerTick, position.upperTick, distribution, market);
760
+ if (sumBefore.eq(0)) {
761
+ throw new types_1.CalculationError("Tree not initialized");
762
+ }
763
+ if (affectedSum.eq(0)) {
764
+ throw new types_1.CalculationError("Cannot calculate quantity from proceeds: affected sum is zero. This usually means the tick range is outside the market or the distribution data is empty.");
765
+ }
766
+ const expProceeds = MathUtils.safeExp(proceedsWad, alpha);
767
+ const targetSumAfter = MathUtils.wDiv(sumBefore, expProceeds);
768
+ const unaffectedSum = sumBefore.minus(affectedSum);
769
+ if (targetSumAfter.lt(unaffectedSum)) {
770
+ throw new types_1.ValidationError("Target proceeds require selling more than the position holds");
771
+ }
772
+ const requiredAffectedSumAfter = targetSumAfter.minus(unaffectedSum);
773
+ if (requiredAffectedSumAfter.lte(0)) {
774
+ throw new types_1.ValidationError("Target proceeds would reduce the affected sum to zero or negative");
775
+ }
776
+ if (requiredAffectedSumAfter.gt(affectedSum)) {
777
+ throw new types_1.CalculationError("Target proceeds require increasing the affected sum, which is impossible for a sale");
778
+ }
779
+ const inverseFactor = MathUtils.wDiv(requiredAffectedSumAfter, affectedSum);
780
+ if (inverseFactor.lte(0) || inverseFactor.gt(MathUtils.WAD)) {
781
+ throw new types_1.CalculationError("Inverse factor out of bounds when calculating sell quantity");
782
+ }
783
+ const factor = MathUtils.wDiv(MathUtils.WAD, inverseFactor);
784
+ const quantityWad = MathUtils.wMul(alpha, MathUtils.wLn(factor));
785
+ const quantityValue = MathUtils.wadToNumber(quantityWad);
786
+ const quantity = quantityValue.mul(MathUtils.USDC_PRECISION);
787
+ this._assertQuantityWithinLimit(quantity, alpha);
788
+ let formattedQuantity = MathUtils.formatUSDC(quantity);
789
+ if (formattedQuantity.gt(position.quantity)) {
790
+ formattedQuantity = MathUtils.formatUSDC(position.quantity);
791
+ }
792
+ let actualProceeds;
793
+ try {
794
+ const verification = this._calcSellProceeds(position.lowerTick, position.upperTick, formattedQuantity, position.quantity, distribution, market);
795
+ actualProceeds = verification.proceeds;
796
+ }
797
+ catch (error) {
798
+ actualProceeds = baseProceeds;
799
+ console.warn("calculateQuantityFromProceeds: verification failed, using target proceeds as approximation", error);
800
+ }
801
+ // Calculate fee information
802
+ const feeOverlay = this.computeFeeOverlay("SELL", actualProceeds, formattedQuantity, position.lowerTick, position.upperTick, market.feePolicyDescriptor);
803
+ return {
804
+ quantity: formattedQuantity,
805
+ actualProceeds: MathUtils.formatUSDC(actualProceeds),
806
+ feeAmount: feeOverlay.amount,
807
+ feeRate: feeOverlay.rate,
808
+ feeInfo: feeOverlay.info,
809
+ };
810
+ }
811
+ _calcSellProceeds(lowerTick, upperTick, sellQuantity, positionQuantity, distribution, market) {
812
+ this.validateTickRange(lowerTick, upperTick, market);
813
+ // Input validation
814
+ if (new big_js_1.default(sellQuantity).lte(0)) {
815
+ throw new types_1.ValidationError("Sell quantity must be positive");
816
+ }
817
+ if (new big_js_1.default(sellQuantity).gt(positionQuantity)) {
818
+ throw new types_1.ValidationError("Cannot sell more than current position");
819
+ }
820
+ // 시장별 최대 수량 검증 (UX 개선)
821
+ this._assertQuantityWithinLimit(sellQuantity, market.liquidityParameter);
822
+ const quantityWad = MathUtils.toWad(sellQuantity);
823
+ const proceedsWad = this._calculateSellProceedsWad(lowerTick, upperTick, quantityWad, distribution, market);
824
+ const proceeds = MathUtils.fromWad(proceedsWad);
825
+ // Calculate average price with proper formatting
826
+ const averagePrice = proceeds.div(sellQuantity);
827
+ const formattedAveragePrice = new big_js_1.default(averagePrice.toFixed(6, big_js_1.default.roundDown)); // 6자리 정밀도로 충분
828
+ return {
829
+ proceeds: MathUtils.formatUSDC(proceeds),
830
+ averagePrice: formattedAveragePrice,
831
+ };
832
+ }
833
+ _computeNetProceedsAfterFees(baseProceeds, quantity, lowerTick, upperTick, descriptor) {
834
+ const formattedBase = MathUtils.formatUSDC(baseProceeds);
835
+ const formattedQuantity = MathUtils.formatUSDC(quantity);
836
+ const feeOverlay = this.computeFeeOverlay("SELL", formattedBase, formattedQuantity, lowerTick, upperTick, descriptor);
837
+ return MathUtils.formatUSDC(formattedBase.minus(feeOverlay.amount));
838
+ }
839
+ computeFeeOverlay(side, baseAmount, quantity, lowerTick, upperTick, descriptor) {
840
+ const makeZeroOverlay = (descriptorString, policyName) => ({
841
+ amount: MathUtils.formatUSDC(new big_js_1.default(0)),
842
+ rate: new big_js_1.default(0),
843
+ info: {
844
+ policy: types_1.FeePolicyKind.Null,
845
+ ...(descriptorString ? { descriptor: descriptorString } : {}),
846
+ name: policyName ?? "NullFeePolicy",
847
+ },
848
+ });
849
+ if (!descriptor || descriptor.trim().length === 0) {
850
+ return makeZeroOverlay();
851
+ }
852
+ const resolved = (0, fees_1.resolveFeePolicyWithMetadata)(descriptor);
853
+ const baseAmountInt = bigToBigInt(baseAmount);
854
+ const quantityInt = bigToBigInt(quantity);
855
+ const trader = ZERO_ADDRESS;
856
+ const marketId = 0;
857
+ const context = ZERO_CONTEXT;
858
+ const feeBigInt = side === "BUY"
859
+ ? (0, fees_1.quoteOpenFee)(resolved.policy, {
860
+ trader,
861
+ marketId,
862
+ lowerTick,
863
+ upperTick,
864
+ quantity6: quantityInt,
865
+ cost6: baseAmountInt,
866
+ context,
867
+ })
868
+ : (0, fees_1.quoteSellFee)(resolved.policy, {
869
+ trader,
870
+ marketId,
871
+ lowerTick,
872
+ upperTick,
873
+ sellQuantity6: quantityInt,
874
+ proceeds6: baseAmountInt,
875
+ context,
876
+ });
877
+ const feeAmount = MathUtils.formatUSDC(new big_js_1.default(feeBigInt.toString()));
878
+ const parsedDescriptor = resolved.descriptor;
879
+ const descriptorString = parsedDescriptor?.descriptor ?? descriptor;
880
+ const policyName = parsedDescriptor?.name ??
881
+ (typeof resolved.policy.name === "string"
882
+ ? resolved.policy.name
883
+ : undefined);
884
+ if (!descriptorString || descriptorString.length === 0) {
885
+ return makeZeroOverlay();
886
+ }
887
+ if (parsedDescriptor?.policy === "null" || resolved.policy === fees_1.NullFeePolicy) {
888
+ return {
889
+ amount: feeAmount,
890
+ rate: new big_js_1.default(0),
891
+ info: {
892
+ policy: types_1.FeePolicyKind.Null,
893
+ descriptor: descriptorString,
894
+ name: policyName ?? "NullFeePolicy",
895
+ },
896
+ };
897
+ }
898
+ if (parsedDescriptor?.policy === "percentage") {
899
+ const bps = new big_js_1.default(parsedDescriptor.bps.toString());
900
+ const rate = bps.div(new big_js_1.default("10000"));
901
+ return {
902
+ amount: feeAmount,
903
+ rate,
904
+ info: {
905
+ policy: types_1.FeePolicyKind.Percentage,
906
+ descriptor: descriptorString,
907
+ name: policyName,
908
+ bps,
909
+ },
910
+ };
911
+ }
912
+ const effectiveRate = baseAmount.gt(0) && feeAmount.gt(0)
913
+ ? feeAmount.div(baseAmount)
914
+ : new big_js_1.default(0);
915
+ return {
916
+ amount: feeAmount,
917
+ rate: effectiveRate,
918
+ info: {
919
+ policy: types_1.FeePolicyKind.Custom,
920
+ descriptor: descriptorString,
921
+ name: policyName,
922
+ },
923
+ };
924
+ }
925
+ validateTickRange(lowerTick, upperTick, market) {
926
+ if (lowerTick >= upperTick) {
927
+ throw new types_1.ValidationError("Lower tick must be less than upper tick");
928
+ }
929
+ const maxUpperTick = market.maxTick + market.tickSpacing;
930
+ if (lowerTick < market.minTick || upperTick > maxUpperTick) {
931
+ throw new types_1.ValidationError("Tick range is out of market bounds");
932
+ }
933
+ if ((lowerTick - market.minTick) % market.tickSpacing !== 0) {
934
+ throw new types_1.ValidationError("Lower tick is not aligned to tick spacing");
935
+ }
936
+ if ((upperTick - market.minTick) % market.tickSpacing !== 0) {
937
+ throw new types_1.ValidationError("Upper tick is not aligned to tick spacing");
938
+ }
939
+ }
940
+ getAffectedSum(lowerTick, upperTick, distribution, market) {
941
+ // 입력 데이터 검증
942
+ if (!distribution) {
943
+ throw new types_1.ValidationError("Distribution data is required but was undefined");
944
+ }
945
+ if (!distribution.binFactors) {
946
+ throw new types_1.ValidationError("binFactors is required but was undefined. Make sure to include 'binFactors' field in your GraphQL query and use mapDistribution() to convert the data.");
947
+ }
948
+ if (!Array.isArray(distribution.binFactors)) {
949
+ throw new types_1.ValidationError("binFactors must be an array");
950
+ }
951
+ // 컨트랙트와 동일한 _rangeToBins 로직 사용
952
+ const lowerBin = Math.floor((lowerTick - market.minTick) / market.tickSpacing);
953
+ const upperBin = Math.floor((upperTick - market.minTick) / market.tickSpacing - 1);
954
+ let affectedSum = new big_js_1.default(0);
955
+ // 컨트랙트와 동일하게 inclusive 범위로 계산 (lowerBin <= binIndex <= upperBin)
956
+ for (let binIndex = lowerBin; binIndex <= upperBin; binIndex++) {
957
+ if (binIndex >= 0 && binIndex < distribution.binFactors.length) {
958
+ // 이미 WAD 형식의 Big 객체이므로 직접 사용
959
+ affectedSum = affectedSum.plus(distribution.binFactors[binIndex]);
960
+ }
961
+ }
962
+ return affectedSum;
963
+ }
964
+ }
965
+ exports.CLMSRSDK = CLMSRSDK;
966
+ // ============================================================================
967
+ // CONVENIENCE FUNCTIONS
968
+ // ============================================================================
969
+ /**
970
+ * Create CLMSR SDK instance
971
+ */
972
+ function createCLMSRSDK() {
973
+ return new CLMSRSDK();
974
+ }
975
+ /**
976
+ * Create Signals SDK instance (v1 alias)
977
+ */
978
+ function createSignalsSDK() {
979
+ return new CLMSRSDK();
980
+ }