@sundaeswap/math 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/cjs/PoolMath/ConcentratedLiquidityPool.js +214 -0
  2. package/dist/cjs/PoolMath/ConcentratedLiquidityPool.js.map +1 -0
  3. package/dist/cjs/PoolMath/ConstantSumPool.js +284 -0
  4. package/dist/cjs/PoolMath/ConstantSumPool.js.map +1 -0
  5. package/dist/cjs/PoolMath/V4StableswapPool.js +529 -0
  6. package/dist/cjs/PoolMath/V4StableswapPool.js.map +1 -0
  7. package/dist/cjs/PoolMath/index.js +7 -1
  8. package/dist/cjs/PoolMath/index.js.map +1 -1
  9. package/dist/esm/PoolMath/ConcentratedLiquidityPool.js +181 -0
  10. package/dist/esm/PoolMath/ConcentratedLiquidityPool.js.map +1 -0
  11. package/dist/esm/PoolMath/ConstantSumPool.js +266 -0
  12. package/dist/esm/PoolMath/ConstantSumPool.js.map +1 -0
  13. package/dist/esm/PoolMath/V4StableswapPool.js +495 -0
  14. package/dist/esm/PoolMath/V4StableswapPool.js.map +1 -0
  15. package/dist/esm/PoolMath/index.js +3 -0
  16. package/dist/esm/PoolMath/index.js.map +1 -1
  17. package/dist/types/PoolMath/ConcentratedLiquidityPool.d.ts +60 -0
  18. package/dist/types/PoolMath/ConcentratedLiquidityPool.d.ts.map +1 -0
  19. package/dist/types/PoolMath/ConstantSumPool.d.ts +133 -0
  20. package/dist/types/PoolMath/ConstantSumPool.d.ts.map +1 -0
  21. package/dist/types/PoolMath/V4StableswapPool.d.ts +221 -0
  22. package/dist/types/PoolMath/V4StableswapPool.d.ts.map +1 -0
  23. package/dist/types/PoolMath/index.d.ts +3 -0
  24. package/dist/types/PoolMath/index.d.ts.map +1 -1
  25. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  26. package/package.json +2 -2
  27. package/src/PoolMath/ConcentratedLiquidityPool.ts +275 -0
  28. package/src/PoolMath/ConstantSumPool.ts +371 -0
  29. package/src/PoolMath/V4StableswapPool.ts +638 -0
  30. package/src/PoolMath/__tests__/ConcentratedLiquidityPool.test.ts +225 -0
  31. package/src/PoolMath/__tests__/ConstantSumPool.test.ts +322 -0
  32. package/src/PoolMath/__tests__/V4StableswapPool.test.ts +688 -0
  33. package/src/PoolMath/index.ts +3 -0
@@ -0,0 +1,225 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { IAssetAmountMetadata } from "@sundaeswap/asset";
3
+ import { Fraction } from "@sundaeswap/fraction";
4
+
5
+ import {
6
+ calculateLiquidity,
7
+ getSwapInput,
8
+ getSwapOutput,
9
+ type TSqrtPrice,
10
+ } from "../ConcentratedLiquidityPool.js";
11
+
12
+ const tokenA: IAssetAmountMetadata = {
13
+ assetId: "09169bb6f5ff5b246d65d65935b2222cc53b5e677d7ed22771878972.744f4b454e41",
14
+ decimals: 0,
15
+ };
16
+
17
+ const threePct = new Fraction(3n, 1000n);
18
+ const zeroFee = new Fraction(0n, 1n);
19
+
20
+ // Range [0.5, 2.0] as sqrt-price rationals.
21
+ const spa: TSqrtPrice = [1n, 2n];
22
+ const spb: TSqrtPrice = [2n, 1n];
23
+
24
+ /** Direct evaluation of the on-chain/scooper virtual-reserve swap formula. */
25
+ const refOutput = (
26
+ dx: bigint,
27
+ a: bigint,
28
+ b: bigint,
29
+ l: bigint,
30
+ isAInput: boolean,
31
+ feeNum: bigint,
32
+ feeDen: bigint,
33
+ ): bigint => {
34
+ const fee = (dx * feeNum) / feeDen;
35
+ const dxEff = dx - fee;
36
+ const va0 = a * spb[0] + l * spb[1];
37
+ const vb0 = b * spa[1] + l * spa[0];
38
+ if (isAInput) {
39
+ const dvaEff = dxEff * spb[0];
40
+ return (vb0 * dvaEff) / ((va0 + dvaEff) * spa[1]);
41
+ }
42
+ const dvbEff = dxEff * spa[0];
43
+ return (va0 * dvbEff) / ((vb0 + dvbEff) * spb[0]);
44
+ };
45
+
46
+ describe("ConcentratedLiquidityPool.getSwapOutput", () => {
47
+ it("matches the hand-computed virtual-reserve formula (A→B, no fee)", () => {
48
+ // a=b=L=1000, range [0.5,2]: va0=vb0=3000, dvaEff=200,
49
+ // out = 3000·200 / ((3000+200)·2) = 600000/6400 = 93.75 -> 93.
50
+ const { output } = getSwapOutput(
51
+ tokenA,
52
+ 100n,
53
+ 1000n,
54
+ 1000n,
55
+ 1000n,
56
+ spa,
57
+ spb,
58
+ zeroFee,
59
+ true,
60
+ );
61
+ expect(output).toEqual(93n);
62
+ });
63
+
64
+ it("matches the hand-computed formula (B→A, no fee)", () => {
65
+ // dvbEff=100, out = 3000·100 / ((3000+100)·2) = 300000/6200 = 48.38 -> 48.
66
+ const { output } = getSwapOutput(
67
+ tokenA,
68
+ 100n,
69
+ 1000n,
70
+ 1000n,
71
+ 1000n,
72
+ spa,
73
+ spb,
74
+ zeroFee,
75
+ false,
76
+ );
77
+ expect(output).toEqual(48n);
78
+ });
79
+
80
+ it("agrees with the reference evaluator across sizes and fees", () => {
81
+ for (const isAInput of [true, false]) {
82
+ for (const dx of [1n, 7n, 100n, 5000n, 250000n]) {
83
+ const { output } = getSwapOutput(
84
+ tokenA,
85
+ dx,
86
+ 1_000_000n,
87
+ 1_000_000n,
88
+ 1_000_000n,
89
+ spa,
90
+ spb,
91
+ threePct,
92
+ isAInput,
93
+ );
94
+ expect(output).toEqual(
95
+ refOutput(dx, 1_000_000n, 1_000_000n, 1_000_000n, isAInput, 3n, 1000n),
96
+ );
97
+ }
98
+ }
99
+ });
100
+
101
+ it("caps output at the output reserve", () => {
102
+ const { output } = getSwapOutput(
103
+ tokenA,
104
+ 10_000_000n,
105
+ 100n,
106
+ 5n,
107
+ 1_000_000n,
108
+ spa,
109
+ spb,
110
+ zeroFee,
111
+ true,
112
+ );
113
+ expect(output).toBeLessThanOrEqual(5n);
114
+ });
115
+
116
+ it("throws on non-positive input/reserves/liquidity", () => {
117
+ expect(() =>
118
+ getSwapOutput(tokenA, 0n, 10n, 10n, 10n, spa, spb, zeroFee, true),
119
+ ).toThrow();
120
+ expect(() =>
121
+ getSwapOutput(tokenA, 1n, 10n, 10n, 0n, spa, spb, zeroFee, true),
122
+ ).toThrow();
123
+ });
124
+ });
125
+
126
+ describe("ConcentratedLiquidityPool.getSwapInput", () => {
127
+ it("returns the minimal input whose forward swap clears the target", () => {
128
+ for (const isAInput of [true, false]) {
129
+ for (const targetOut of [1n, 40n, 93n, 1000n, 90_000n]) {
130
+ const { input } = getSwapInput(
131
+ tokenA,
132
+ targetOut,
133
+ 1_000_000n,
134
+ 1_000_000n,
135
+ 1_000_000n,
136
+ spa,
137
+ spb,
138
+ threePct,
139
+ isAInput,
140
+ );
141
+ const got = getSwapOutput(
142
+ tokenA,
143
+ input,
144
+ 1_000_000n,
145
+ 1_000_000n,
146
+ 1_000_000n,
147
+ spa,
148
+ spb,
149
+ threePct,
150
+ isAInput,
151
+ ).output;
152
+ // Reaches the target...
153
+ expect(got).toBeGreaterThanOrEqual(targetOut);
154
+ // ...and is minimal: one unit less falls short.
155
+ const less = getSwapOutput(
156
+ tokenA,
157
+ input - 1n,
158
+ 1_000_000n,
159
+ 1_000_000n,
160
+ 1_000_000n,
161
+ spa,
162
+ spb,
163
+ threePct,
164
+ isAInput,
165
+ ).output;
166
+ expect(less).toBeLessThan(targetOut);
167
+ }
168
+ }
169
+ });
170
+
171
+ it("throws when the output exceeds the reserve", () => {
172
+ expect(() =>
173
+ getSwapInput(tokenA, 200n, 1000n, 100n, 1000n, spa, spb, threePct, true),
174
+ ).toThrow();
175
+ });
176
+ });
177
+
178
+ describe("ConcentratedLiquidityPool.calculateLiquidity", () => {
179
+ // The on-chain CL non-swap invariant a proportional deposit must satisfy.
180
+ const invariantHolds = (
181
+ a0: bigint,
182
+ b0: bigint,
183
+ l0: bigint,
184
+ da: bigint,
185
+ db: bigint,
186
+ dl: bigint,
187
+ ): boolean => {
188
+ const va0 = a0 * spb[0] + l0 * spb[1];
189
+ const vb0 = b0 * spa[1] + l0 * spa[0];
190
+ const a1 = a0 + da;
191
+ const b1 = b0 + db;
192
+ const l1 = l0 + dl;
193
+ const va1 = a1 * spb[0] + l1 * spb[1];
194
+ const vb1 = b1 * spa[1] + l1 * spa[0];
195
+ return va1 * vb1 * l0 * l0 >= va0 * vb0 * l1 * l1;
196
+ };
197
+
198
+ it("mints proportionally and the fill satisfies the CL invariant", () => {
199
+ const cases: Array<[bigint, bigint, bigint, bigint, bigint]> = [
200
+ // [offeredA, offeredB, reserveA, reserveB, totalLp]
201
+ [100n, 100n, 1000n, 1000n, 1000n],
202
+ [100n, 250n, 1000n, 1000n, 1000n], // B surplus
203
+ [500n, 100n, 3333n, 991n, 1777n], // coprime-ish reserves
204
+ [12345n, 67890n, 1_000_000n, 5_000_000n, 2_000_000n],
205
+ ];
206
+ for (const [a, b, ra, rb, lp] of cases) {
207
+ const res = calculateLiquidity(a, b, ra, rb, lp);
208
+ expect(res.generatedLp).toBeGreaterThan(0n);
209
+ // Never consumes more than offered.
210
+ expect(res.actualDepositedA).toBeLessThanOrEqual(a);
211
+ expect(res.actualDepositedB).toBeLessThanOrEqual(b);
212
+ // The resulting deposit is chain-valid under the CL invariant.
213
+ expect(
214
+ invariantHolds(
215
+ ra,
216
+ rb,
217
+ lp,
218
+ res.actualDepositedA,
219
+ res.actualDepositedB,
220
+ res.generatedLp,
221
+ ),
222
+ ).toBe(true);
223
+ }
224
+ });
225
+ });
@@ -0,0 +1,322 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { AssetAmount, IAssetAmountMetadata } from "@sundaeswap/asset";
3
+ import { Fraction } from "@sundaeswap/fraction";
4
+
5
+ import {
6
+ calculateDepositN,
7
+ calculateLiquidity,
8
+ calculatePinnedDeposit,
9
+ getSwapInput,
10
+ getSwapOutput,
11
+ pinnedDepositFromAnchor,
12
+ } from "../ConstantSumPool.js";
13
+
14
+ const tokenA: IAssetAmountMetadata = {
15
+ assetId: "09169bb6f5ff5b246d65d65935b2222cc53b5e677d7ed22771878972.744f4b454e41",
16
+ decimals: 0,
17
+ };
18
+
19
+ const threePct = new Fraction(3n, 1000n);
20
+
21
+ describe("ConstantSumPool.getSwapOutput", () => {
22
+ it("throws on non-positive input, reserves, or prices", () => {
23
+ expect(() =>
24
+ getSwapOutput(tokenA, -1n, 10n, 10n, 1n, 1n, threePct),
25
+ ).toThrow();
26
+ expect(() =>
27
+ getSwapOutput(tokenA, 1n, -10n, 10n, 1n, 1n, threePct),
28
+ ).toThrow();
29
+ expect(() =>
30
+ getSwapOutput(tokenA, 1n, 10n, 10n, 0n, 1n, threePct),
31
+ ).toThrow();
32
+ });
33
+
34
+ it("throws when the fee is out of [0,1]", () => {
35
+ expect(() =>
36
+ getSwapOutput(tokenA, 1n, 10n, 10n, 1n, 1n, Fraction.asFraction(1.1)),
37
+ ).toThrow();
38
+ });
39
+
40
+ // Vectors cross-checked against the scooper's cs_swap_result and the live
41
+ // pools.quote endpoint (sundae-protocol). out = floor((dx·priceIn −
42
+ // floor(dx·priceIn·fee)) / priceOut).
43
+ it("matches the scooper at par prices (equal prices, 0.3% fee)", () => {
44
+ // 10000 in, prices 1e6/1e6, fee 3/1000 -> 9970 out.
45
+ const { output } = getSwapOutput(
46
+ tokenA,
47
+ 10_000n,
48
+ 1_000_000_000n,
49
+ 1_000_000_000n,
50
+ 1_000_000n,
51
+ 1_000_000n,
52
+ threePct,
53
+ );
54
+ expect(output).toEqual(9970n);
55
+ });
56
+
57
+ it("matches the live quote for skewed prices", () => {
58
+ // priceIn=7, priceOut=3, 1000 in -> 2326 (tOKENA->tOKENB on preview).
59
+ expect(
60
+ getSwapOutput(tokenA, 1_000n, 1_000_000n, 1_000_000n, 7n, 3n, threePct)
61
+ .output,
62
+ ).toEqual(2326n);
63
+ // priceIn=3, priceOut=5 -> 598 (tOKENB->ADAb).
64
+ expect(
65
+ getSwapOutput(tokenA, 1_000n, 1_000_000n, 1_000_000n, 3n, 5n, threePct)
66
+ .output,
67
+ ).toEqual(598n);
68
+ // priceIn=5, priceOut=3 -> 1661 (ADAb->tOKENB).
69
+ expect(
70
+ getSwapOutput(tokenA, 1_000n, 1_000_000n, 1_000_000n, 5n, 3n, threePct)
71
+ .output,
72
+ ).toEqual(1661n);
73
+ });
74
+
75
+ it("caps the output at the output reserve", () => {
76
+ const { output } = getSwapOutput(
77
+ tokenA,
78
+ 1_000_000n,
79
+ 10n,
80
+ 5n, // tiny output reserve
81
+ 1_000_000n,
82
+ 1n,
83
+ threePct,
84
+ );
85
+ expect(output).toEqual(5n);
86
+ });
87
+
88
+ it("reports the lp fee in input-asset units", () => {
89
+ const { lpFee } = getSwapOutput(
90
+ tokenA,
91
+ 10_000n,
92
+ 1_000_000_000n,
93
+ 1_000_000_000n,
94
+ 1_000_000n,
95
+ 1_000_000n,
96
+ threePct,
97
+ );
98
+ // 10000 * 3/1000 = 30 (input units).
99
+ expect(lpFee.amount).toEqual(30n);
100
+ expect(lpFee.metadata.assetId).toEqual(tokenA.assetId);
101
+ });
102
+ });
103
+
104
+ describe("ConstantSumPool.getSwapInput", () => {
105
+ it("throws on non-positive output, reserves, or prices", () => {
106
+ expect(() => getSwapInput(tokenA, 0n, 10n, 10n, 1n, 1n, threePct)).toThrow();
107
+ expect(() => getSwapInput(tokenA, 1n, 0n, 10n, 1n, 1n, threePct)).toThrow();
108
+ expect(() => getSwapInput(tokenA, 1n, 10n, 10n, 1n, 0n, threePct)).toThrow();
109
+ });
110
+
111
+ it("throws when the output exceeds the output reserve, but allows draining it", () => {
112
+ expect(() =>
113
+ getSwapInput(tokenA, 11n, 1_000n, 10n, 1n, 1n, threePct),
114
+ ).toThrow();
115
+ expect(
116
+ getSwapInput(tokenA, 10n, 1_000n, 10n, 1n, 1n, threePct).input,
117
+ ).toBeGreaterThan(0n);
118
+ });
119
+
120
+ it("throws when the fee is 1 or more", () => {
121
+ expect(() =>
122
+ getSwapInput(tokenA, 1n, 10n, 10n, 1n, 1n, Fraction.ONE),
123
+ ).toThrow();
124
+ });
125
+
126
+ it("inverts getSwapOutput at par prices", () => {
127
+ // Forward: 10000 in -> 9970 out. Inverse of 9970 must be minimal.
128
+ const { input } = getSwapInput(
129
+ tokenA,
130
+ 9_970n,
131
+ 1_000_000_000n,
132
+ 1_000_000_000n,
133
+ 1_000_000n,
134
+ 1_000_000n,
135
+ threePct,
136
+ );
137
+ expect(
138
+ getSwapOutput(
139
+ tokenA,
140
+ input,
141
+ 1_000_000_000n,
142
+ 1_000_000_000n,
143
+ 1_000_000n,
144
+ 1_000_000n,
145
+ threePct,
146
+ ).output,
147
+ ).toBeGreaterThanOrEqual(9_970n);
148
+ expect(input).toBeLessThanOrEqual(10_000n);
149
+ });
150
+
151
+ it("returns the minimal input across skewed prices and fees", () => {
152
+ const vectors: Array<[bigint, bigint, bigint, Fraction]> = [
153
+ [2_326n, 7n, 3n, threePct],
154
+ [598n, 3n, 5n, threePct],
155
+ [1_661n, 5n, 3n, threePct],
156
+ [1_000n, 1n, 1n, Fraction.ZERO],
157
+ [12_345n, 1_000_000n, 999_000n, new Fraction(1n, 100n)],
158
+ ];
159
+ for (const [output, priceIn, priceOut, fee] of vectors) {
160
+ const { input } = getSwapInput(
161
+ tokenA,
162
+ output,
163
+ 1_000_000_000n,
164
+ 1_000_000_000n,
165
+ priceIn,
166
+ priceOut,
167
+ fee,
168
+ );
169
+ const forward = (candidate: bigint) =>
170
+ getSwapOutput(
171
+ tokenA,
172
+ candidate,
173
+ 1_000_000_000n,
174
+ 1_000_000_000n,
175
+ priceIn,
176
+ priceOut,
177
+ fee,
178
+ ).output;
179
+ // Sufficient: the computed input actually yields the requested output…
180
+ expect(forward(input)).toBeGreaterThanOrEqual(output);
181
+ // …and minimal: one unit less does not.
182
+ if (input > 1n) {
183
+ expect(forward(input - 1n)).toBeLessThan(output);
184
+ }
185
+ }
186
+ });
187
+ });
188
+
189
+ describe("ConstantSumPool.calculateLiquidity", () => {
190
+ // Target-pinned (the deployed validator's rule): the mint is capped by the
191
+ // scarcest offered asset; extra above the pinned deltas is refunded.
192
+ it("mints by the scarcest asset and refunds the excess", () => {
193
+ const result = calculateLiquidity(
194
+ 100n,
195
+ 50n,
196
+ 1_000n,
197
+ 2_000n,
198
+ 5_000n,
199
+ 3n,
200
+ 5n,
201
+ );
202
+ // V_b = 3000 + 10000 = 13000; t = min(100·13000/1000, 50·13000/2000)
203
+ // = min(1300, 325) = 325.
204
+ // deltas = [ceil(1000·325/13000), ceil(2000·325/13000)] = [25, 50].
205
+ // after_lp = floor(5000·13325/13000) = 5125 -> minted 125.
206
+ expect(result.generatedLp).toEqual(125n);
207
+ expect(result.nextTotalLp).toEqual(5_125n);
208
+ expect(result.actualDepositedA).toEqual(25n);
209
+ expect(result.actualDepositedB).toEqual(50n);
210
+ expect(result.aChange).toEqual(75n);
211
+ expect(result.bChange).toEqual(0n);
212
+ });
213
+
214
+ it("rejects single-sided deposits (asymmetric deposits can't validate)", () => {
215
+ expect(() =>
216
+ calculateLiquidity(0n, 100n, 1_000n, 1_000n, 2_000n, 1n, 1n),
217
+ ).toThrow(/every pool asset/);
218
+ });
219
+
220
+ it("N-asset: values the deposit against ALL reserves, not just the touched pair", () => {
221
+ // 3-asset pool, unit prices: totalValue = 1000 + 2000 + 7000 = 10000.
222
+ // Depositing 100 of asset0 only: lp = 100·5000/10000 = 50 — NOT the
223
+ // 100·5000/3000 = 166 a pair-only totalValue would claim.
224
+ const result = calculateDepositN(
225
+ [100n, 0n, 0n],
226
+ [1_000n, 2_000n, 7_000n],
227
+ [1n, 1n, 1n],
228
+ 5_000n,
229
+ );
230
+ expect(result.generatedLp).toEqual(50n);
231
+ expect(result.nextTotalLp).toEqual(5_050n);
232
+ });
233
+
234
+ it("the 2-asset wrapper matches the pinned N-asset form", () => {
235
+ const viaWrapper = calculateLiquidity(
236
+ 100n,
237
+ 50n,
238
+ 1_000n,
239
+ 2_000n,
240
+ 5_000n,
241
+ 3n,
242
+ 5n,
243
+ );
244
+ const viaPinned = calculatePinnedDeposit(
245
+ [100n, 50n],
246
+ [1_000n, 2_000n],
247
+ [3n, 5n],
248
+ 5_000n,
249
+ );
250
+ expect(viaWrapper.generatedLp).toEqual(viaPinned.generatedLp);
251
+ expect(viaWrapper.nextTotalLp).toEqual(viaPinned.nextTotalLp);
252
+ });
253
+
254
+ it("N-asset: rejects misaligned arrays", () => {
255
+ expect(() =>
256
+ calculateDepositN([1n, 2n], [10n, 20n, 30n], [1n, 1n, 1n], 100n),
257
+ ).toThrow();
258
+ });
259
+
260
+ it("pinned: mints by the scarcest asset, deltas ceil-pinned, surplus implied", () => {
261
+ // Live preview c618 shape: coprime-ish reserves, unit prices.
262
+ const reserves = [26_018_415_418n, 1_997_073_427n, 2_000_535_565n];
263
+ const prices = [1n, 1n, 1n];
264
+ const totalLp = 30_016_024_410n;
265
+ // Equal offers: t capped by the LARGEST reserve's coverage (USDR-like).
266
+ const offered = [100_000_000n, 100_000_000n, 100_000_000n];
267
+ const result = calculatePinnedDeposit(offered, reserves, prices, totalLp);
268
+
269
+ const vB = reserves[0] + reserves[1] + reserves[2];
270
+ const expectedT = (offered[0] * vB) / reserves[0]; // scarcest coverage
271
+ expect(result.targetDeltaV).toEqual(expectedT);
272
+ // Deltas never exceed the offers, and match the validator's ceil pin.
273
+ result.deltas.forEach((delta, i) => {
274
+ expect(delta).toBeLessThanOrEqual(offered[i]);
275
+ expect(delta).toEqual(
276
+ (reserves[i] * result.targetDeltaV + vB - 1n) / vB,
277
+ );
278
+ });
279
+ expect(result.generatedLp).toEqual(
280
+ (totalLp * (vB + result.targetDeltaV)) / vB - totalLp,
281
+ );
282
+ // The plain value formula would overstate the mint for this offer.
283
+ const naive = calculateDepositN(offered, reserves, prices, totalLp);
284
+ expect(naive.generatedLp).toBeGreaterThan(result.generatedLp);
285
+ });
286
+
287
+ it("pinned: rejects a deposit missing any pool asset", () => {
288
+ expect(() =>
289
+ calculatePinnedDeposit(
290
+ [100n, 0n, 100n],
291
+ [1_000n, 2_000n, 3_000n],
292
+ [1n, 1n, 1n],
293
+ 6_000n,
294
+ ),
295
+ ).toThrow(/every pool asset/);
296
+ });
297
+
298
+ it("pinned: anchor auto-fill produces amounts the pin accepts at full t", () => {
299
+ const reserves = [26_018_415_418n, 1_997_073_427n, 2_000_535_565n];
300
+ const prices = [1n, 1n, 1n];
301
+ const totalLp = 30_016_024_410n;
302
+ const anchor = 100_000_000n;
303
+ const filled = pinnedDepositFromAnchor(1, anchor, reserves, prices);
304
+ expect(filled[1]).toEqual(anchor);
305
+ // Depositing exactly the auto-filled amounts reaches (at least) the
306
+ // anchor's implied t — nothing is scarcer than the anchor.
307
+ const vB = reserves[0] + reserves[1] + reserves[2];
308
+ const anchorT = (anchor * vB) / reserves[1];
309
+ const result = calculatePinnedDeposit(filled, reserves, prices, totalLp);
310
+ expect(result.targetDeltaV).toBeGreaterThanOrEqual(anchorT);
311
+ });
312
+
313
+ it("throws on empty deposits, empty pools, or bad prices", () => {
314
+ expect(() =>
315
+ calculateLiquidity(0n, 0n, 1_000n, 1_000n, 2_000n, 1n, 1n),
316
+ ).toThrow();
317
+ expect(() => calculateLiquidity(1n, 1n, 0n, 0n, 0n, 1n, 1n)).toThrow();
318
+ expect(() =>
319
+ calculateLiquidity(1n, 1n, 1_000n, 1_000n, 2_000n, 0n, 1n),
320
+ ).toThrow();
321
+ });
322
+ });