@sundaeswap/math 0.5.0 → 0.6.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.
@@ -1,181 +1,649 @@
1
1
  import { AssetAmount } from "@sundaeswap/asset";
2
2
  import { Fraction } from "@sundaeswap/fraction";
3
- import * as ConstantProductPool from "./ConstantProductPool.js";
4
3
 
5
4
  /**
6
- * Holds the calculated outcome of a concentrated-liquidity swap. Mirrors
7
- * {@link ConstantProductPool.TSwapOutcome} so it flows through the same generic
8
- * swap-outcome consumers.
5
+ * Holds the calculated outcome of a concentrated-liquidity swap. It has the
6
+ * same shape as `ConstantProductPool.TSwapOutcome`, so the generic
7
+ * swap-outcome consumers accept it.
9
8
  */
10
9
 
11
- /** A sqrt-price bound from the CL config, as an exact rational `[num, den]`. */
12
-
13
10
  /**
14
- * Concentrated liquidity is Sundae v4's single-range invariant. Unlike Uniswap
15
- * v3 there are no per-position ranges or ticks: the whole pool shares ONE
16
- * immutable price range `[sqrtPriceA, sqrtPriceB]` (a < b) set at creation, and
17
- * LP is a single fungible token. Swaps run on *virtual* reserves that fold the
18
- * pool's liquidity `L` (its total LP supply) into each side:
11
+ * Concentrated liquidity: Sundae v4's ladder invariant.
12
+ *
13
+ * A pool is a ladder of bands in ascending sqrt-price order. Each band is a
14
+ * concentrated-liquidity arc (`curve` 0) or a constant-sum bin (`curve` 1)
15
+ * with its own share of the pool's liquidity (`weight / weightTotal`) and
16
+ * its own two fees. LP is one fungible token for the whole ladder; a
17
+ * deposit or withdrawal scales every band.
19
18
  *
20
- * VA = a·spbNum + L·spbDen
21
- * VB = b·spaDen + L·spaNum
19
+ * The chain never stores the price. It stores reserves, and the state is a
20
+ * WITNESS `(X, k)`: the ladder counter `X` (the pool's `total_lp`, spec V12)
21
+ * and the active band `k`, which the reserves must prove. Everything here
22
+ * mirrors `lib/modules/cl_check.ak` and the scooper's ladder
23
+ * math, rounding included; where they disagree the chain wins
24
+ * and the transaction fails. The rounding is not negotiable:
22
25
  *
23
- * where `a`/`b` are the raw assetA/assetB reserves. This is a direct port of the
24
- * scooper's `cl_swap_result` (scooper-v2/src/sundaev4/swap_math.rs) and the
25
- * on-chain `cl_check_swap` (sundae-v4/lib/modules/cl_check.ak) — those are the
26
- * source of truth; keep this in lockstep with them.
26
+ * - `L_k` floors; saturation ceils, in ONE division;
27
+ * - the band fee ceils, per step, on the step's real input;
28
+ * - the prefix sums come from the LADDER INDEX (cumulative coefficients
29
+ * in fixed point at 2^64, spec §2.3), multiplied by the counter and
30
+ * ceiled ONCE. A per-band sum of ceilings is a different number and the
31
+ * chain rejects it.
32
+ */
33
+
34
+ /** An exact rational `num / den`. */
35
+
36
+ /** One band of the ladder. */
37
+
38
+ /** A ladder: bands, the top edge, and the sum of the weights. */
39
+
40
+ /** The witness the chain checks: the ladder counter and the active band. */
41
+
42
+ /** 2^64, the fixed-point scale of the ladder index. */
43
+ export const FIXED_ONE = 1n << 64n;
44
+
45
+ /**
46
+ * The least counter per unit of weight a ladder may be created with.
47
+ * Mirrors `cl_check.min_counter_per_weight` (sundae-v4 #229, SV4-007):
48
+ * Create requires `total_lp >= MIN_COUNTER_PER_WEIGHT * weightTotal`.
49
+ */
50
+ export const MIN_COUNTER_PER_WEIGHT = 1000n;
51
+
52
+ /** The least counter (initial `total_lp`) the chain accepts at Create for `ladder`. */
53
+ export const minCreateCounter = ladder => MIN_COUNTER_PER_WEIGHT * ladder.weightTotal;
54
+ const ceilDiv = (n, d) => (n + d - 1n) / d;
55
+
56
+ /** Band i's upper sqrt-price edge. */
57
+ export const upperEdge = (l, i) => i + 1 < l.bands.length ? l.bands[i + 1].start : l.closing;
58
+
59
+ /** `D_i = hi.num·lo.den − lo.num·hi.den`: positive exactly when the edges increase. */
60
+ export const delta = (lo, hi) => hi.num * lo.den - lo.num * hi.den;
61
+
62
+ /** `a_sat_i = ceil(w_i·X·D_i / (W·lo.num·hi.num))`: the A a band above the price holds. */
63
+ export const aSat = (l, x, i) => {
64
+ const lo = l.bands[i].start;
65
+ const hi = upperEdge(l, i);
66
+ return ceilDiv(l.bands[i].weight * x * delta(lo, hi), l.weightTotal * lo.num * hi.num);
67
+ };
68
+
69
+ /** `b_sat_i = ceil(w_i·X·D_i / (W·lo.den·hi.den))`: the B a band below the price holds. */
70
+ export const bSat = (l, x, i) => {
71
+ const lo = l.bands[i].start;
72
+ const hi = upperEdge(l, i);
73
+ return ceilDiv(l.bands[i].weight * x * delta(lo, hi), l.weightTotal * lo.den * hi.den);
74
+ };
75
+
76
+ /** `L_k = floor(w_k·X / W)`. */
77
+ export const liquidityOf = (l, x, k) => l.bands[k].weight * x / l.weightTotal;
78
+
79
+ /** Band i's A-side saturation coefficient: the part of `a_sat_i` independent of X, rounded up. */
80
+ export const coeffA = (l, i) => {
81
+ const lo = l.bands[i].start;
82
+ const hi = upperEdge(l, i);
83
+ return ceilDiv(l.bands[i].weight * delta(lo, hi) * FIXED_ONE, l.weightTotal * lo.num * hi.num);
84
+ };
85
+
86
+ /** The same on the B side. */
87
+ export const coeffB = (l, i) => {
88
+ const lo = l.bands[i].start;
89
+ const hi = upperEdge(l, i);
90
+ return ceilDiv(l.bands[i].weight * delta(lo, hi) * FIXED_ONE, l.weightTotal * lo.den * hi.den);
91
+ };
92
+
93
+ /** One index entry: the cumulative coefficients above and below band k. */
94
+
95
+ /**
96
+ * The ladder index `Create` pins and every spend reads: entry k is the pair
97
+ * of cumulative coefficients for the bands strictly above and strictly
98
+ * below band k. `cl_check.build_index`, to the digit.
27
99
  */
100
+ export const buildIndex = l => {
101
+ const n = l.bands.length;
102
+ const a = Array.from({
103
+ length: n
104
+ }, (_, i) => coeffA(l, i));
105
+ const b = Array.from({
106
+ length: n
107
+ }, (_, i) => coeffB(l, i));
108
+ const out = [];
109
+ for (let k = 0; k < n; k++) {
110
+ let ca = 0n;
111
+ let cb = 0n;
112
+ for (let i = 0; i < n; i++) {
113
+ if (i > k) ca += a[i];
114
+ if (i < k) cb += b[i];
115
+ }
116
+ out.push({
117
+ ca,
118
+ cb
119
+ });
120
+ }
121
+ return out;
122
+ };
123
+
124
+ /**
125
+ * A ladder with its index built once. Every probe of the witness search
126
+ * reads the index for every band, and rebuilding it per probe is most of
127
+ * the cost of a quote.
128
+ */
129
+
130
+ export const indexLadder = l => "index" in l && Array.isArray(l.index) ? l : {
131
+ ...l,
132
+ index: buildIndex(l)
133
+ };
134
+
135
+ /** `C_A`, `C_B` at (X, k) from the index: one ceiling after the multiply by X. */
136
+ export const prefixes = (l, x, k) => {
137
+ const e = l.index[k];
138
+ return {
139
+ ca: ceilDiv(e.ca * x, FIXED_ONE),
140
+ cb: ceilDiv(e.cb * x, FIXED_ONE)
141
+ };
142
+ };
143
+
144
+ /** `cl_check.f_at_raw`: the CL arc, cross-multiplied. */
145
+ const fAt = (a, b, L, lo, hi) => (a * hi.num + L * hi.den) * (b * lo.den + L * lo.num) - L * L * hi.num * lo.den;
146
+
147
+ /** G at a witness, on whichever curve the active band carries. */
148
+ const gAt = (l, A, B, x, k) => {
149
+ const {
150
+ ca,
151
+ cb
152
+ } = prefixes(l, x, k);
153
+ const lo = l.bands[k].start;
154
+ const hi = upperEdge(l, k);
155
+ const ra = A - ca;
156
+ const rb = B - cb;
157
+ const L = liquidityOf(l, x, k);
158
+ return l.bands[k].curve === 0 ? fAt(ra, rb, L, lo, hi) : ra * (lo.num * hi.num) + rb * (lo.den * hi.den) - L * delta(lo, hi);
159
+ };
28
160
 
29
- const asFeeParts = fee => {
30
- const f = Fraction.asFraction(fee);
31
- if (f.lt(Fraction.ZERO) || f.gte(Fraction.ONE)) throw new Error("fee must be in [0, 1)");
32
- return [BigInt(f.numerator), BigInt(f.denominator)];
161
+ /** The band proof, P1 through P7. The chain runs exactly this. */
162
+ export const bandProof = (l, A, B, x, k) => {
163
+ if (x < l.weightTotal) return false; // P1
164
+ const {
165
+ ca,
166
+ cb
167
+ } = prefixes(l, x, k);
168
+ const ra = A - ca;
169
+ const rb = B - cb;
170
+ if (ra < 0n || rb < 0n) return false; // P2, P3
171
+ if (ra > aSat(l, x, k) || rb > bSat(l, x, k)) return false; // P4, P5
172
+ if (gAt(l, A, B, x, k) < 0n) return false; // P6
173
+ return gAt(l, A, B, x + 1n, k) < 0n; // P7
33
174
  };
175
+ const SCAN = 400n;
176
+ const CAP = 1n << 80n;
34
177
 
35
178
  /**
36
- * The raw forward swap: given a pre-fee input `dx`, return the output amount.
37
- * A→B and B→A use different virtual-reserve scalings (see `cl_check_swap`).
38
- * All quantities are non-negative so integer division floors, matching chain.
179
+ * The witness in band k, seeded at `hint` when given: the counter only
180
+ * grows along a swap, so the doubling search starts from the previous
181
+ * witness. `lo` is the largest x with non-negative residuals and G ≥ 0,
182
+ * and a witness is such an x whose successor fails G, so the scan walks
183
+ * DOWN from `lo` first. Mirrors the scooper's `find_witness_in_band_from`.
39
184
  */
40
- const rawSwapOutput = (dx, aReserve, bReserve, totalLp, spaNum, spaDen, spbNum, spbDen, feeNum, feeDen, isAInput) => {
41
- const fee = dx * feeNum / feeDen;
42
- const dxEff = dx - fee;
43
- const va0 = aReserve * spbNum + totalLp * spbDen;
44
- const vb0 = bReserve * spaDen + totalLp * spaNum;
45
- if (isAInput) {
46
- const dvaEff = dxEff * spbNum;
47
- const denom = (va0 + dvaEff) * spaDen;
48
- if (denom <= 0n) return 0n;
49
- return vb0 * dvaEff / denom;
185
+ export const findWitnessInBand = (l, A, B, k, hint) => {
186
+ const ok = x => {
187
+ const {
188
+ ca,
189
+ cb
190
+ } = prefixes(l, x, k);
191
+ return A - ca >= 0n && B - cb >= 0n && gAt(l, A, B, x, k) >= 0n;
192
+ };
193
+ const search = start => {
194
+ let lo = start;
195
+ let span = 1n;
196
+ let hi = lo + span;
197
+ while (ok(hi) && hi < CAP) {
198
+ lo = hi;
199
+ span *= 2n;
200
+ hi = lo + span;
201
+ }
202
+ while (lo + 1n < hi) {
203
+ const m = (lo + hi) / 2n;
204
+ if (ok(m)) lo = m;else hi = m;
205
+ }
206
+ const from = lo > SCAN ? lo - SCAN : 1n;
207
+ for (let x = lo; x >= from; x--) {
208
+ if (bandProof(l, A, B, x, k)) return {
209
+ x,
210
+ k
211
+ };
212
+ }
213
+ for (let x = lo + 1n; x <= lo + SCAN; x++) {
214
+ if (bandProof(l, A, B, x, k)) return {
215
+ x,
216
+ k
217
+ };
218
+ }
219
+ return null;
220
+ };
221
+ if (hint !== undefined && hint > 0n && ok(hint)) {
222
+ const w = search(hint);
223
+ if (w) return w;
50
224
  }
51
- const dvbEff = dxEff * spaNum;
52
- const denom = (vb0 + dvbEff) * spbNum;
53
- if (denom <= 0n) return 0n;
54
- return va0 * dvbEff / denom;
225
+ if (!ok(1n)) return null;
226
+ return search(1n);
55
227
  };
56
228
 
57
229
  /**
58
- * Calculate the swap outcome for a concentrated-liquidity pool.
230
+ * The witness `(X, k)` for a pool's reserves. A hint (the indexed
231
+ * `bandCounter` / `activeBand`) is checked first and seeds the search in
232
+ * its band; a stale hint falls back to every band. Null when the reserves
233
+ * sit on no point of the ladder — the chain would reject them too.
234
+ */
235
+ export const findWitness = (ladder, A, B, hint) => {
236
+ const l = indexLadder(ladder);
237
+ if (hint && hint.k >= 0 && hint.k < l.bands.length) {
238
+ if (bandProof(l, A, B, hint.x, hint.k)) return hint;
239
+ const w = findWitnessInBand(l, A, B, hint.k, hint.x);
240
+ if (w) return w;
241
+ }
242
+ for (let k = 0; k < l.bands.length; k++) {
243
+ const w = findWitnessInBand(l, A, B, k);
244
+ if (w) return w;
245
+ }
246
+ return null;
247
+ };
248
+
249
+ /** The active band's view at a witness: its residual reserves and liquidity. */
250
+
251
+ export const bandView = (l, A, B, w) => {
252
+ const {
253
+ ca,
254
+ cb
255
+ } = prefixes(l, w.x, w.k);
256
+ const band = l.bands[w.k];
257
+ return {
258
+ ra: A - ca,
259
+ rb: B - cb,
260
+ l: liquidityOf(l, w.x, w.k),
261
+ lo: band.start,
262
+ hi: upperEdge(l, w.k),
263
+ curve: band.curve,
264
+ feeBuy: band.feeBuy,
265
+ feeSell: band.feeSell
266
+ };
267
+ };
268
+
269
+ /** The band's fee for a direction: `feeSell` when A is sold, `feeBuy` when A is bought. */
270
+ export const feeFor = (v, isAInput) => isAInput ? v.feeSell : v.feeBuy;
271
+
272
+ /**
273
+ * The band fee on a step's real input `dx`: `ceil(dx * num / den)`.
274
+ * Mirrors `cl_check.band_fee` (sundae-v4 #228, SV4-006). The chain charges
275
+ * it per transcript step and rounds it UP, so a step below `den / num`
276
+ * units still pays one unit. A zero rate charges nothing.
277
+ */
278
+ export const bandFee = (dx, fee) => (dx * fee.num + fee.den - 1n) / fee.den;
279
+
280
+ /** The output one band pays for `dx` within itself. Mirrors the scooper's `band_output`. */
281
+ export const bandOutput = (v, isAInput, dx) => {
282
+ const fee = feeFor(v, isAInput);
283
+ if (fee.den <= 0n) return 0n;
284
+ const dxEff = dx - bandFee(dx, fee);
285
+ if (dxEff <= 0n) return 0n;
286
+ if (v.curve === 0) {
287
+ const va0 = v.ra * v.hi.num + v.l * v.hi.den;
288
+ const vb0 = v.rb * v.lo.den + v.l * v.lo.num;
289
+ if (isAInput) {
290
+ const dva = dxEff * v.hi.num;
291
+ const den = (va0 + dva) * v.lo.den;
292
+ return den === 0n ? 0n : vb0 * dva / den;
293
+ }
294
+ const dvb = dxEff * v.lo.den;
295
+ const den = (vb0 + dvb) * v.hi.num;
296
+ return den === 0n ? 0n : va0 * dvb / den;
297
+ }
298
+ const pn = v.lo.num * v.hi.num;
299
+ const pd = v.lo.den * v.hi.den;
300
+ return isAInput ? dxEff * pn / pd : dxEff * pd / pn;
301
+ };
302
+
303
+ /** What the band can pay out in a direction: its residual of the output asset. */
304
+ export const bandCapacity = (v, isAInput) => isAInput ? v.rb : v.ra;
305
+
306
+ /** The largest input whose output stays within the band. */
307
+ export const maxDxInBand = (v, isAInput) => {
308
+ const cap = bandCapacity(v, isAInput);
309
+ if (cap <= 0n) return 0n;
310
+ const fits = dx => bandOutput(v, isAInput, dx) <= cap;
311
+ let hi = 1n;
312
+ for (let guard = 0; fits(hi); guard++) {
313
+ hi *= 2n;
314
+ if (guard > 200) return hi;
315
+ }
316
+ let lo = 0n;
317
+ while (lo + 1n < hi) {
318
+ const m = (lo + hi) / 2n;
319
+ if (fits(m)) lo = m;else hi = m;
320
+ }
321
+ return lo;
322
+ };
323
+
324
+ /** The next band a fill enters: selling A walks down the ladder, buying A walks up. */
325
+ export const nextBand = (k, isAInput, n) => isAInput ? k === 0 ? null : k - 1 : k + 1 >= n ? null : k + 1;
326
+
327
+ /** One band of a fill: what it paid, and the state it left. */
328
+
329
+ /**
330
+ * Price `dx` across as many bands as it takes, as the scooper's
331
+ * `swap_steps` does: fill the active band, cross to the next, repeat.
332
+ * Returns the steps, or null when the ladder cannot absorb `dx` (the
333
+ * direction is exhausted, or the remainder pays nothing).
334
+ */
335
+ export const swapSteps = (ladder, A, B, isAInput, dx, hint) => {
336
+ const l = indexLadder(ladder);
337
+ const n = l.bands.length;
338
+ let ca = A;
339
+ let cb = B;
340
+ let remaining = dx;
341
+ const steps = [];
342
+ let known = findWitness(l, A, B, hint);
343
+ if (!known) return null;
344
+ let prefer = null;
345
+ let hintX;
346
+ for (let guard = 0; remaining > 0n; guard++) {
347
+ if (guard > 2 * n + 2) return null;
348
+ let before = known;
349
+ known = null;
350
+ if (!before) {
351
+ if (prefer !== null) before = findWitnessInBand(l, ca, cb, prefer, hintX);
352
+ if (!before) before = findWitness(l, ca, cb);
353
+ if (!before) return null;
354
+ }
355
+ const view = bandView(l, ca, cb, before);
356
+ const capDx = maxDxInBand(view, isAInput);
357
+ const stepDx = capDx < remaining ? capDx : remaining;
358
+ const dy = stepDx > 0n ? bandOutput(view, isAInput, stepDx) : 0n;
359
+ if (dy <= 0n) {
360
+ const kNext = nextBand(before.k, isAInput, n);
361
+ if (kNext === null || prefer === kNext) return null;
362
+ prefer = kNext;
363
+ continue;
364
+ }
365
+ const na = isAInput ? ca + stepDx : ca - dy;
366
+ const nb = isAInput ? cb - dy : cb + stepDx;
367
+ const crossing = stepDx < remaining;
368
+ let after = null;
369
+ if (crossing) {
370
+ const kNext = nextBand(before.k, isAInput, n);
371
+ if (kNext !== null) after = findWitnessInBand(l, na, nb, kNext, before.x);
372
+ } else {
373
+ after = findWitnessInBand(l, na, nb, before.k, before.x);
374
+ }
375
+ if (!after) after = findWitness(l, na, nb);
376
+ if (!after || after.x < before.x) return null;
377
+ steps.push({
378
+ band: before.k,
379
+ input: stepDx,
380
+ output: dy,
381
+ after
382
+ });
383
+ remaining -= stepDx;
384
+ ca = na;
385
+ cb = nb;
386
+ prefer = after.k;
387
+ hintX = after.x;
388
+ known = after;
389
+ }
390
+ return steps;
391
+ };
392
+
393
+ /**
394
+ * The swap output of a concentrated-liquidity pool for `input` of one asset, crossing
395
+ * bands as needed. `aReserve`/`bReserve` are the pool's reserves in
396
+ * canonical order; `hint` is the indexed `bandCounter` / `activeBand`,
397
+ * which seeds the witness search and may be stale.
59
398
  *
60
- * @param inputMetadata Metadata for the supplied (input) asset — used for the fee AssetAmount.
61
- * @param input The pre-fee amount of the input asset being swapped.
62
- * @param aReserve The pool's raw reserve of asset A.
63
- * @param bReserve The pool's raw reserve of asset B.
64
- * @param totalLp The pool's total LP supply (the virtual liquidity `L`).
65
- * @param sqrtPriceA The lower sqrt-price bound `[num, den]` from the CL config.
66
- * @param sqrtPriceB The upper sqrt-price bound `[num, den]` from the CL config.
67
- * @param fee The liquidity-provider fee (plus protocol fee) as a fraction.
68
- * @param isAInput True when asset A is the supplied asset (A→B swap).
69
- * @returns The swap details in the shared {@link TSwapOutcome} shape.
399
+ * The `lpFee` is the fee the bands kept of the input, summed across the
400
+ * steps. `priceImpact` is against the active band's opening marginal
401
+ * price. Throws when the ladder cannot absorb the input.
70
402
  */
71
- export const getSwapOutput = (inputMetadata, input, aReserve, bReserve, totalLp, sqrtPriceA, sqrtPriceB, fee, isAInput) => {
72
- if (input <= 0n || aReserve <= 0n || bReserve <= 0n || totalLp <= 0n) throw new Error("Input, reserves and liquidity must be positive");
73
- const [spaNum, spaDen] = sqrtPriceA;
74
- const [spbNum, spbDen] = sqrtPriceB;
75
- if (spaNum <= 0n || spaDen <= 0n || spbNum <= 0n || spbDen <= 0n) throw new Error("sqrt prices must be positive");
76
- const [feeNum, feeDen] = asFeeParts(fee);
403
+ export const getSwapOutput = (inputMetadata, input, aReserve, bReserve, ladder, isAInput, hint) => {
404
+ if (input <= 0n || aReserve < 0n || bReserve < 0n) throw new Error("Input must be positive and reserves non-negative");
405
+ if (ladder.bands.length === 0 || ladder.weightTotal <= 0n) throw new Error("The ladder has no bands");
406
+ const l = indexLadder(ladder);
407
+ const steps = swapSteps(l, aReserve, bReserve, isAInput, input, hint);
408
+ if (!steps || steps.length === 0) throw new Error("The ladder cannot absorb this input in this direction");
409
+ let output = 0n;
410
+ let lpFee = 0n;
411
+ for (const s of steps) {
412
+ output += s.output;
413
+ const band = l.bands[s.band];
414
+ const fee = isAInput ? band.feeSell : band.feeBuy;
415
+ lpFee += bandFee(s.input, fee);
416
+ }
77
417
  const inputReserve = isAInput ? aReserve : bReserve;
78
418
  const outputReserve = isAInput ? bReserve : aReserve;
79
- let output = rawSwapOutput(input, aReserve, bReserve, totalLp, spaNum, spaDen, spbNum, spbDen, feeNum, feeDen, isAInput);
80
- // Can't take more than the pool actually holds (virtual reserves can
81
- // otherwise let the formula exceed the real reserve near the boundary).
82
419
  if (output > outputReserve) output = outputReserve;
83
- const inputLpFee = input * feeNum / feeDen;
84
- const nextInputReserve = inputReserve + input;
85
- const nextOutputReserve = outputReserve - output;
86
-
87
- // Price impact vs the marginal (dx→0) rate. Marginal output-per-effective-
88
- // input is vb0·spbNum/(va0·spaDen) for A→B (symmetric for B→A), so the ideal
89
- // input-per-output is its reciprocal. Display-only.
90
- const va0 = aReserve * spbNum + totalLp * spbDen;
91
- const vb0 = bReserve * spaDen + totalLp * spaNum;
420
+ // Price impact against the opening marginal of the first band traded.
421
+ const w0 = findWitness(l, aReserve, bReserve, hint);
422
+ const v0 = bandView(l, aReserve, bReserve, w0);
92
423
  let priceImpact = Fraction.ZERO;
93
424
  if (output > 0n) {
94
- const idealPrice = isAInput ? new Fraction(va0 * spaDen, vb0 * spbNum) : new Fraction(vb0 * spbNum, va0 * spaDen);
95
- const actualPrice = new Fraction(input - inputLpFee, output);
96
- priceImpact = Fraction.ONE.subtract(idealPrice.divide(actualPrice));
425
+ let ideal;
426
+ if (v0.curve === 1) {
427
+ const pn = v0.lo.num * v0.hi.num;
428
+ const pd = v0.lo.den * v0.hi.den;
429
+ ideal = isAInput ? new Fraction(pd, pn) : new Fraction(pn, pd);
430
+ } else {
431
+ const va0 = v0.ra * v0.hi.num + v0.l * v0.hi.den;
432
+ const vb0 = v0.rb * v0.lo.den + v0.l * v0.lo.num;
433
+ ideal = isAInput ? new Fraction(va0 * v0.lo.den, vb0 * v0.hi.num) : new Fraction(vb0 * v0.hi.num, va0 * v0.lo.den);
434
+ }
435
+ const actual = new Fraction(input - lpFee, output);
436
+ priceImpact = Fraction.ONE.subtract(ideal.divide(actual));
97
437
  }
98
438
  return {
99
439
  input,
100
440
  output,
101
- lpFee: new AssetAmount(inputLpFee, inputMetadata),
102
- nextInputReserve,
103
- nextOutputReserve,
104
- priceImpact
441
+ lpFee: new AssetAmount(lpFee, inputMetadata),
442
+ nextInputReserve: inputReserve + input,
443
+ nextOutputReserve: outputReserve - output,
444
+ priceImpact,
445
+ steps
105
446
  };
106
447
  };
107
448
 
449
+ /** What each band holds at the pool's reserves. */
450
+
108
451
  /**
109
- * Calculate the minimal input required to receive a given output — the inverse
110
- * of {@link getSwapOutput}. The forward swap is linear-fractional in the
111
- * effective input, so it inverts in closed form; a short correction loop
112
- * absorbs the fee floor and integer rounding so the result is the exact minimal
113
- * input whose forward swap yields at least `output`.
452
+ * Every band's state at the pool's reserves: a band below the price holds
453
+ * only B, a band above it only A, the active band some of both. Null when
454
+ * the reserves sit on no point of the ladder.
114
455
  */
115
- export const getSwapInput = (inputMetadata, output, aReserve, bReserve, totalLp, sqrtPriceA, sqrtPriceB, fee, isAInput) => {
116
- if (output <= 0n || aReserve <= 0n || bReserve <= 0n || totalLp <= 0n) throw new Error("Output, reserves and liquidity must be positive");
117
- const [spaNum, spaDen] = sqrtPriceA;
118
- const [spbNum, spbDen] = sqrtPriceB;
119
- if (spaNum <= 0n || spaDen <= 0n || spbNum <= 0n || spbDen <= 0n) throw new Error("sqrt prices must be positive");
120
- const outputReserve = isAInput ? bReserve : aReserve;
121
- if (output > outputReserve) throw new Error("Output must not exceed the output reserve");
122
- const [feeNum, feeDen] = asFeeParts(fee);
123
- const feeDiff = feeDen - feeNum;
124
- const va0 = aReserve * spbNum + totalLp * spbDen;
125
- const vb0 = bReserve * spaDen + totalLp * spaNum;
126
- const ceilDiv = (n, d) => (n + d - 1n) / d;
127
-
128
- // Closed-form minimal effective input, then gross up for the fee floor.
129
- let effMin;
130
- if (isAInput) {
131
- // dvaEff ≥ output·spaDen·va0 / (vb0 − output·spaDen)
132
- const denom = vb0 - output * spaDen;
133
- if (denom <= 0n) throw new Error("Output not reachable within the pool's range");
134
- const dvaEffMin = ceilDiv(output * spaDen * va0, denom);
135
- effMin = ceilDiv(dvaEffMin, spbNum);
136
- } else {
137
- const denom = va0 - output * spbNum;
138
- if (denom <= 0n) throw new Error("Output not reachable within the pool's range");
139
- const dvbEffMin = ceilDiv(output * spbNum * vb0, denom);
140
- effMin = ceilDiv(dvbEffMin, spaNum);
456
+ export const bandStates = (ladder, aReserve, bReserve, hint) => {
457
+ const l = indexLadder(ladder);
458
+ const w = findWitness(l, aReserve, bReserve, hint);
459
+ if (!w) return null;
460
+ const out = [];
461
+ let restA = 0n;
462
+ let restB = 0n;
463
+ for (let i = 0; i < l.bands.length; i++) {
464
+ const liquidity = liquidityOf(l, w.x, i);
465
+ if (i < w.k) {
466
+ const b = bSat(l, w.x, i);
467
+ restB += b;
468
+ out.push({
469
+ quantities: [0n, b],
470
+ liquidity,
471
+ active: false
472
+ });
473
+ } else if (i > w.k) {
474
+ const a = aSat(l, w.x, i);
475
+ restA += a;
476
+ out.push({
477
+ quantities: [a, 0n],
478
+ liquidity,
479
+ active: false
480
+ });
481
+ } else {
482
+ out.push({
483
+ quantities: [0n, 0n],
484
+ liquidity,
485
+ active: true
486
+ });
487
+ }
141
488
  }
142
- let input = feeDiff > 0n ? ceilDiv(effMin * feeDen, feeDiff) : effMin;
143
-
144
- // Correct for floor interactions: nudge up until the forward swap clears the
145
- // target, then trim any over-shoot. Bounded — a couple of iterations at most.
146
- const fwd = dx => rawSwapOutput(dx, aReserve, bReserve, totalLp, spaNum, spaDen, spbNum, spbDen, feeNum, feeDen, isAInput);
147
- let guard = 0;
148
- while (fwd(input) < output && guard++ < 64) input += 1n;
149
- while (input > 1n && fwd(input - 1n) >= output) input -= 1n;
150
- const inputLpFee = input * feeNum / feeDen;
151
- const inputReserve = isAInput ? aReserve : bReserve;
152
- const nextInputReserve = inputReserve + input;
153
- const nextOutputReserve = outputReserve - output;
154
- let priceImpact = Fraction.ZERO;
155
- if (output > 0n) {
156
- const idealPrice = isAInput ? new Fraction(va0 * spaDen, vb0 * spbNum) : new Fraction(vb0 * spbNum, va0 * spaDen);
157
- const actualPrice = new Fraction(input - inputLpFee, output);
158
- priceImpact = Fraction.ONE.subtract(idealPrice.divide(actualPrice));
489
+ const ra = aReserve - restA;
490
+ const rb = bReserve - restB;
491
+ out[w.k].quantities = [ra < 0n ? 0n : ra, rb < 0n ? 0n : rb];
492
+ return out;
493
+ };
494
+
495
+ /** Which band holds a sqrt-price. */
496
+ export const bandOf = (l, s) => {
497
+ for (let i = 0; i < l.bands.length; i++) {
498
+ const hi = upperEdge(l, i);
499
+ if (s.num * hi.den < hi.num * s.den) return i;
500
+ }
501
+ return l.bands.length - 1;
502
+ };
503
+
504
+ /**
505
+ * The exact reserves a ladder holds at counter `x` with the price at
506
+ * sqrt-price `s`, which must lie in band `k`: what a pool creator deposits
507
+ * for a chosen shape, scale and launch price. The result is floored, so
508
+ * `findWitness` must still be run on it to learn the counter the chain
509
+ * derives — usually `x`, but rounding can move it.
510
+ */
511
+ export const reservesAt = (l, x, k, s) => {
512
+ let A = 0n;
513
+ let B = 0n;
514
+ for (let i = 0; i < l.bands.length; i++) {
515
+ if (i > k) {
516
+ A += aSat(l, x, i);
517
+ continue;
518
+ }
519
+ if (i < k) {
520
+ B += bSat(l, x, i);
521
+ continue;
522
+ }
523
+ const lo = l.bands[i].start;
524
+ const hi = upperEdge(l, i);
525
+ const L = liquidityOf(l, x, i);
526
+ if (l.bands[i].curve === 1) {
527
+ // A constant-sum bin prices at the geometric mean; `s` only says how
528
+ // far through the bin the conversion has gone.
529
+ const K = L * delta(lo, hi);
530
+ const frac = (s.num * lo.den - lo.num * s.den) * hi.den;
531
+ const span = (hi.num * lo.den - lo.num * hi.den) * s.den;
532
+ const rb = K * frac / (span * (lo.den * hi.den));
533
+ A += (K - rb * (lo.den * hi.den)) / (lo.num * hi.num);
534
+ B += rb;
535
+ } else {
536
+ // a = L·(1/s − 1/hi), b = L·(s − lo), in exact rationals.
537
+ A += L * (s.den * hi.num - s.num * hi.den) / (s.num * hi.num);
538
+ B += L * (s.num * lo.den - lo.num * s.den) / (s.den * lo.den);
539
+ }
159
540
  }
160
541
  return {
161
- input,
162
- output,
163
- lpFee: new AssetAmount(inputLpFee, inputMetadata),
164
- nextInputReserve,
165
- nextOutputReserve,
166
- priceImpact
542
+ A,
543
+ B
167
544
  };
168
545
  };
169
546
 
547
+ /** A fraction in lowest terms. */
548
+ const reduceFrac = f => {
549
+ const gcd = (a, b) => b === 0n ? a : gcd(b, a % b);
550
+ const g = gcd(f.num < 0n ? -f.num : f.num, f.den);
551
+ return g > 1n ? {
552
+ num: f.num / g,
553
+ den: f.den / g
554
+ } : f;
555
+ };
556
+
170
557
  /**
171
- * Deposit LP estimate for a concentrated-liquidity pool. CL deposits reuse the
172
- * constant-product *proportional* pinning: minting by the scarcest offered
173
- * asset and ceil-pinning the reserve deltas gives, per asset,
174
- * `a1·L0 ≥ a0·L1` and `b1·L0 ≥ b0·L1` — and those two bounds multiply to
175
- * exactly the on-chain CL non-swap invariant `va1·vb1·L0² ≥ va0·vb0·L1²`
176
- * (`cl_check.ak`). So the proportional deposit is always chain-valid; excess of
177
- * either asset is refunded via aChange/bChange. This mirrors the scooper's
178
- * `resolve_cl_deposit` (which likewise reuses the CP pinning).
558
+ * An equal-weight ladder of `n` bands from `lo` to `hi`, every band a CL
559
+ * arc at `fee` unless named in `curves`. The edges are built over a common
560
+ * denominator and reduced: every rational component of a concentrated-liquidity config
561
+ * must stay below 2^64 (spec V9).
179
562
  */
180
- export const calculateLiquidity = (a, b, aReserve, bReserve, totalLp) => ConstantProductPool.calculateLiquidity(a, b, aReserve, bReserve, totalLp);
563
+ export const uniformLadder = (n, lo, hi, fee, curves = []) => {
564
+ if (n < 1) throw new Error("A ladder needs at least one band");
565
+ if (lo.num * hi.den >= hi.num * lo.den) throw new Error("The lower edge must be strictly below the upper edge");
566
+ const span = hi.num * lo.den - lo.num * hi.den;
567
+ const commonDen = lo.den * hi.den * BigInt(n);
568
+ const bands = [];
569
+ for (let i = 0; i < n; i++) {
570
+ bands.push({
571
+ start: reduceFrac({
572
+ num: lo.num * hi.den * BigInt(n) + span * BigInt(i),
573
+ den: commonDen
574
+ }),
575
+ weight: 1n,
576
+ curve: curves[i] ?? 0,
577
+ feeBuy: fee,
578
+ feeSell: fee
579
+ });
580
+ }
581
+ return {
582
+ bands,
583
+ closing: reduceFrac({
584
+ num: lo.num * hi.den * BigInt(n) + span * BigInt(n),
585
+ den: commonDen
586
+ }),
587
+ weightTotal: BigInt(n)
588
+ };
589
+ };
590
+
591
+ /** Band weight profiles. Every weight is at least 1, as `Create` demands. */
592
+
593
+ export const bandWeights = (n, shape = "concentrated", opts = {}) => {
594
+ if (shape === "flat") return Array.from({
595
+ length: n
596
+ }, () => 1n);
597
+ const peak = Number(opts.peak ?? 64n);
598
+ const tail = Number(opts.tail ?? 1n);
599
+ const centre = opts.centre ?? (n - 1) / 2;
600
+ const sigma = Math.max(n / 5, 0.75);
601
+ const bell = (i, c) => Math.exp(-(((i - c) / sigma) ** 2));
602
+ const profile = i => {
603
+ switch (shape) {
604
+ case "concentrated":
605
+ return bell(i, centre);
606
+ case "skewed-low":
607
+ return Math.exp(-i / Math.max(n / 3, 1));
608
+ case "skewed-high":
609
+ return Math.exp(-(n - 1 - i) / Math.max(n / 3, 1));
610
+ case "bimodal":
611
+ return Math.max(bell(i, (n - 1) / 4), bell(i, 3 * (n - 1) / 4));
612
+ default:
613
+ return 1;
614
+ }
615
+ };
616
+ return Array.from({
617
+ length: n
618
+ }, (_, i) => {
619
+ const w = Math.round(tail + (peak - tail) * profile(i));
620
+ return BigInt(Math.max(1, w));
621
+ });
622
+ };
623
+
624
+ /**
625
+ * A ladder of `n` bands from `lo` to `hi` with a weight shape, optional
626
+ * constant-sum bins and an optional per-band fee. The edges are
627
+ * `uniformLadder`'s exactly; only the weights, curves and fees differ.
628
+ */
629
+ export const shapedLadder = (n, lo, hi, fee, opts = {}) => {
630
+ const base = uniformLadder(n, lo, hi, fee);
631
+ const weights = bandWeights(n, opts.shape ?? "concentrated", opts);
632
+ const cs = new Set(opts.csBands ?? []);
633
+ const bands = base.bands.map((b, i) => {
634
+ const f = opts.feeAt ? opts.feeAt(i) : fee;
635
+ return {
636
+ ...b,
637
+ weight: weights[i],
638
+ curve: cs.has(i) ? 1 : 0,
639
+ feeBuy: f,
640
+ feeSell: f
641
+ };
642
+ });
643
+ return {
644
+ bands,
645
+ closing: base.closing,
646
+ weightTotal: weights.reduce((s, w) => s + w, 0n)
647
+ };
648
+ };
181
649
  //# sourceMappingURL=ConcentratedLiquidityPool.js.map