@sundaeswap/math 0.3.0 → 0.5.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,638 @@
1
+ import { AssetAmount, IAssetAmountMetadata } from "@sundaeswap/asset";
2
+ import { Fraction, type TFractionLike } from "@sundaeswap/fraction";
3
+
4
+ import { SharedPoolMath } from "./index.js";
5
+
6
+ /**
7
+ * THE V4 STABLESWAP INVARIANT MODULE — NOT THE V3 "STABLESWAPS" CONTRACT.
8
+ *
9
+ * Two different things in this SDK carry the word "stableswap". Keep them
10
+ * apart:
11
+ *
12
+ * - `StableSwapsPool` / `EContractVersion.Stableswaps` is the **v3** stableswap
13
+ * contract. It is a whole contract version with its own datum builder and
14
+ * transaction builder. It stores `D` in the pool datum and has no per-asset
15
+ * rates.
16
+ * - `V4StableswapPool` / `EPoolCurve.V4Stableswap` (this file) is a **v4
17
+ * invariant module**. It is one curve among several a v4 pool can bind, next
18
+ * to constant product, constant sum and concentrated liquidity. It stores no
19
+ * `D` and carries per-asset integer `rates`.
20
+ *
21
+ * The amplification `A` is the ONE thing they share: the same parameter on the
22
+ * same scale, carried by the same `linearAmplificationFactor` field, with each
23
+ * implementation applying its own internal scaling. Everything else is
24
+ * different — do not pass one's reserves, rates or `D` to the other.
25
+ *
26
+ * The curve, on rated and scaled reserves `x = r_a·rate_a·P` and
27
+ * `y = r_b·rate_b·P` with `P = calc_precision = 10^12` and `A` the linear
28
+ * amplification:
29
+ *
30
+ * 4A(x + y) + D = 4AD + D³ / (4xy)
31
+ *
32
+ * `D` is the largest integer that satisfies it. Neither `D` nor a swap output
33
+ * can be computed on-chain, because both need Newton's method. The scooper
34
+ * computes them and the module checks that each supplied integer is the unique
35
+ * one that solves the invariant. This file is the off-chain twin of
36
+ * `lib/modules/ss_math.ak` and `lib/modules/ss_check.ak` in the sundae-v4
37
+ * repository — those are the source of truth; keep this in lockstep with them.
38
+ */
39
+
40
+ /** `ss_math.calc_precision`: the scale applied to reserves before they enter the invariant. */
41
+ export const CALC_PRECISION = 1_000_000_000_000n;
42
+
43
+ /** Newton iteration cap, matching the reference implementation. */
44
+ const NEWTON_MAX_ITERATIONS = 255;
45
+
46
+ /** Bound on the fix-up walk of the reverse swap. The closed form lands within a few units. */
47
+ const CORRECTION_MAX_STEPS = 256;
48
+
49
+ /**
50
+ * Holds the calculated outcome of a v4 stableswap swap. Mirrors
51
+ * {@link ConstantProductPool.TSwapOutcome} so it flows through the same generic
52
+ * swap-outcome consumers.
53
+ *
54
+ * `lpFee` is denominated in the **output** asset, not the input asset. The
55
+ * stableswap fee is taken off the gross output (`fee = ceil(gross · feeRate)`),
56
+ * unlike constant product and constant sum which take the fee off the input.
57
+ * The v3 `StableSwapsPool` does the same.
58
+ */
59
+ export type TSwapOutcome = {
60
+ input: bigint;
61
+ output: bigint;
62
+ lpFee: AssetAmount<IAssetAmountMetadata>;
63
+ nextInputReserve: bigint;
64
+ nextOutputReserve: bigint;
65
+ priceImpact: Fraction;
66
+ };
67
+
68
+ // ─── Integer helpers ────────────────────────────────────────────────────────
69
+
70
+ /** Plutus `divideInteger`: floor toward −∞. */
71
+ const floorDiv = (a: bigint, b: bigint): bigint => {
72
+ const q = a / b;
73
+ const r = a % b;
74
+ return r !== 0n && r < 0n !== b < 0n ? q - 1n : q;
75
+ };
76
+
77
+ /** ceil(a / b) with the same sign convention as {@link floorDiv}. */
78
+ const ceilDiv = (a: bigint, b: bigint): bigint => -floorDiv(-a, b);
79
+
80
+ const asFeeParts = (fee: TFractionLike): [bigint, bigint] => {
81
+ const f = Fraction.asFraction(fee);
82
+ if (f.lt(Fraction.ZERO) || f.gte(Fraction.ONE))
83
+ throw new Error("fee must be in [0, 1)");
84
+ return [BigInt(f.numerator), BigInt(f.denominator)];
85
+ };
86
+
87
+ // ─── Invariant polynomials (`ss_math.ak`) ───────────────────────────────────
88
+
89
+ /**
90
+ * `f(D)` from the on-chain `liquidity_invariant`. It is ≤ 0 when `D` is at or
91
+ * below the curve. All arguments are rated and scaled.
92
+ */
93
+ export const invariantF = (
94
+ x: bigint,
95
+ y: bigint,
96
+ amp: bigint,
97
+ d: bigint,
98
+ ): bigint => {
99
+ const sixteenAxy = 16n * amp * x * y;
100
+ return sixteenAxy * d + d * d * d - (sixteenAxy * (x + y) + 4n * x * y * d);
101
+ };
102
+
103
+ /**
104
+ * `g(y)` from the on-chain `exchange_invariant`. It is ≥ 0 when the pool keeps
105
+ * enough of the taken asset. `g` is symmetric in `x` and `y`, which is what
106
+ * lets {@link getSwapInput} reuse the same solver in the other direction.
107
+ */
108
+ export const invariantG = (
109
+ x: bigint,
110
+ y: bigint,
111
+ amp: bigint,
112
+ d: bigint,
113
+ ): bigint => {
114
+ const fourXy = 4n * x * y;
115
+ return fourXy * (4n * amp * (x + y) + d) - fourXy * 4n * amp * d - d * d * d;
116
+ };
117
+
118
+ // ─── D ──────────────────────────────────────────────────────────────────────
119
+
120
+ /** The ±1 fix-up that turns a Newton result into the exact largest `D`. */
121
+ const fixD = (x: bigint, y: bigint, amp: bigint, d0: bigint): bigint => {
122
+ let d = d0;
123
+ while (invariantF(x, y, amp, d) > 0n) d -= 1n;
124
+ while (invariantF(x, y, amp, d + 1n) <= 0n) d += 1n;
125
+ return d;
126
+ };
127
+
128
+ /**
129
+ * The sum invariant `D` of a two-asset v4 stableswap pool: the largest integer
130
+ * that satisfies the curve on the rated, scaled reserves. Newton from
131
+ * `D = x + y`, then a ±1 fix-up.
132
+ *
133
+ * `D` carries the `rate · CALC_PRECISION` scale — it is not a token amount.
134
+ *
135
+ * @param amp The pool's `linear_amplification` (`A`), the raw stored integer.
136
+ * @param aReserve The pool's reserve of asset A, in token units.
137
+ * @param bReserve The pool's reserve of asset B, in token units.
138
+ * @param rateA The pool's rate for asset A (from the stableswap config).
139
+ * @param rateB The pool's rate for asset B.
140
+ */
141
+ export const getD = (
142
+ amp: bigint,
143
+ aReserve: bigint,
144
+ bReserve: bigint,
145
+ rateA = 1n,
146
+ rateB = 1n,
147
+ ): bigint => {
148
+ if (amp <= 0n) throw new Error("amplification must be positive");
149
+ if (aReserve < 0n || bReserve < 0n)
150
+ throw new Error("reserves must be non-negative");
151
+ if (rateA <= 0n || rateB <= 0n) throw new Error("rates must be positive");
152
+
153
+ const xs = aReserve * rateA * CALC_PRECISION;
154
+ const ys = bReserve * rateB * CALC_PRECISION;
155
+ const sum = xs + ys;
156
+ // An empty pool has no invariant to solve; `D` is zero by definition.
157
+ if (sum === 0n) return 0n;
158
+ // Exactly one empty reserve is a different case, and it has no answer: the
159
+ // Newton step divides by `4·xs·ys`, which is zero here. The chain never
160
+ // reaches this state — Create requires both reserves positive, and the
161
+ // exchange invariant leaves at least one unit on the output side — but this
162
+ // is a client library, so it reports the bad input instead of faulting on a
163
+ // native division by zero.
164
+ if (xs === 0n || ys === 0n) {
165
+ throw new Error(
166
+ "getD: a stableswap pool cannot hold exactly one empty reserve; D is undefined there",
167
+ );
168
+ }
169
+
170
+ const ann = 4n * amp;
171
+ let d = sum;
172
+ for (let i = 0; i < NEWTON_MAX_ITERATIONS; i++) {
173
+ const dP = floorDiv(d * d * d, 4n * xs * ys);
174
+ const dNext = floorDiv((ann * sum + 2n * dP) * d, (ann - 1n) * d + 3n * dP);
175
+ const diff = dNext > d ? dNext - d : d - dNext;
176
+ d = dNext;
177
+ if (diff <= 1n) return fixD(xs, ys, amp, d);
178
+ }
179
+ throw new Error("getD: reached the Newton iteration cap");
180
+ };
181
+
182
+ // ─── The exchange solver ────────────────────────────────────────────────────
183
+
184
+ /**
185
+ * The smallest scaled `y` with `g(xs, y) ≥ 0` at the pre-swap `d`: the least
186
+ * the pool may keep of the counterparty asset and still sit on the curve.
187
+ * Newton on `y`, then a walk to the exact integer — the walk leaves `y` with
188
+ * `g(y) ≥ 0` and `g(y − 1) < 0`, so it is the boundary and not merely a valid
189
+ * point.
190
+ *
191
+ * Read the direction carefully, because the two framings invert. A caller
192
+ * turning this into an output computes `raw = Y − y`, so the SMALLEST `y` is
193
+ * the LARGEST output the invariant admits. It is still not generous: `y` is a
194
+ * ceiling on the exact real-valued requirement, so `raw` is at or below the
195
+ * exact output. Most the curve allows, never more than it allows.
196
+ *
197
+ * Both swap directions use this: `g` is symmetric, so solving for the
198
+ * counterparty reserve answers "what must the other side hold" whichever side
199
+ * is given.
200
+ */
201
+ const solveCounterparty = (amp: bigint, d: bigint, xs: bigint): bigint => {
202
+ if (xs <= 0n)
203
+ throw new Error("solveCounterparty: the given side must be positive");
204
+ const ann = 4n * amp;
205
+ // n = 2: c = D³ / (4·x·Ann), b = x + D / Ann. Evaluated in the same order as
206
+ // the Aiken source so the intermediate floors match.
207
+ const c = floorDiv(floorDiv(d * d, 2n * xs) * d, 2n * ann);
208
+ const b = xs + floorDiv(d, ann);
209
+ let y = d;
210
+ let converged = false;
211
+ for (let i = 0; i < NEWTON_MAX_ITERATIONS; i++) {
212
+ const yNext = floorDiv(y * y + c, 2n * y + b - d);
213
+ const diff = yNext > y ? yNext - y : y - yNext;
214
+ y = yNext;
215
+ if (diff <= 1n) {
216
+ converged = true;
217
+ break;
218
+ }
219
+ }
220
+ if (!converged)
221
+ throw new Error("solveCounterparty: reached the Newton iteration cap");
222
+ while (invariantG(xs, y, amp, d) < 0n) y += 1n;
223
+ while (invariantG(xs, y - 1n, amp, d) >= 0n) y -= 1n;
224
+ return y;
225
+ };
226
+
227
+ /**
228
+ * The raw swap output: the scaled, pre-fee amount the curve releases when the
229
+ * given reserve becomes `inAfter` and the taken reserve starts at `outBefore`,
230
+ * at the pre-swap `d`. It is the largest such amount the exchange invariant
231
+ * admits — one unit more leaves the pool below the curve — and it is at or
232
+ * below the exact real-valued output, so it never over-quotes.
233
+ *
234
+ * The result carries the `rate · CALC_PRECISION` scale. Divide by
235
+ * `rateOut · CALC_PRECISION` to get the gross output in token units.
236
+ *
237
+ * @param amp The pool's `linear_amplification` (`A`), the raw stored integer.
238
+ * @param d The pre-swap sum invariant, from {@link getD}.
239
+ * @param inAfter The given asset's reserve after the input arrives, in token units.
240
+ * @param outBefore The taken asset's reserve before the swap, in token units.
241
+ * @param rateIn The pool's rate for the given asset.
242
+ * @param rateOut The pool's rate for the taken asset.
243
+ */
244
+ export const getRawSwap = (
245
+ amp: bigint,
246
+ d: bigint,
247
+ inAfter: bigint,
248
+ outBefore: bigint,
249
+ rateIn = 1n,
250
+ rateOut = 1n,
251
+ ): bigint => {
252
+ if (amp <= 0n) throw new Error("amplification must be positive");
253
+ if (d <= 0n) throw new Error("the sum invariant must be positive");
254
+ if (inAfter <= 0n || outBefore <= 0n)
255
+ throw new Error("reserves must be positive");
256
+ if (rateIn <= 0n || rateOut <= 0n) throw new Error("rates must be positive");
257
+ const xs = inAfter * rateIn * CALC_PRECISION;
258
+ return outBefore * rateOut * CALC_PRECISION - solveCounterparty(amp, d, xs);
259
+ };
260
+
261
+ // ─── Marginal price ─────────────────────────────────────────────────────────
262
+
263
+ /**
264
+ * The marginal price of the curve at the current reserves, as input units per
265
+ * output unit. Differentiating the invariant at fixed `D` gives
266
+ *
267
+ * −dy/dx = (16A·x²y² + D³·y) / (16A·x²y² + D³·x)
268
+ *
269
+ * in scaled value units, which converts to token units through the rates. At a
270
+ * balanced pool (`x = y`) it reduces to `rateOut / rateIn`, the par price.
271
+ */
272
+ const marginalInputPerOutput = (
273
+ amp: bigint,
274
+ xs: bigint,
275
+ ys: bigint,
276
+ d: bigint,
277
+ rateIn: bigint,
278
+ rateOut: bigint,
279
+ ): Fraction => {
280
+ const common = 16n * amp * xs * xs * ys * ys;
281
+ const cube = d * d * d;
282
+ return new Fraction(
283
+ rateOut * (common + cube * xs),
284
+ rateIn * (common + cube * ys),
285
+ );
286
+ };
287
+
288
+ /**
289
+ * The pool's marginal price as raw units of asset A per raw unit of asset B.
290
+ * Use it where a constant-product pool would use the reserve ratio: for a
291
+ * stableswap pool the reserve ratio is not the price, because the curve holds
292
+ * the price near par across a wide band of ratios.
293
+ *
294
+ * The result is in raw (undecimalized) units. Adjust for the two assets'
295
+ * decimals before display.
296
+ */
297
+ export const getPrice = (
298
+ amp: bigint,
299
+ aReserve: bigint,
300
+ bReserve: bigint,
301
+ rateA = 1n,
302
+ rateB = 1n,
303
+ ): Fraction => {
304
+ if (aReserve <= 0n || bReserve <= 0n)
305
+ throw new Error("reserves must be positive");
306
+ const d = getD(amp, aReserve, bReserve, rateA, rateB);
307
+ const xs = aReserve * rateA * CALC_PRECISION;
308
+ const ys = bReserve * rateB * CALC_PRECISION;
309
+ // A per B: asset B is the output side of the derivative.
310
+ return marginalInputPerOutput(amp, xs, ys, d, rateA, rateB);
311
+ };
312
+
313
+ // ─── Swap output ────────────────────────────────────────────────────────────
314
+
315
+ /**
316
+ * Calculate the swap outcome for a v4 stableswap pool.
317
+ *
318
+ * The sequence the chain enforces (`ss_check.ak`, tag 3):
319
+ *
320
+ * 1. `D` is derived from the pre-swap reserves at the pool's rates.
321
+ * 2. `raw` is the LARGEST scaled output the exchange invariant admits at that
322
+ * `D`, with the input already added to the given reserve. The solver pins
323
+ * the pool's remaining taken reserve `y` to the SMALLEST integer that still
324
+ * satisfies the invariant, and `raw = outBefore·rateOut·P − y`, so the two
325
+ * statements are the same one: least reserve kept, most output released.
326
+ * 3. `gross = floor(raw / (rateOut · CALC_PRECISION))` is the output in token units.
327
+ * 4. `fee = ceil(gross · feeRate)` stays in the pool.
328
+ * 5. The trader receives `gross − fee`.
329
+ *
330
+ * Every rounding step favours the pool, never the trader: `y` is a ceiling on
331
+ * the exact reserve the curve requires, `gross` floors to whole tokens, and the
332
+ * fee ceilings. A trader is never quoted more than the chain will pay.
333
+ *
334
+ * @param outputMetadata Metadata for the **taken** asset. The fee is denominated in it.
335
+ * @param input The amount of the given asset being swapped.
336
+ * @param inputReserve The pool's reserve of the given asset.
337
+ * @param outputReserve The pool's reserve of the taken asset.
338
+ * @param rateIn The pool's rate for the given asset (from the stableswap config).
339
+ * @param rateOut The pool's rate for the taken asset.
340
+ * @param amp The pool's `linear_amplification` (`A`), the raw stored integer.
341
+ * @param fee The swap fee rate, applied to the gross output.
342
+ * @returns The swap details in the shared {@link TSwapOutcome} shape.
343
+ */
344
+ export const getSwapOutput = (
345
+ outputMetadata: IAssetAmountMetadata,
346
+ input: bigint,
347
+ inputReserve: bigint,
348
+ outputReserve: bigint,
349
+ rateIn: bigint,
350
+ rateOut: bigint,
351
+ amp: bigint,
352
+ fee: TFractionLike,
353
+ ): TSwapOutcome => {
354
+ if (input <= 0n || inputReserve <= 0n || outputReserve <= 0n)
355
+ throw new Error("Input and reserves must be positive");
356
+ if (rateIn <= 0n || rateOut <= 0n) throw new Error("Rates must be positive");
357
+ if (amp <= 0n) throw new Error("Amplification must be positive");
358
+ const [feeNum, feeDen] = asFeeParts(fee);
359
+
360
+ const d = getD(amp, inputReserve, outputReserve, rateIn, rateOut);
361
+ const raw = getRawSwap(
362
+ amp,
363
+ d,
364
+ inputReserve + input,
365
+ outputReserve,
366
+ rateIn,
367
+ rateOut,
368
+ );
369
+ const gross = floorDiv(raw, rateOut * CALC_PRECISION);
370
+ const lpFee = ceilDiv(gross * feeNum, feeDen);
371
+ const output = gross - lpFee;
372
+ if (output <= 0n)
373
+ throw new Error("Swap yields no output after the fee; raise the input");
374
+
375
+ const nextInputReserve = inputReserve + input;
376
+ const nextOutputReserve = outputReserve - output;
377
+
378
+ // Price impact against the pool's marginal price before the swap, the same
379
+ // definition concentrated liquidity uses. Constant sum measures against its
380
+ // par price instead, because a constant-sum pool has no other price. A
381
+ // stableswap pool does: its marginal price moves with the reserve ratio, so
382
+ // measuring against par would report the pool's standing imbalance as this
383
+ // trade's impact. The fee is excluded so the number is curve slippage only.
384
+ const xs = inputReserve * rateIn * CALC_PRECISION;
385
+ const ys = outputReserve * rateOut * CALC_PRECISION;
386
+ const idealPrice = marginalInputPerOutput(amp, xs, ys, d, rateIn, rateOut);
387
+ const actualPrice = new Fraction(input, gross);
388
+ const priceImpact = Fraction.ONE.subtract(idealPrice.divide(actualPrice));
389
+
390
+ return {
391
+ input,
392
+ output,
393
+ lpFee: new AssetAmount(lpFee, outputMetadata),
394
+ nextInputReserve,
395
+ nextOutputReserve,
396
+ priceImpact,
397
+ };
398
+ };
399
+
400
+ // ─── Swap input (the reverse) ───────────────────────────────────────────────
401
+
402
+ /**
403
+ * Calculate the minimal input that buys a given output — the inverse of
404
+ * {@link getSwapOutput}.
405
+ *
406
+ * The steps:
407
+ *
408
+ * 1. Invert the fee: the smallest gross output that nets `output` after
409
+ * `ceil(gross · feeRate)` is `ceil(output · feeDen / (feeDen − feeNum))`.
410
+ * 2. Turn that gross into the scaled reserve the pool may keep of the taken
411
+ * asset.
412
+ * 3. Solve the exchange invariant in the other direction for the given
413
+ * reserve that supports it. `g` is symmetric, so this is the same solver.
414
+ * 4. Convert to token units and walk down to the minimum, which absorbs the
415
+ * integer rounding of step 3.
416
+ *
417
+ * @param outputMetadata Metadata for the **taken** asset. The fee is denominated in it.
418
+ * @param output The wanted amount of the taken asset.
419
+ * @param inputReserve The pool's reserve of the given asset.
420
+ * @param outputReserve The pool's reserve of the taken asset.
421
+ * @param rateIn The pool's rate for the given asset (from the stableswap config).
422
+ * @param rateOut The pool's rate for the taken asset.
423
+ * @param amp The pool's `linear_amplification` (`A`), the raw stored integer.
424
+ * @param fee The swap fee rate, applied to the gross output.
425
+ * @returns The swap details in the shared {@link TSwapOutcome} shape.
426
+ */
427
+ export const getSwapInput = (
428
+ outputMetadata: IAssetAmountMetadata,
429
+ output: bigint,
430
+ inputReserve: bigint,
431
+ outputReserve: bigint,
432
+ rateIn: bigint,
433
+ rateOut: bigint,
434
+ amp: bigint,
435
+ fee: TFractionLike,
436
+ ): TSwapOutcome => {
437
+ if (output <= 0n || inputReserve <= 0n || outputReserve <= 0n)
438
+ throw new Error("Output and reserves must be positive");
439
+ if (rateIn <= 0n || rateOut <= 0n) throw new Error("Rates must be positive");
440
+ if (amp <= 0n) throw new Error("Amplification must be positive");
441
+ if (output >= outputReserve)
442
+ throw new Error("Output must be below the output reserve");
443
+ const [feeNum, feeDen] = asFeeParts(fee);
444
+
445
+ // 1. Smallest gross whose post-fee remainder reaches `output`.
446
+ // gross − ceil(gross·n/d) = floor(gross·(d−n)/d), which is ≥ output
447
+ // exactly when gross ≥ ceil(output·d / (d−n)).
448
+ const grossMin = ceilDiv(output * feeDen, feeDen - feeNum);
449
+ if (grossMin >= outputReserve)
450
+ throw new Error(
451
+ "Output is not reachable: the fee pushes it past the reserve",
452
+ );
453
+
454
+ const d = getD(amp, inputReserve, outputReserve, rateIn, rateOut);
455
+ const scaleIn = rateIn * CALC_PRECISION;
456
+ const scaleOut = rateOut * CALC_PRECISION;
457
+
458
+ // 2. The most the pool may keep of the taken asset and still pay `grossMin`.
459
+ const yTarget = (outputReserve - grossMin) * scaleOut;
460
+ if (yTarget <= 0n)
461
+ throw new Error("Output is not reachable within the pool's reserves");
462
+
463
+ // 3. + 4. The scaled given reserve that supports it, then token units.
464
+ const xNeeded = solveCounterparty(amp, d, yTarget);
465
+ const xBefore = inputReserve * scaleIn;
466
+ let input = ceilDiv(xNeeded - xBefore, scaleIn);
467
+ if (input < 1n) input = 1n;
468
+
469
+ const grossOf = (dx: bigint): bigint =>
470
+ floorDiv(
471
+ getRawSwap(amp, d, inputReserve + dx, outputReserve, rateIn, rateOut),
472
+ scaleOut,
473
+ );
474
+
475
+ let steps = 0;
476
+ while (grossOf(input) < grossMin) {
477
+ input += 1n;
478
+ if (++steps > CORRECTION_MAX_STEPS)
479
+ throw new Error("getSwapInput: the correction walk did not converge");
480
+ }
481
+ while (input > 1n && grossOf(input - 1n) >= grossMin) {
482
+ input -= 1n;
483
+ if (++steps > CORRECTION_MAX_STEPS)
484
+ throw new Error("getSwapInput: the correction walk did not converge");
485
+ }
486
+
487
+ const gross = grossOf(input);
488
+ const lpFee = ceilDiv(gross * feeNum, feeDen);
489
+
490
+ const nextInputReserve = inputReserve + input;
491
+ const nextOutputReserve = outputReserve - output;
492
+
493
+ const xs = xBefore;
494
+ const ys = outputReserve * scaleOut;
495
+ const idealPrice = marginalInputPerOutput(amp, xs, ys, d, rateIn, rateOut);
496
+ const actualPrice = new Fraction(input, gross);
497
+ const priceImpact = Fraction.ONE.subtract(idealPrice.divide(actualPrice));
498
+
499
+ return {
500
+ input,
501
+ output,
502
+ lpFee: new AssetAmount(lpFee, outputMetadata),
503
+ nextInputReserve,
504
+ nextOutputReserve,
505
+ priceImpact,
506
+ };
507
+ };
508
+
509
+ // ─── Deposits ───────────────────────────────────────────────────────────────
510
+
511
+ /**
512
+ * The deposit target a holder of `offered` can cover, and everything pinned to
513
+ * it. This is the target-pinned rule the v4 module enforces (`ss_check.ak`,
514
+ * tag 6), and it is the same shape constant sum uses, stated on `D` instead of
515
+ * on the value sum `V`:
516
+ *
517
+ * - the fill declares a `D` delta `t`;
518
+ * - every reserve moves by `ceil(r_i · t / D_before)`;
519
+ * - `total_lp` becomes `floor(lp_before · (D_before + t) / D_before)`.
520
+ *
521
+ * The largest `t` a holder can cover is capped by the scarcest offered asset,
522
+ * `t = min_i floor(offered_i · D_before / r_i)`. Anything above the pinned
523
+ * deltas is refunded. Every pool asset must be offered, or `t` is zero and the
524
+ * deposit can never fill.
525
+ *
526
+ * The rates do not enter the deltas: a proportional move is proportional in
527
+ * any units. They enter only through `D`.
528
+ */
529
+ export const calculatePinnedDeposit = (
530
+ offered: bigint[],
531
+ reserves: bigint[],
532
+ totalLp: bigint,
533
+ d: bigint,
534
+ ): {
535
+ targetDeltaD: bigint;
536
+ deltas: bigint[];
537
+ generatedLp: bigint;
538
+ nextTotalLp: bigint;
539
+ shareAfterDeposit: Fraction;
540
+ } => {
541
+ if (offered.length !== reserves.length)
542
+ throw new Error("offered and reserves must be aligned");
543
+ if (totalLp <= 0n) throw new Error("Not enough pool liquidity");
544
+ if (d <= 0n) throw new Error("Pool sum invariant is zero");
545
+ if (offered.some((o) => o < 0n))
546
+ throw new Error("Deposit amounts must be non-negative");
547
+
548
+ let t: bigint | undefined;
549
+ for (let i = 0; i < offered.length; i++) {
550
+ if (reserves[i] <= 0n) continue;
551
+ const cap = floorDiv(offered[i] * d, reserves[i]);
552
+ t = t === undefined || cap < t ? cap : t;
553
+ }
554
+ if (t === undefined || t <= 0n)
555
+ throw new Error(
556
+ "A stableswap deposit must offer every pool asset in proportion (asymmetric deposits are disallowed on-chain)",
557
+ );
558
+
559
+ const deltas = reserves.map((r) => ceilDiv(r * t, d));
560
+ const nextTotalLp = floorDiv(totalLp * (d + t), d);
561
+ const generatedLp = nextTotalLp - totalLp;
562
+ if (generatedLp <= 0n) throw new Error("Deposit mints zero LP");
563
+
564
+ return {
565
+ targetDeltaD: t,
566
+ deltas,
567
+ generatedLp,
568
+ nextTotalLp,
569
+ shareAfterDeposit: SharedPoolMath.getShare(generatedLp, nextTotalLp),
570
+ };
571
+ };
572
+
573
+ /**
574
+ * Calculate the deposit parameters for a two-asset v4 stableswap pool through
575
+ * the target-pinned rule (see {@link calculatePinnedDeposit}). Surplus above
576
+ * the pinned deltas comes back as `aChange` / `bChange`, the way the scoop
577
+ * refunds it on-chain.
578
+ *
579
+ * @param a The amount of token A to deposit.
580
+ * @param b The amount of token B to deposit.
581
+ * @param aReserve The pool's reserve of token A.
582
+ * @param bReserve The pool's reserve of token B.
583
+ * @param totalLp The pool's total LP supply before the deposit.
584
+ * @param rateA The pool's rate for token A (from the stableswap config).
585
+ * @param rateB The pool's rate for token B.
586
+ * @param amp The pool's `linear_amplification` (`A`), the raw stored integer.
587
+ */
588
+ export const calculateLiquidity = (
589
+ a: bigint,
590
+ b: bigint,
591
+ aReserve: bigint,
592
+ bReserve: bigint,
593
+ totalLp: bigint,
594
+ rateA: bigint,
595
+ rateB: bigint,
596
+ amp: bigint,
597
+ ) => {
598
+ if (aReserve <= 0n || bReserve <= 0n)
599
+ throw new Error("Not enough pool liquidity");
600
+ const d = getD(amp, aReserve, bReserve, rateA, rateB);
601
+ const { deltas, generatedLp, nextTotalLp, shareAfterDeposit } =
602
+ calculatePinnedDeposit([a, b], [aReserve, bReserve], totalLp, d);
603
+
604
+ return {
605
+ nextTotalLp,
606
+ generatedLp,
607
+ shareAfterDeposit,
608
+ aChange: a - deltas[0],
609
+ bChange: b - deltas[1],
610
+ actualDepositedA: deltas[0],
611
+ actualDepositedB: deltas[1],
612
+ };
613
+ };
614
+
615
+ /**
616
+ * The per-asset amounts a pinned deposit needs when the user anchors on one
617
+ * asset's amount: `t` from the anchor (`floor(anchor · D / r_anchor)`), every
618
+ * other delta ceil-pinned to it. Drives proportional auto-fill in deposit
619
+ * forms.
620
+ */
621
+ export const pinnedDepositFromAnchor = (
622
+ anchorIndex: number,
623
+ anchorAmount: bigint,
624
+ reserves: bigint[],
625
+ d: bigint,
626
+ ): bigint[] => {
627
+ if (anchorIndex < 0 || anchorIndex >= reserves.length)
628
+ throw new Error("anchor index is out of range");
629
+ if (reserves[anchorIndex] <= 0n)
630
+ throw new Error("anchor asset has no reserve");
631
+ if (d <= 0n) throw new Error("Pool sum invariant is zero");
632
+ if (anchorAmount <= 0n) return reserves.map(() => 0n);
633
+
634
+ const t = floorDiv(anchorAmount * d, reserves[anchorIndex]);
635
+ return reserves.map((r, i) =>
636
+ i === anchorIndex ? anchorAmount : ceilDiv(r * t, d),
637
+ );
638
+ };