lovewebconnection 0.0.0-stage → 0.2.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 (68) hide show
  1. package/.env.example +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1915 -2
  4. package/dist/alert-queue.d.ts +72 -0
  5. package/dist/alert-queue.js +155 -0
  6. package/dist/alert-store.d.ts +29 -0
  7. package/dist/alert-store.js +75 -0
  8. package/dist/balances.d.ts +221 -0
  9. package/dist/balances.js +1232 -0
  10. package/dist/cache.d.ts +37 -0
  11. package/dist/cache.js +128 -0
  12. package/dist/call.d.ts +46 -0
  13. package/dist/call.js +377 -0
  14. package/dist/chain.d.ts +97 -0
  15. package/dist/chain.js +419 -0
  16. package/dist/cli.d.ts +2 -0
  17. package/dist/cli.js +1011 -0
  18. package/dist/compare-history.d.ts +82 -0
  19. package/dist/compare-history.js +340 -0
  20. package/dist/compare.d.ts +98 -0
  21. package/dist/compare.js +492 -0
  22. package/dist/contract.d.ts +107 -0
  23. package/dist/contract.js +162 -0
  24. package/dist/default-abi.d.ts +53 -0
  25. package/dist/default-abi.js +85 -0
  26. package/dist/history.d.ts +83 -0
  27. package/dist/history.js +422 -0
  28. package/dist/index.d.ts +54 -0
  29. package/dist/index.js +27 -0
  30. package/dist/market-page.d.ts +8 -0
  31. package/dist/market-page.js +554 -0
  32. package/dist/market-server.d.ts +46 -0
  33. package/dist/market-server.js +473 -0
  34. package/dist/market.d.ts +323 -0
  35. package/dist/market.js +835 -0
  36. package/dist/metadata-cache.d.ts +72 -0
  37. package/dist/metadata-cache.js +197 -0
  38. package/dist/metrics.d.ts +64 -0
  39. package/dist/metrics.js +138 -0
  40. package/dist/plot.d.ts +60 -0
  41. package/dist/plot.js +317 -0
  42. package/dist/presets-history.d.ts +372 -0
  43. package/dist/presets-history.js +1243 -0
  44. package/dist/presets-summary.d.ts +17 -0
  45. package/dist/presets-summary.js +55 -0
  46. package/dist/presets.d.ts +59 -0
  47. package/dist/presets.js +181 -0
  48. package/dist/prices.d.ts +63 -0
  49. package/dist/prices.js +154 -0
  50. package/dist/rotate.d.ts +19 -0
  51. package/dist/rotate.js +34 -0
  52. package/dist/scan.d.ts +81 -0
  53. package/dist/scan.js +501 -0
  54. package/dist/sessions.d.ts +104 -0
  55. package/dist/sessions.js +515 -0
  56. package/dist/signatures.d.ts +13 -0
  57. package/dist/signatures.js +43 -0
  58. package/dist/tokenfile.d.ts +50 -0
  59. package/dist/tokenfile.js +218 -0
  60. package/dist/views.d.ts +63 -0
  61. package/dist/views.js +415 -0
  62. package/dist/watch.d.ts +306 -0
  63. package/dist/watch.js +1121 -0
  64. package/dist/webhook.d.ts +17 -0
  65. package/dist/webhook.js +35 -0
  66. package/examples/erc20.abi.json +57 -0
  67. package/examples/xlayer.tokens.json +4 -0
  68. package/package.json +77 -4
package/dist/market.js ADDED
@@ -0,0 +1,835 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { formatUnits, parseUnits } from "viem";
4
+ export class MarketError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.name = "MarketError";
9
+ this.code = code;
10
+ }
11
+ }
12
+ export class MarketArgError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "MarketArgError";
16
+ }
17
+ }
18
+ const BPS = 10000n;
19
+ const MAX_FEE_BPS = 1_000;
20
+ const MAX_DECIMALS = 36;
21
+ /** Where `lwc market --serve` listens unless told otherwise. */
22
+ export const DEFAULT_MARKET_HOST = "127.0.0.1";
23
+ export const DEFAULT_MARKET_PORT = 8787;
24
+ export const MAX_MARKET_PORT = 65_535;
25
+ /** Requests per minute one client may make unless `--rate-limit` says otherwise. */
26
+ export const DEFAULT_MARKET_RATE_LIMIT = 120;
27
+ const MAX_RATE_LIMIT = 100_000;
28
+ function reduce(num, den) {
29
+ const left = num < 0n ? -num : num;
30
+ const right = den < 0n ? -den : den;
31
+ let a = left;
32
+ let b = right;
33
+ while (b !== 0n) {
34
+ const rest = a % b;
35
+ a = b;
36
+ b = rest;
37
+ }
38
+ const divisor = a === 0n ? 1n : a;
39
+ return { num: num / divisor, den: den / divisor };
40
+ }
41
+ function pow10(exponent) {
42
+ return 10n ** BigInt(exponent);
43
+ }
44
+ /** Render a rational as a decimal string, without trailing zeros. */
45
+ export function rationalToDecimal(num, den, maxDecimals = 18) {
46
+ if (den === 0n)
47
+ throw new MarketArgError("cannot render a price over zero");
48
+ const negative = num < 0n !== den < 0n;
49
+ const top = num < 0n ? -num : num;
50
+ const bottom = den < 0n ? -den : den;
51
+ const whole = top / bottom;
52
+ let remainder = top % bottom;
53
+ let fraction = "";
54
+ for (let digit = 0; digit < maxDecimals && remainder !== 0n; digit += 1) {
55
+ remainder *= 10n;
56
+ fraction += String(remainder / bottom);
57
+ remainder %= bottom;
58
+ }
59
+ const text = fraction.length > 0 ? `${whole}.${fraction}` : `${whole}`;
60
+ return negative ? `-${text}` : text;
61
+ }
62
+ /**
63
+ * Turn a human price into the pool's rational. `"28.842"` with 8 base decimals
64
+ * and 6 quote decimals means one whole base token is worth 28.842 quote tokens,
65
+ * and the rational is expressed in raw units of each token.
66
+ */
67
+ export function fixedPrice(human, baseDecimals, quoteDecimals) {
68
+ const trimmed = human.trim();
69
+ const match = /^(\d+)(?:\.(\d+))?$/.exec(trimmed);
70
+ if (!match) {
71
+ throw new MarketArgError(`price must be a positive decimal number, got "${human}".`);
72
+ }
73
+ const whole = match[1] ?? "0";
74
+ const fraction = match[2] ?? "";
75
+ const value = BigInt(`${whole}${fraction}`);
76
+ if (value === 0n)
77
+ throw new MarketArgError("price must be greater than zero.");
78
+ return reduce(value * pow10(quoteDecimals), pow10(fraction.length) * pow10(baseDecimals));
79
+ }
80
+ /** Share counts are whole units, so they never take decimals. */
81
+ export function parseShares(value) {
82
+ const trimmed = value.trim();
83
+ if (!/^\d+$/.test(trimmed)) {
84
+ throw new MarketArgError(`shares must be a whole number, got "${value}".`);
85
+ }
86
+ const shares = BigInt(trimmed);
87
+ if (shares <= 0n)
88
+ throw new MarketArgError("shares must be greater than zero.");
89
+ return shares;
90
+ }
91
+ /** Human price of a rational, quote per one whole base token. */
92
+ export function humanPrice(num, den, baseDecimals, quoteDecimals) {
93
+ return rationalToDecimal(num * pow10(baseDecimals), den * pow10(quoteDecimals));
94
+ }
95
+ /** A decimal amount as base units, with the pool's own decimals. */
96
+ export function parseMarketAmount(value, decimals, label) {
97
+ const trimmed = value.trim();
98
+ if (!/^\d+(?:\.\d+)?$/.test(trimmed)) {
99
+ throw new MarketArgError(`${label} must be a positive decimal amount, got "${value}".`);
100
+ }
101
+ const point = trimmed.indexOf(".");
102
+ const places = point === -1 ? 0 : trimmed.length - point - 1;
103
+ if (places > decimals) {
104
+ throw new MarketArgError(`${label} "${value}" needs at most ${decimals} decimal place${decimals === 1 ? "" : "s"}.`);
105
+ }
106
+ let amount;
107
+ try {
108
+ amount = parseUnits(trimmed, decimals);
109
+ }
110
+ catch (error) {
111
+ throw new MarketArgError(`${label} "${value}" needs at most ${decimals} decimal place${decimals === 1 ? "" : "s"}.`);
112
+ }
113
+ if (amount <= 0n)
114
+ throw new MarketArgError(`${label} must be greater than zero.`);
115
+ return amount;
116
+ }
117
+ /** Base units as a trimmed decimal string. */
118
+ export function formatMarketAmount(value, decimals) {
119
+ const text = formatUnits(value, decimals);
120
+ if (!text.includes("."))
121
+ return text;
122
+ return text.replace(/0+$/, "").replace(/\.$/, "");
123
+ }
124
+ function assertDecimals(value, label) {
125
+ if (!Number.isInteger(value) || value < 0 || value > MAX_DECIMALS) {
126
+ throw new MarketArgError(`${label} must be a whole number between 0 and ${MAX_DECIMALS}.`);
127
+ }
128
+ }
129
+ function assertFeeBps(value) {
130
+ if (!Number.isInteger(value) || value < 0 || value > MAX_FEE_BPS) {
131
+ throw new MarketArgError(`fee must be a whole number of basis points between 0 and ${MAX_FEE_BPS}.`);
132
+ }
133
+ }
134
+ function bigintField(raw, name) {
135
+ if (typeof raw !== "string" || !/^-?\d+$/.test(raw)) {
136
+ throw new MarketArgError(`pool file field "${name}" must be a decimal integer string.`);
137
+ }
138
+ return BigInt(raw);
139
+ }
140
+ function stringField(raw, name) {
141
+ if (typeof raw !== "string" || raw.trim().length === 0) {
142
+ throw new MarketArgError(`pool file field "${name}" must be a non-empty string.`);
143
+ }
144
+ return raw;
145
+ }
146
+ function numberField(raw, name) {
147
+ if (typeof raw !== "number" || !Number.isFinite(raw)) {
148
+ throw new MarketArgError(`pool file field "${name}" must be a number.`);
149
+ }
150
+ return raw;
151
+ }
152
+ /** Convert a parsed pool file into runtime state, validating every field. */
153
+ export function poolFromJson(raw) {
154
+ if (typeof raw !== "object" || raw === null) {
155
+ throw new MarketArgError("pool file must contain a JSON object.");
156
+ }
157
+ const record = raw;
158
+ if (record.version !== 1) {
159
+ throw new MarketArgError(`unsupported pool file version ${String(record.version)}; expected 1.`);
160
+ }
161
+ const config = {
162
+ baseSymbol: stringField(record.baseSymbol, "baseSymbol"),
163
+ quoteSymbol: stringField(record.quoteSymbol, "quoteSymbol"),
164
+ baseDecimals: numberField(record.baseDecimals, "baseDecimals"),
165
+ quoteDecimals: numberField(record.quoteDecimals, "quoteDecimals"),
166
+ priceNum: bigintField(record.priceNum, "priceNum"),
167
+ priceDen: bigintField(record.priceDen, "priceDen"),
168
+ feeBps: numberField(record.feeBps, "feeBps"),
169
+ };
170
+ return new MarketPool(config, {
171
+ baseReserve: bigintField(record.baseReserve, "baseReserve"),
172
+ quoteReserve: bigintField(record.quoteReserve, "quoteReserve"),
173
+ shares: bigintField(record.shares, "shares"),
174
+ volumeQuote: bigintField(record.volumeQuote, "volumeQuote"),
175
+ swaps: numberField(record.swaps, "swaps"),
176
+ openedBase: bigintField(record.openedBase, "openedBase"),
177
+ openedQuote: bigintField(record.openedQuote, "openedQuote"),
178
+ });
179
+ }
180
+ /** Read a pool file from disk, validating every field. */
181
+ export function loadPoolFile(path) {
182
+ let text;
183
+ try {
184
+ text = readFileSync(path, "utf8");
185
+ }
186
+ catch (error) {
187
+ const reason = error instanceof Error ? error.message : String(error);
188
+ throw new MarketArgError(`cannot read pool file ${path}: ${reason}`);
189
+ }
190
+ let parsed;
191
+ try {
192
+ parsed = JSON.parse(text);
193
+ }
194
+ catch {
195
+ throw new MarketArgError(`pool file ${path} is not valid JSON.`);
196
+ }
197
+ return poolFromJson(parsed);
198
+ }
199
+ /** Write a pool file atomically, refusing to replace one without `force`. */
200
+ export function savePoolFile(path, pool, options = {}) {
201
+ const json = pool.toJson();
202
+ const text = `${JSON.stringify(json, null, 2)}\n`;
203
+ if (!options.force && existsSync(path)) {
204
+ throw new MarketArgError(`${path} already exists; pass --force to replace it.`);
205
+ }
206
+ try {
207
+ mkdirSync(dirname(path), { recursive: true });
208
+ const temporary = `${path}.tmp`;
209
+ writeFileSync(temporary, text);
210
+ renameSync(temporary, path);
211
+ }
212
+ catch (error) {
213
+ const reason = error instanceof Error ? error.message : String(error);
214
+ throw new MarketArgError(`cannot write ${path}: ${reason}`);
215
+ }
216
+ return json;
217
+ }
218
+ /**
219
+ * A fixed-price settlement pool. Reserves only move when flow is net
220
+ * directional, so matched volume costs the pool nothing.
221
+ */
222
+ export class MarketPool {
223
+ config;
224
+ baseReserve;
225
+ quoteReserve;
226
+ sharesOut;
227
+ volumeQuote;
228
+ swapCount;
229
+ openedBase;
230
+ openedQuote;
231
+ constructor(config, state) {
232
+ assertDecimals(config.baseDecimals, "base decimals");
233
+ assertDecimals(config.quoteDecimals, "quote decimals");
234
+ assertFeeBps(config.feeBps);
235
+ if (config.priceNum <= 0n || config.priceDen <= 0n) {
236
+ throw new MarketArgError("price numerator and denominator must both be positive.");
237
+ }
238
+ if (config.baseSymbol.trim().length === 0 || config.quoteSymbol.trim().length === 0) {
239
+ throw new MarketArgError("base and quote symbols are required.");
240
+ }
241
+ this.config = config;
242
+ this.baseReserve = state?.baseReserve ?? 0n;
243
+ this.quoteReserve = state?.quoteReserve ?? 0n;
244
+ this.sharesOut = state?.shares ?? 0n;
245
+ this.volumeQuote = state?.volumeQuote ?? 0n;
246
+ this.swapCount = state?.swaps ?? 0;
247
+ this.openedBase = state?.openedBase ?? this.baseReserve;
248
+ this.openedQuote = state?.openedQuote ?? this.quoteReserve;
249
+ for (const [label, value] of [
250
+ ["base reserve", this.baseReserve],
251
+ ["quote reserve", this.quoteReserve],
252
+ ["shares", this.sharesOut],
253
+ ["volume", this.volumeQuote],
254
+ ]) {
255
+ if (value < 0n)
256
+ throw new MarketArgError(`pool file ${label} cannot be negative.`);
257
+ }
258
+ if (!Number.isInteger(this.swapCount) || this.swapCount < 0) {
259
+ throw new MarketArgError("pool file swap count must be a whole number.");
260
+ }
261
+ }
262
+ /**
263
+ * Create a pool worth `liquidity` of the quote token, split evenly between the
264
+ * two legs at the fixed price, so one dollar on deposit really is one dollar.
265
+ */
266
+ static create(config, liquidity) {
267
+ if (liquidity <= 0n)
268
+ throw new MarketArgError("seed liquidity must be greater than zero.");
269
+ const quoteSide = liquidity / 2n;
270
+ const quoteRemainder = liquidity - quoteSide;
271
+ // Round the base leg up so the pool never opens a raw unit short of the seed.
272
+ const baseSide = (quoteRemainder * config.priceDen + config.priceNum - 1n) / config.priceNum;
273
+ if (baseSide <= 0n || quoteSide <= 0n) {
274
+ throw new MarketArgError("seed liquidity is too small to fund both legs at this price; use a larger --seed.");
275
+ }
276
+ const shares = quoteSide + (baseSide * config.priceNum) / config.priceDen;
277
+ return new MarketPool(config, { baseReserve: baseSide, quoteReserve: quoteSide, shares });
278
+ }
279
+ /** Price of the base token in quote units, in raw units of each token. */
280
+ get ratio() {
281
+ return { num: this.config.priceNum, den: this.config.priceDen };
282
+ }
283
+ /** Quote base units for a given base amount, at the fixed price (rounded down). */
284
+ quoteForBase(baseAmount) {
285
+ return (baseAmount * this.ratio.num) / this.ratio.den;
286
+ }
287
+ /** Base units for a given quote amount, at the fixed price (rounded down). */
288
+ baseForQuote(quoteAmount) {
289
+ return (quoteAmount * this.ratio.den) / this.ratio.num;
290
+ }
291
+ /** Value of both reserves at the fixed price, in quote base units. */
292
+ liquidityValueQuote() {
293
+ return this.quoteReserve + this.quoteForBase(this.baseReserve);
294
+ }
295
+ /**
296
+ * Price a trade without touching the pool. A well-formed amount always gets a
297
+ * quote; `executable` and `reason` say whether the pool can settle it.
298
+ */
299
+ quote(side, amountIn) {
300
+ if (amountIn <= 0n) {
301
+ throw new MarketError("invalid-amount", "amountIn must be greater than zero.");
302
+ }
303
+ const fee = (amountIn * BigInt(this.config.feeBps)) / BPS;
304
+ const net = amountIn - fee;
305
+ const notional = side === "buy" ? amountIn : this.quoteForBase(amountIn);
306
+ let amountOut;
307
+ let reservesAfter;
308
+ if (side === "sell") {
309
+ // Base in: the fee stays in base, the rest is paid at the fixed price.
310
+ amountOut = this.quoteForBase(net);
311
+ reservesAfter = {
312
+ base: this.baseReserve + amountIn,
313
+ quote: this.quoteReserve - amountOut,
314
+ };
315
+ }
316
+ else {
317
+ // Quote in: the fee stays in quote, the rest buys base at the fixed price.
318
+ amountOut = this.baseForQuote(net);
319
+ reservesAfter = {
320
+ base: this.baseReserve - amountOut,
321
+ quote: this.quoteReserve + amountIn,
322
+ };
323
+ }
324
+ let issue = null;
325
+ let reason = null;
326
+ if (amountOut <= 0n) {
327
+ issue = "amount-too-small";
328
+ reason = `amount is too small to receive one unit of ${this.outSymbol(side)} at this price`;
329
+ }
330
+ else if (reservesAfter.base < 0n) {
331
+ issue = "insufficient-liquidity";
332
+ reason = `the pool holds ${this.describe(this.baseReserve, "base")} and cannot sell ${this.describe(amountOut, "base")}`;
333
+ }
334
+ else if (reservesAfter.quote < 0n) {
335
+ issue = "insufficient-liquidity";
336
+ reason = `the pool holds ${this.describe(this.quoteReserve, "quote")} and cannot pay ${this.describe(amountOut, "quote")}`;
337
+ }
338
+ return {
339
+ side,
340
+ amountIn,
341
+ amountOut: issue === null ? amountOut : 0n,
342
+ fee,
343
+ notional,
344
+ reservesAfter: issue === null
345
+ ? reservesAfter
346
+ : { base: this.baseReserve, quote: this.quoteReserve },
347
+ priceImpactBps: 0,
348
+ executable: issue === null,
349
+ issue,
350
+ reason,
351
+ };
352
+ }
353
+ /** Execute a trade at the fixed price and return the fill. */
354
+ swap(side, amountIn) {
355
+ const filled = this.quote(side, amountIn);
356
+ if (!filled.executable) {
357
+ const code = filled.issue === "amount-too-small" ? "invalid-amount" : "insufficient-liquidity";
358
+ throw new MarketError(code, filled.reason ?? "swap cannot be settled");
359
+ }
360
+ this.baseReserve = filled.reservesAfter.base;
361
+ this.quoteReserve = filled.reservesAfter.quote;
362
+ this.volumeQuote += filled.notional;
363
+ this.swapCount += 1;
364
+ return filled;
365
+ }
366
+ /** Deposit one or both legs and mint shares at the pre-deposit value. */
367
+ addLiquidity(input) {
368
+ const base = input.base ?? 0n;
369
+ const quote = input.quote ?? 0n;
370
+ if (base < 0n || quote < 0n) {
371
+ throw new MarketError("invalid-amount", "liquidity amounts cannot be negative.");
372
+ }
373
+ if (base === 0n && quote === 0n) {
374
+ throw new MarketError("invalid-amount", "deposit at least one of base or quote.");
375
+ }
376
+ const value = quote + this.quoteForBase(base);
377
+ if (value <= 0n) {
378
+ throw new MarketError("invalid-amount", "deposit is too small to be worth one unit of the quote token at this price.");
379
+ }
380
+ const existingValue = this.liquidityValueQuote();
381
+ const shares = this.sharesOut === 0n ? value : (value * this.sharesOut) / (existingValue === 0n ? 1n : existingValue);
382
+ this.baseReserve += base;
383
+ this.quoteReserve += quote;
384
+ this.sharesOut += shares;
385
+ return {
386
+ kind: "add",
387
+ base,
388
+ quote,
389
+ shares,
390
+ sharesAfter: this.sharesOut,
391
+ liquidityValueQuote: this.liquidityValueQuote(),
392
+ };
393
+ }
394
+ /** Burn shares for their pro-rata slice of both reserves. */
395
+ removeLiquidity(shares) {
396
+ if (shares <= 0n) {
397
+ throw new MarketError("invalid-amount", "shares must be greater than zero.");
398
+ }
399
+ if (shares > this.sharesOut) {
400
+ throw new MarketError("insufficient-shares", `cannot burn ${shares} shares; the pool has ${this.sharesOut}.`);
401
+ }
402
+ const outBase = (shares * this.baseReserve) / this.sharesOut;
403
+ const outQuote = (shares * this.quoteReserve) / this.sharesOut;
404
+ this.baseReserve -= outBase;
405
+ this.quoteReserve -= outQuote;
406
+ this.sharesOut -= shares;
407
+ // Rounding dust is worthless to a zero-share pool, so it is cleared rather
408
+ // than left as inventory nobody can claim.
409
+ if (this.sharesOut === 0n) {
410
+ this.baseReserve = 0n;
411
+ this.quoteReserve = 0n;
412
+ }
413
+ return {
414
+ kind: "remove",
415
+ base: outBase,
416
+ quote: outQuote,
417
+ shares,
418
+ sharesAfter: this.sharesOut,
419
+ liquidityValueQuote: this.liquidityValueQuote(),
420
+ };
421
+ }
422
+ /** What the pool can settle right now, and how much volume its liquidity backs. */
423
+ capacity() {
424
+ const liquidity = this.liquidityValueQuote();
425
+ return {
426
+ maxBuyBase: this.baseReserve,
427
+ maxSellBase: this.baseForQuote(this.quoteReserve),
428
+ payQuote: this.quoteReserve,
429
+ liquidityValueQuote: liquidity,
430
+ };
431
+ }
432
+ /** What the pool could settle right after `quote`, without applying it. */
433
+ capacityAfter(quote) {
434
+ const base = quote.reservesAfter.base;
435
+ const reserve = quote.reservesAfter.quote;
436
+ return {
437
+ maxBuyBase: base,
438
+ maxSellBase: this.baseForQuote(reserve),
439
+ payQuote: reserve,
440
+ liquidityValueQuote: reserve + this.quoteForBase(base),
441
+ };
442
+ }
443
+ state() {
444
+ const liquidity = this.liquidityValueQuote();
445
+ return {
446
+ baseSymbol: this.config.baseSymbol,
447
+ quoteSymbol: this.config.quoteSymbol,
448
+ baseDecimals: this.config.baseDecimals,
449
+ quoteDecimals: this.config.quoteDecimals,
450
+ priceNum: this.config.priceNum,
451
+ priceDen: this.config.priceDen,
452
+ price: humanPrice(this.config.priceNum, this.config.priceDen, this.config.baseDecimals, this.config.quoteDecimals),
453
+ feeBps: this.config.feeBps,
454
+ baseReserve: this.baseReserve,
455
+ quoteReserve: this.quoteReserve,
456
+ liquidityValueQuote: liquidity,
457
+ shares: this.sharesOut,
458
+ volumeQuote: this.volumeQuote,
459
+ swaps: this.swapCount,
460
+ turnover: liquidity === 0n ? 0 : Number(this.volumeQuote) / Number(liquidity),
461
+ netBase: this.baseReserve - this.openedBase,
462
+ netQuote: this.quoteReserve - this.openedQuote,
463
+ };
464
+ }
465
+ toJson() {
466
+ return {
467
+ version: 1,
468
+ baseSymbol: this.config.baseSymbol,
469
+ quoteSymbol: this.config.quoteSymbol,
470
+ baseDecimals: this.config.baseDecimals,
471
+ quoteDecimals: this.config.quoteDecimals,
472
+ priceNum: this.config.priceNum.toString(),
473
+ priceDen: this.config.priceDen.toString(),
474
+ feeBps: this.config.feeBps,
475
+ baseReserve: this.baseReserve.toString(),
476
+ quoteReserve: this.quoteReserve.toString(),
477
+ shares: this.sharesOut.toString(),
478
+ volumeQuote: this.volumeQuote.toString(),
479
+ swaps: this.swapCount,
480
+ openedBase: this.openedBase.toString(),
481
+ openedQuote: this.openedQuote.toString(),
482
+ };
483
+ }
484
+ outSymbol(side) {
485
+ return side === "sell" ? this.config.quoteSymbol : this.config.baseSymbol;
486
+ }
487
+ describe(amount, leg) {
488
+ return leg === "base"
489
+ ? `${formatMarketAmount(amount, this.config.baseDecimals)} ${this.config.baseSymbol}`
490
+ : `${formatMarketAmount(amount, this.config.quoteDecimals)} ${this.config.quoteSymbol}`;
491
+ }
492
+ }
493
+ /** JSON form of a market state, with bigints as decimal strings. */
494
+ export function toMarketJson(state, path = null) {
495
+ return {
496
+ pool: path,
497
+ base: {
498
+ symbol: state.baseSymbol,
499
+ decimals: state.baseDecimals,
500
+ reserve: state.baseReserve.toString(),
501
+ },
502
+ quote: {
503
+ symbol: state.quoteSymbol,
504
+ decimals: state.quoteDecimals,
505
+ reserve: state.quoteReserve.toString(),
506
+ },
507
+ price: {
508
+ value: state.price,
509
+ num: state.priceNum.toString(),
510
+ den: state.priceDen.toString(),
511
+ fixed: true,
512
+ priceImpactBps: 0,
513
+ },
514
+ feeBps: state.feeBps,
515
+ liquidity: {
516
+ valueQuote: state.liquidityValueQuote.toString(),
517
+ shares: state.shares.toString(),
518
+ },
519
+ volume: {
520
+ quote: state.volumeQuote.toString(),
521
+ swaps: state.swaps,
522
+ turnover: state.turnover,
523
+ },
524
+ netFlow: {
525
+ base: state.netBase.toString(),
526
+ quote: state.netQuote.toString(),
527
+ },
528
+ };
529
+ }
530
+ /** JSON form of a swap quote, with bigints as decimal strings. */
531
+ export function toSwapQuoteJson(quote, state) {
532
+ return {
533
+ side: quote.side,
534
+ amountIn: quote.amountIn.toString(),
535
+ amountOut: quote.amountOut.toString(),
536
+ fee: quote.fee.toString(),
537
+ notional: quote.notional.toString(),
538
+ inSymbol: quote.side === "buy" ? state.quoteSymbol : state.baseSymbol,
539
+ outSymbol: quote.side === "buy" ? state.baseSymbol : state.quoteSymbol,
540
+ price: state.price,
541
+ priceImpactBps: quote.priceImpactBps,
542
+ executable: quote.executable,
543
+ issue: quote.issue,
544
+ reason: quote.reason,
545
+ reservesAfter: {
546
+ base: quote.reservesAfter.base.toString(),
547
+ quote: quote.reservesAfter.quote.toString(),
548
+ },
549
+ };
550
+ }
551
+ /** Human report for `lwc market`. */
552
+ export function formatMarketState(state, path) {
553
+ const base = (value) => formatMarketAmount(value, state.baseDecimals);
554
+ const quote = (value) => formatMarketAmount(value, state.quoteDecimals);
555
+ const pct = (value) => Number.isFinite(value) ? `${value.toLocaleString("en-US", { maximumFractionDigits: 2 })}×` : "—";
556
+ const lines = [
557
+ "lwc — market",
558
+ "",
559
+ ` pool ${path ?? "(in memory)"}`,
560
+ ` base ${state.baseSymbol} (${state.baseDecimals} decimals)`,
561
+ ` quote ${state.quoteSymbol} (${state.quoteDecimals} decimals)`,
562
+ ` price ${state.price} ${state.quoteSymbol} per ${state.baseSymbol} (fixed)`,
563
+ ` fee ${state.feeBps} bps`,
564
+ "",
565
+ ` reserve ${quote(state.quoteReserve)} ${state.quoteSymbol} + ${base(state.baseReserve)} ${state.baseSymbol}`,
566
+ ` liquidity ${quote(state.liquidityValueQuote)} ${state.quoteSymbol} in ${state.shares} shares`,
567
+ "",
568
+ ` volume ${quote(state.volumeQuote)} ${state.quoteSymbol} over ${state.swaps} swap${state.swaps === 1 ? "" : "s"}`,
569
+ ` turnover ${pct(state.turnover)} liquidity settled`,
570
+ ` net flow ${base(state.netBase)} ${state.baseSymbol}, ${quote(state.netQuote)} ${state.quoteSymbol}`,
571
+ ];
572
+ return lines.join("\n");
573
+ }
574
+ /** Human report for one fill or quote. */
575
+ export function formatSwapQuote(quote, state, options = {}) {
576
+ const inSymbol = quote.side === "buy" ? state.quoteSymbol : state.baseSymbol;
577
+ const outSymbol = quote.side === "buy" ? state.baseSymbol : state.quoteSymbol;
578
+ const inDecimals = quote.side === "buy" ? state.quoteDecimals : state.baseDecimals;
579
+ const outDecimals = quote.side === "buy" ? state.baseDecimals : state.quoteDecimals;
580
+ const inLabel = ` ${options.applied ? "swapped" : "quote "} ${formatMarketAmount(quote.amountIn, inDecimals)} ${inSymbol} → `;
581
+ if (!quote.executable) {
582
+ return `${inLabel}not executable — ${quote.reason ?? quote.issue ?? "unknown reason"}`;
583
+ }
584
+ return [
585
+ `${inLabel}${formatMarketAmount(quote.amountOut, outDecimals)} ${outSymbol}`,
586
+ ` fee ${formatMarketAmount(quote.fee, inDecimals)} ${inSymbol} (left in the pool)`,
587
+ ` price ${state.price} ${state.quoteSymbol} per ${state.baseSymbol} — no price impact`,
588
+ ` reserves ${formatMarketAmount(quote.reservesAfter.quote, state.quoteDecimals)} ${state.quoteSymbol} + ${formatMarketAmount(quote.reservesAfter.base, state.baseDecimals)} ${state.baseSymbol}`,
589
+ ].join("\n");
590
+ }
591
+ /** Human report for a liquidity deposit or withdrawal. */
592
+ export function formatLiquidity(receipt, state) {
593
+ const base = formatMarketAmount(receipt.base, state.baseDecimals);
594
+ const quote = formatMarketAmount(receipt.quote, state.quoteDecimals);
595
+ const verb = receipt.kind === "add" ? "deposited" : "withdrew";
596
+ return [
597
+ ` ${verb}${receipt.kind === "add" ? " " : " "} ${quote} ${state.quoteSymbol} + ${base} ${state.baseSymbol} → ${receipt.shares} shares`,
598
+ ` shares ${receipt.sharesAfter}`,
599
+ ` liquidity ${formatMarketAmount(receipt.liquidityValueQuote, state.quoteDecimals)} ${state.quoteSymbol}`,
600
+ ].join("\n");
601
+ }
602
+ /** Shortest access key `--api-key` accepts before the API is exposed. */
603
+ export const MIN_MARKET_KEY_LENGTH = 8;
604
+ function requiredValue(argv, index, flag) {
605
+ const value = argv[index + 1];
606
+ if (value === undefined || value.startsWith("-")) {
607
+ throw new MarketArgError(`${flag} requires a value.`);
608
+ }
609
+ return value;
610
+ }
611
+ function wholeNumber(raw, flag, max) {
612
+ if (!/^\d+$/.test(raw)) {
613
+ throw new MarketArgError(`${flag} must be a whole number, got "${raw}".`);
614
+ }
615
+ const value = Number(raw);
616
+ if (value > max)
617
+ throw new MarketArgError(`${flag} must be at most ${max}, got ${value}.`);
618
+ return value;
619
+ }
620
+ function parseCorsOrigin(raw) {
621
+ const value = raw.trim();
622
+ if (value === "*")
623
+ return "*";
624
+ if (!/^https?:\/\/[a-z0-9.-]+(?::\d+)?$/i.test(value)) {
625
+ throw new MarketArgError(`--cors must be "*" or an http(s) origin, got "${raw}".`);
626
+ }
627
+ return value;
628
+ }
629
+ export function parseMarketArgs(argv) {
630
+ let poolPath = null;
631
+ let force = false;
632
+ let dryRun = false;
633
+ let serve = false;
634
+ let port = null;
635
+ let host = null;
636
+ const corsOrigins = [];
637
+ let accessKey = null;
638
+ let rateLimit = null;
639
+ let limitsPath = null;
640
+ let price = null;
641
+ let baseSymbol = null;
642
+ let quoteSymbol = null;
643
+ let baseDecimals = null;
644
+ let quoteDecimals = null;
645
+ let feeBps = null;
646
+ let seed = null;
647
+ let init = false;
648
+ let buy = null;
649
+ let sell = null;
650
+ let addQuote = null;
651
+ let addBase = null;
652
+ let remove = null;
653
+ for (let index = 0; index < argv.length; index += 1) {
654
+ const arg = argv[index] ?? "";
655
+ const [flag, inline] = arg.includes("=")
656
+ ? [arg.slice(0, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1)]
657
+ : [arg, null];
658
+ const take = () => {
659
+ if (inline !== null)
660
+ return inline;
661
+ const value = requiredValue(argv, index, flag);
662
+ index += 1;
663
+ return value;
664
+ };
665
+ switch (flag) {
666
+ case "--json":
667
+ break;
668
+ case "--serve":
669
+ serve = true;
670
+ break;
671
+ case "--port":
672
+ port = wholeNumber(take(), flag, MAX_MARKET_PORT);
673
+ break;
674
+ case "--host":
675
+ host = take();
676
+ break;
677
+ case "--cors":
678
+ corsOrigins.push(parseCorsOrigin(take()));
679
+ break;
680
+ case "--api-key":
681
+ accessKey = take();
682
+ break;
683
+ case "--rate-limit":
684
+ rateLimit = wholeNumber(take(), flag, MAX_RATE_LIMIT);
685
+ break;
686
+ case "--limits-file":
687
+ limitsPath = take();
688
+ break;
689
+ case "--pool":
690
+ poolPath = take();
691
+ break;
692
+ case "--init":
693
+ init = true;
694
+ break;
695
+ case "--force":
696
+ force = true;
697
+ break;
698
+ case "--dry-run":
699
+ dryRun = true;
700
+ break;
701
+ case "--price":
702
+ price = take();
703
+ break;
704
+ case "--base":
705
+ baseSymbol = take();
706
+ break;
707
+ case "--quote":
708
+ quoteSymbol = take();
709
+ break;
710
+ case "--base-decimals":
711
+ baseDecimals = wholeNumber(take(), flag, MAX_DECIMALS);
712
+ break;
713
+ case "--quote-decimals":
714
+ quoteDecimals = wholeNumber(take(), flag, MAX_DECIMALS);
715
+ break;
716
+ case "--fee-bps":
717
+ feeBps = wholeNumber(take(), flag, MAX_FEE_BPS);
718
+ break;
719
+ case "--seed":
720
+ seed = take();
721
+ break;
722
+ case "--buy":
723
+ buy = take();
724
+ break;
725
+ case "--sell":
726
+ sell = take();
727
+ break;
728
+ case "--add":
729
+ addQuote = take();
730
+ break;
731
+ case "--add-base":
732
+ addBase = take();
733
+ break;
734
+ case "--remove":
735
+ remove = take();
736
+ break;
737
+ default:
738
+ if (arg.startsWith("-"))
739
+ throw new MarketArgError(`unknown flag ${arg}.`);
740
+ throw new MarketArgError(`unexpected argument "${arg}"; amounts are passed to a flag.`);
741
+ }
742
+ }
743
+ if (poolPath === null) {
744
+ throw new MarketArgError("--pool <path> is required (the pool file to read or create).");
745
+ }
746
+ let initArgs = null;
747
+ if (init) {
748
+ if (price === null)
749
+ throw new MarketArgError("--init requires --price <quote per base>.");
750
+ if (baseSymbol === null)
751
+ throw new MarketArgError("--init requires --base <symbol>.");
752
+ if (quoteSymbol === null)
753
+ throw new MarketArgError("--init requires --quote <symbol>.");
754
+ if (seed === null)
755
+ throw new MarketArgError("--init requires --seed <quote liquidity>.");
756
+ initArgs = {
757
+ price,
758
+ baseSymbol,
759
+ quoteSymbol,
760
+ baseDecimals: baseDecimals ?? 18,
761
+ quoteDecimals: quoteDecimals ?? 6,
762
+ feeBps: feeBps ?? 30,
763
+ seed,
764
+ };
765
+ }
766
+ else if (price !== null || seed !== null || baseSymbol !== null || quoteSymbol !== null) {
767
+ throw new MarketArgError("--price, --base, --quote and --seed only apply with --init.");
768
+ }
769
+ const operations = [
770
+ buy !== null ? "--buy" : null,
771
+ sell !== null ? "--sell" : null,
772
+ addQuote !== null || addBase !== null ? "--add" : null,
773
+ remove !== null ? "--remove" : null,
774
+ ].filter((entry) => entry !== null);
775
+ if (operations.length > 1) {
776
+ throw new MarketArgError(`run one operation at a time, got ${operations.join(" and ")}.`);
777
+ }
778
+ if (init && operations.length > 0) {
779
+ throw new MarketArgError("--init creates the pool; run it without a trade.");
780
+ }
781
+ if (serve && init) {
782
+ throw new MarketArgError("--serve runs an existing pool; it cannot be combined with --init.");
783
+ }
784
+ if (serve && operations.length > 0) {
785
+ throw new MarketArgError("--serve runs the API; it cannot be combined with a single operation.");
786
+ }
787
+ if (serve && dryRun) {
788
+ throw new MarketArgError("--dry-run only prices a swap; it cannot be combined with --serve.");
789
+ }
790
+ if (accessKey !== null) {
791
+ if (accessKey.length < MIN_MARKET_KEY_LENGTH) {
792
+ throw new MarketArgError(`--api-key must be at least ${MIN_MARKET_KEY_LENGTH} characters.`);
793
+ }
794
+ if (/\s/.test(accessKey)) {
795
+ throw new MarketArgError("--api-key cannot contain spaces.");
796
+ }
797
+ }
798
+ if (!serve &&
799
+ (port !== null ||
800
+ host !== null ||
801
+ corsOrigins.length > 0 ||
802
+ accessKey !== null ||
803
+ rateLimit !== null ||
804
+ limitsPath !== null)) {
805
+ throw new MarketArgError("--port, --host, --cors, --api-key, --rate-limit and --limits-file only apply with --serve.");
806
+ }
807
+ let operation = { kind: "state" };
808
+ if (buy !== null)
809
+ operation = { kind: "swap", side: "buy", amount: buy };
810
+ else if (sell !== null)
811
+ operation = { kind: "swap", side: "sell", amount: sell };
812
+ else if (addQuote !== null || addBase !== null) {
813
+ operation = { kind: "add", quote: addQuote, base: addBase, remove: "" };
814
+ }
815
+ else if (remove !== null) {
816
+ operation = { kind: "add", quote: null, base: null, remove };
817
+ }
818
+ return {
819
+ poolPath,
820
+ init: initArgs,
821
+ operation,
822
+ force,
823
+ dryRun,
824
+ serve: serve
825
+ ? {
826
+ host: host ?? DEFAULT_MARKET_HOST,
827
+ port: port ?? DEFAULT_MARKET_PORT,
828
+ corsOrigins,
829
+ accessKey,
830
+ rateLimitPerMinute: rateLimit ?? DEFAULT_MARKET_RATE_LIMIT,
831
+ limitsPath: limitsPath ?? (poolPath === null ? null : `${poolPath}.limits.json`),
832
+ }
833
+ : null,
834
+ };
835
+ }