lovewebconnection 0.0.0-stage → 0.2.1

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 +1937 -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
@@ -0,0 +1,554 @@
1
+ /**
2
+ * The minimal swap page served at `GET /` by `lwc market --serve`.
3
+ *
4
+ * It is one self-contained document: inline CSS, inline JavaScript, no external
5
+ * assets, no build step. Every value it renders comes from the market API as
6
+ * text via `textContent`, never as HTML.
7
+ */
8
+ export const MARKET_PAGE_HTML = `<!doctype html>
9
+ <html lang="en">
10
+ <head>
11
+ <meta charset="utf-8" />
12
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
13
+ <title>Swap</title>
14
+ <style>
15
+ :root {
16
+ color-scheme: light dark;
17
+ --bg: #f6f7f9;
18
+ --panel: #ffffff;
19
+ --line: #dfe3e8;
20
+ --text: #10151c;
21
+ --muted: #5d6b7a;
22
+ --accent: #0a7a55;
23
+ --accent-text: #ffffff;
24
+ --good: #0a7a55;
25
+ --bad: #b3261e;
26
+ }
27
+ @media (prefers-color-scheme: dark) {
28
+ :root {
29
+ --bg: #0a0e13;
30
+ --panel: #121821;
31
+ --line: #222d3a;
32
+ --text: #e6edf3;
33
+ --muted: #8b9bab;
34
+ --accent: #2fbf8b;
35
+ --accent-text: #05100c;
36
+ --good: #37d399;
37
+ --bad: #ff8b82;
38
+ }
39
+ }
40
+ * { box-sizing: border-box; }
41
+ body {
42
+ margin: 0;
43
+ padding: 32px 20px 56px;
44
+ background: var(--bg);
45
+ color: var(--text);
46
+ font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
47
+ }
48
+ main { width: 100%; max-width: 480px; margin: 0 auto; }
49
+ h1 { margin: 0 0 4px; font-size: 20px; letter-spacing: -0.01em; }
50
+ h2 {
51
+ margin: 0 0 10px; font-size: 12px; font-weight: 600; color: var(--muted);
52
+ text-transform: uppercase; letter-spacing: 0.08em;
53
+ }
54
+ .sub { margin: 0 0 22px; color: var(--muted); font-size: 13px; min-height: 20px; }
55
+ .card { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 18px; }
56
+ .card + .card { margin-top: 16px; }
57
+ .tabs { display: flex; gap: 6px; margin-bottom: 16px; }
58
+ .tabs button {
59
+ flex: 1; padding: 9px 12px; border-radius: 9px; border: 1px solid var(--line);
60
+ background: transparent; color: var(--muted); font: inherit; font-weight: 600; cursor: pointer;
61
+ }
62
+ .tabs button[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
63
+ label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
64
+ input {
65
+ width: 100%; padding: 12px 14px; border-radius: 10px; border: 1px solid var(--line);
66
+ background: var(--bg); color: var(--text);
67
+ font: 600 18px/1.2 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
68
+ }
69
+ input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
70
+ input::placeholder { color: var(--muted); opacity: 1; }
71
+ .out {
72
+ margin: 12px 0 0; font: 600 16px/1.3 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
73
+ font-variant-numeric: tabular-nums;
74
+ }
75
+ .note { margin: 8px 0 0; min-height: 20px; font-size: 13px; color: var(--muted); }
76
+ .note[data-tone="good"] { color: var(--good); }
77
+ .note[data-tone="bad"] { color: var(--bad); }
78
+ button.primary {
79
+ width: 100%; margin-top: 14px; padding: 12px 16px; border: 0; border-radius: 10px;
80
+ background: var(--accent); color: var(--accent-text); font: inherit; font-weight: 700; cursor: pointer;
81
+ }
82
+ button.primary:disabled {
83
+ background: var(--line); color: var(--muted); cursor: not-allowed;
84
+ }
85
+ dl { margin: 0; font-size: 13px; }
86
+ .row {
87
+ display: flex; flex-wrap: wrap; align-items: baseline;
88
+ justify-content: space-between; gap: 0 12px;
89
+ }
90
+ .row + .row { margin-top: 6px; }
91
+ dt { color: var(--muted); }
92
+ dd {
93
+ margin: 0 0 0 auto; text-align: right;
94
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
95
+ font-variant-numeric: tabular-nums;
96
+ }
97
+ ol { margin: 0; padding: 0; list-style: none; font-size: 13px; }
98
+ li { padding: 7px 0; border-top: 1px solid var(--line); display: flex; justify-content: space-between; gap: 12px; }
99
+ li:first-child { border-top: 0; }
100
+ li time { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
101
+ li span { font-variant-numeric: tabular-nums; }
102
+ .empty { color: var(--muted); }
103
+ .amount-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
104
+ .amount-head label { margin-bottom: 6px; }
105
+ button.linkish {
106
+ padding: 0; border: 0; background: transparent; color: var(--accent);
107
+ font: inherit; font-weight: 600; cursor: pointer;
108
+ }
109
+ button.linkish:disabled { color: var(--muted); cursor: not-allowed; }
110
+ #liquidity-note { color: var(--bad); }
111
+ #liquidity-note[hidden] { display: none; }
112
+ .cover {
113
+ margin: 0 0 16px; padding: 10px 12px; border: 1px solid var(--bad);
114
+ border-radius: 10px; color: var(--bad); font-size: 13px;
115
+ }
116
+ .cover[hidden] { display: none; }
117
+ </style>
118
+ </head>
119
+ <body>
120
+ <main>
121
+ <h1>Swap</h1>
122
+ <p class="sub" id="sub">loading the pool…</p>
123
+ <p class="cover" id="cover-warning" role="status" aria-live="polite" hidden></p>
124
+
125
+ <section class="card" id="unlock" hidden>
126
+ <h2>Access key</h2>
127
+ <label for="key">This market asks for a key before it will quote or swap.</label>
128
+ <input id="key" type="password" autocomplete="off" spellcheck="false" placeholder="access key" />
129
+ <button class="primary" id="unlock-go" type="button">Unlock</button>
130
+ <p class="note" id="unlock-note" role="status" aria-live="polite"></p>
131
+ </section>
132
+
133
+ <form class="card" id="form">
134
+ <div class="tabs" role="group" aria-label="Side">
135
+ <button type="button" id="buy" aria-pressed="true">Buy <span id="buy-sym">base</span></button>
136
+ <button type="button" id="sell" aria-pressed="false">Sell <span id="sell-sym">base</span></button>
137
+ </div>
138
+ <div class="amount-head">
139
+ <label for="amount">You pay (<span id="pay-sym">quote</span>)</label>
140
+ <button type="button" class="linkish" id="max" disabled>Max</button>
141
+ </div>
142
+ <input id="amount" inputmode="decimal" autocomplete="off" spellcheck="false" placeholder="0.0" />
143
+ <p class="out" id="quote-out">You receive —</p>
144
+ <p class="note" id="quote-note" role="status" aria-live="polite"></p>
145
+ <p class="note" id="liquidity-note" role="status" aria-live="polite" hidden></p>
146
+ <button class="primary" id="swap" type="submit" disabled>Swap</button>
147
+ </form>
148
+
149
+ <section class="card" aria-labelledby="pool-h">
150
+ <h2 id="pool-h">Pool</h2>
151
+ <dl>
152
+ <div class="row"><dt>Price</dt><dd id="price">—</dd></div>
153
+ <div class="row"><dt>Net flow</dt><dd id="net-flow">—</dd></div>
154
+ <div class="row"><dt>Backing</dt><dd id="backing">—</dd></div>
155
+ <div class="row"><dt>Liquidity</dt><dd id="liquidity">—</dd></div>
156
+ <div class="row"><dt>Reserves</dt><dd id="reserves">—</dd></div>
157
+ <div class="row"><dt>Volume</dt><dd id="volume">—</dd></div>
158
+ <div class="row"><dt>Turnover</dt><dd id="turnover">—</dd></div>
159
+ <div class="row"><dt>One trade up to</dt><dd id="depth">—</dd></div>
160
+ <div class="row"><dt>After this trade</dt><dd id="depth-after">—</dd></div>
161
+ </dl>
162
+ </section>
163
+
164
+ <section class="card" aria-labelledby="fills-h">
165
+ <h2 id="fills-h">Fills</h2>
166
+ <ol id="fills"><li class="empty" id="no-fills">Nothing yet.</li></ol>
167
+ </section>
168
+ </main>
169
+ <script>
170
+ (function () {
171
+ var $ = function (id) { return document.getElementById(id); };
172
+ var base = (new URLSearchParams(location.search).get("api") || "").replace(/\\/+$/, "");
173
+ var state = null;
174
+ var side = "buy";
175
+ var quote = null;
176
+ var timer = null;
177
+ var seq = 0;
178
+ var KEY_STORE = "lwc-market-key";
179
+ var key = "";
180
+ try {
181
+ key = new URLSearchParams(location.search).get("key") || sessionStorage.getItem(KEY_STORE) || "";
182
+ } catch (error) {
183
+ key = "";
184
+ }
185
+
186
+ function authHeaders(extra) {
187
+ var headers = extra || {};
188
+ if (key) headers["x-market-key"] = key;
189
+ return headers;
190
+ }
191
+
192
+ /** Keep a working key for this tab only, and take it out of the address bar. */
193
+ function rememberKey(value) {
194
+ key = value;
195
+ try { sessionStorage.setItem(KEY_STORE, value); } catch (error) {}
196
+ var url;
197
+ try { url = new URL(location.href); } catch (error) { return; }
198
+ if (url.searchParams.has("key")) {
199
+ url.searchParams.delete("key");
200
+ history.replaceState(null, "", url.toString());
201
+ }
202
+ }
203
+
204
+ function askForKey(message) {
205
+ $("unlock").hidden = false;
206
+ $("unlock-note").textContent = message || "";
207
+ $("key").focus();
208
+ }
209
+
210
+ function unlock() {
211
+ var typed = $("key").value.trim();
212
+ if (!typed) {
213
+ $("unlock-note").textContent = "enter the key the api was started with";
214
+ return;
215
+ }
216
+ rememberKey(typed);
217
+ $("unlock").hidden = true;
218
+ $("sub").textContent = "checking the key…";
219
+ loadState().then(function (ok) { if (ok) setSide("buy"); });
220
+ }
221
+
222
+ function fmt(raw, decimals) {
223
+ var negative = raw.charAt(0) === "-";
224
+ var digits = negative ? raw.slice(1) : raw;
225
+ while (digits.length <= decimals) digits = "0" + digits;
226
+ var cut = digits.length - decimals;
227
+ var whole = digits.slice(0, cut);
228
+ var fraction = digits.slice(cut).replace(/0+$/, "");
229
+ return (negative ? "-" : "") + whole + (fraction ? "." + fraction : "");
230
+ }
231
+
232
+ /** A flow with its direction made explicit, and no sign noise on zero. */
233
+ function signed(raw, decimals) {
234
+ var text = fmt(raw, decimals);
235
+ if (Number(text) === 0) return text;
236
+ return text.charAt(0) === "-" ? text : "+" + text;
237
+ }
238
+
239
+ function setNote(text, tone) {
240
+ var note = $("quote-note");
241
+ note.textContent = text;
242
+ if (tone) note.setAttribute("data-tone", tone);
243
+ else note.removeAttribute("data-tone");
244
+ }
245
+
246
+ function setLiquidity(text) {
247
+ var note = $("liquidity-note");
248
+ note.textContent = text || "";
249
+ if (note.textContent) note.removeAttribute("hidden");
250
+ else note.setAttribute("hidden", "");
251
+ }
252
+
253
+ function setDepthAfter(text) {
254
+ $("depth-after").textContent = text || "—";
255
+ }
256
+
257
+ function setCover(text) {
258
+ var warning = $("cover-warning");
259
+ warning.textContent = text || "";
260
+ if (warning.textContent) warning.removeAttribute("hidden");
261
+ else warning.setAttribute("hidden", "");
262
+ }
263
+
264
+ /** A decimal string that stays one unit below the target, so it cannot be refused. */
265
+ function decimalFrom(value, decimals) {
266
+ if (!isFinite(value) || value <= 0) return "";
267
+ var units = Math.floor(value * Math.pow(10, decimals));
268
+ if (units > 1) units -= 1;
269
+ if (units <= 0) return "";
270
+ return fmt(String(units), decimals);
271
+ }
272
+
273
+ /**
274
+ * The most this side can pay right now. A buy is limited by the base reserve,
275
+ * so its quote cost is the reserve at the fixed price plus the fee; a sell is
276
+ * limited by the quote reserve, which the API already reports as maxSellBase.
277
+ */
278
+ function maxInput(capacity) {
279
+ capacity = capacity || (state && state.capacity);
280
+ if (!capacity) return "";
281
+ if (side === "sell") {
282
+ var sell = fmt(capacity.maxSellBase, state.base.decimals);
283
+ return Number(sell) > 0 ? sell : "";
284
+ }
285
+
286
+ var base = Number(fmt(capacity.maxBuyBase, state.base.decimals));
287
+ var price = Number(state.price && state.price.value);
288
+ var fee = Number(state.feeBps || 0) / 10000;
289
+ if (base > 0 && isFinite(price) && price > 0 && fee < 1) {
290
+ return decimalFrom((base * price) / (1 - fee), state.quote.decimals);
291
+ }
292
+ var fallback = fmt(capacity.payQuote, state.quote.decimals);
293
+ return Number(fallback) > 0 ? fallback : "";
294
+ }
295
+
296
+ function updateMax() {
297
+ var button = $("max");
298
+ var value = maxInput();
299
+ button.disabled = value === "";
300
+ button.title = value
301
+ ? "fill " + value + " " + (side === "buy" ? state.quote.symbol : state.base.symbol)
302
+ : "the pool has no depth on this side";
303
+ }
304
+
305
+ function renderState() {
306
+ if (!state) return;
307
+ var q = state.quote;
308
+ var b = state.base;
309
+ $("sub").textContent = "Fixed price, no price impact. Every quote is answered.";
310
+ $("price").textContent = state.price.value + " " + q.symbol + " per " + b.symbol;
311
+ $("liquidity").textContent = fmt(state.liquidity.valueQuote, q.decimals) + " " + q.symbol;
312
+ $("reserves").textContent =
313
+ fmt(q.reserve, q.decimals) + " " + q.symbol + " + " + fmt(b.reserve, b.decimals) + " " + b.symbol;
314
+ $("volume").textContent =
315
+ fmt(state.volume.quote, q.decimals) + " " + q.symbol + " in " + state.volume.swaps + " swaps";
316
+ $("turnover").textContent =
317
+ Number(state.volume.turnover).toLocaleString(undefined, { maximumFractionDigits: 2 }) + "× liquidity";
318
+ var net = state.netFlow;
319
+ $("net-flow").textContent = net
320
+ ? signed(net.base, b.decimals) + " " + b.symbol + " · " +
321
+ signed(net.quote, q.decimals) + " " + q.symbol
322
+ : "—";
323
+
324
+ var capacity = state.capacity;
325
+ $("depth").textContent = capacity
326
+ ? fmt(capacity.maxBuyBase, b.decimals) + " " + b.symbol + " / " +
327
+ fmt(capacity.payQuote, q.decimals) + " " + q.symbol
328
+ : "—";
329
+ $("backing").textContent = capacity
330
+ ? fmt(q.reserve, q.decimals) + " " + q.symbol + " · covers " +
331
+ fmt(capacity.maxSellBase, b.decimals) + " " + b.symbol + " of sells"
332
+ : "—";
333
+
334
+ // Base the pool has sold into circulation has to be bought back at this
335
+ // price, and only the quote reserve can pay for it.
336
+ var issued = net && net.base.charAt(0) === "-" ? -Number(fmt(net.base, b.decimals)) : 0;
337
+ var cover = capacity ? Number(fmt(capacity.maxSellBase, b.decimals)) : 0;
338
+ if (capacity && issued > cover) {
339
+ setCover(
340
+ "net flow: the pool sold " + fmt(net.base, b.decimals).slice(1) + " " + b.symbol +
341
+ " but can buy back only " + fmt(capacity.maxSellBase, b.decimals) + " " + b.symbol +
342
+ " at " + state.price.value + " " + q.symbol + " — larger sells are refused"
343
+ );
344
+ } else {
345
+ setCover("");
346
+ }
347
+ $("buy-sym").textContent = b.symbol;
348
+ $("sell-sym").textContent = b.symbol;
349
+ $("pay-sym").textContent = side === "buy" ? q.symbol : b.symbol;
350
+ updateMax();
351
+ }
352
+
353
+ function refreshQuote() {
354
+ var amount = $("amount").value.trim();
355
+ var mine = ++seq;
356
+ quote = null;
357
+ $("swap").disabled = true;
358
+
359
+ if (!state || !/^\\d+(\\.\\d+)?$/.test(amount) || Number(amount) <= 0) {
360
+ $("quote-out").textContent = "You receive —";
361
+ setNote("", null);
362
+ setLiquidity("");
363
+ setDepthAfter("");
364
+ return;
365
+ }
366
+
367
+ setNote("quoting…", null);
368
+ fetch(base + "/quote?side=" + side + "&amount=" + encodeURIComponent(amount), {
369
+ cache: "no-store",
370
+ headers: authHeaders()
371
+ })
372
+ .then(function (response) {
373
+ return response.json().then(function (body) {
374
+ return { ok: response.ok, status: response.status, body: body };
375
+ });
376
+ })
377
+ .then(function (result) {
378
+ if (mine !== seq) return;
379
+ if (result.status === 401) {
380
+ askForKey("that key was not accepted");
381
+ setNote("access key required", "bad");
382
+ setLiquidity("");
383
+ setDepthAfter("");
384
+ return;
385
+ }
386
+ if (!result.ok) throw new Error(result.body.error || "quote failed");
387
+ var filled = result.body;
388
+ quote = filled;
389
+ if (!filled.executable) {
390
+ $("quote-out").textContent = "You receive —";
391
+ setNote(filled.reason || "this trade cannot be settled right now", "bad");
392
+ setLiquidity("");
393
+ setDepthAfter("");
394
+ return;
395
+ }
396
+ $("quote-out").textContent = "You receive " + filled.amountOutFormatted + " " + filled.outSymbol;
397
+ setNote(
398
+ "fee " + filled.feeFormatted + " " + filled.inSymbol + " · " + filled.price + " " +
399
+ state.quote.symbol + " per " + state.base.symbol + " · no price impact",
400
+ null
401
+ );
402
+ var depth = Number(maxInput());
403
+ var used = depth > 0 ? Number(filled.amountInFormatted) / depth : 0;
404
+ var unit = side === "buy" ? state.quote.symbol : state.base.symbol;
405
+ var left = filled.capacityAfter ? maxInput(filled.capacityAfter) : "";
406
+ setDepthAfter(left ? left + " " + unit : "nothing on this side");
407
+ var tail = left ? " — " + left + " " + unit + " left after it" : " — nothing left after it";
408
+ if (used >= 0.995) {
409
+ setLiquidity("low liquidity: this fills the pool's whole depth on this side" + tail);
410
+ } else if (used >= 0.5) {
411
+ setLiquidity(
412
+ "low liquidity: this trade uses " + Math.round(used * 100) + "% of the pool's depth" + tail
413
+ );
414
+ } else {
415
+ setLiquidity("");
416
+ }
417
+ $("swap").disabled = false;
418
+ })
419
+ .catch(function (error) {
420
+ if (mine !== seq) return;
421
+ setNote(String(error.message || error), "bad");
422
+ });
423
+ }
424
+
425
+ function addFill(fill) {
426
+ var empty = $("no-fills");
427
+ if (empty) empty.remove();
428
+
429
+ var item = document.createElement("li");
430
+ var text = document.createElement("span");
431
+ text.textContent =
432
+ fill.side + " " + fill.amountInFormatted + " " + fill.inSymbol +
433
+ " → " + fill.amountOutFormatted + " " + fill.outSymbol;
434
+ var when = document.createElement("time");
435
+ when.textContent = new Date().toLocaleTimeString();
436
+ item.appendChild(text);
437
+ item.appendChild(when);
438
+
439
+ var list = $("fills");
440
+ list.insertBefore(item, list.firstChild);
441
+ while (list.children.length > 20) list.removeChild(list.lastChild);
442
+ }
443
+
444
+ function loadState() {
445
+ return fetch(base + "/market", { cache: "no-store", headers: authHeaders() })
446
+ .then(function (response) {
447
+ if (response.status === 401) {
448
+ $("sub").textContent = "This market asks for an access key.";
449
+ askForKey("");
450
+ return null;
451
+ }
452
+ if (!response.ok) throw new Error("market answered " + response.status);
453
+ return response.json();
454
+ })
455
+ .then(function (body) {
456
+ if (body === null) return false;
457
+ state = body;
458
+ renderState();
459
+ return true;
460
+ })
461
+ .catch(function (error) {
462
+ $("sub").textContent =
463
+ "Cannot reach the market API" + (base ? " at " + base : "") + ": " + (error.message || error);
464
+ return false;
465
+ });
466
+ }
467
+
468
+ function setSide(next) {
469
+ side = next;
470
+ $("buy").setAttribute("aria-pressed", String(next === "buy"));
471
+ $("sell").setAttribute("aria-pressed", String(next === "sell"));
472
+ $("pay-sym").textContent = state ? (next === "buy" ? state.quote.symbol : state.base.symbol) : "—";
473
+ $("amount").value = "";
474
+ refreshQuote();
475
+ updateMax();
476
+ }
477
+
478
+ $("max").addEventListener("click", function () {
479
+ var value = maxInput();
480
+ if (!value) return;
481
+ $("amount").value = value;
482
+ refreshQuote();
483
+ });
484
+
485
+ $("buy").addEventListener("click", function () { setSide("buy"); });
486
+ $("sell").addEventListener("click", function () { setSide("sell"); });
487
+
488
+ $("amount").addEventListener("input", function () {
489
+ clearTimeout(timer);
490
+ timer = setTimeout(refreshQuote, 200);
491
+ });
492
+
493
+ $("form").addEventListener("submit", function (event) {
494
+ event.preventDefault();
495
+ if (!quote) return;
496
+
497
+ var sending = { side: side, amount: $("amount").value.trim() };
498
+ $("swap").disabled = true;
499
+ setNote("submitting…", null);
500
+
501
+ fetch(base + "/swap", {
502
+ method: "POST",
503
+ headers: authHeaders({ "content-type": "application/json" }),
504
+ body: JSON.stringify(sending)
505
+ })
506
+ .then(function (response) {
507
+ return response.json().then(function (body) {
508
+ return { ok: response.ok, status: response.status, body: body };
509
+ });
510
+ })
511
+ .then(function (result) {
512
+ if (result.status === 401) {
513
+ askForKey("that key was not accepted");
514
+ setNote("access key required", "bad");
515
+ return;
516
+ }
517
+ if (!result.ok) throw new Error(result.body.error || "swap failed");
518
+ var filled = result.body;
519
+ addFill(filled);
520
+ if (filled.state) state = filled.state;
521
+ setLiquidity("");
522
+ renderState();
523
+ $("amount").value = "";
524
+ quote = null;
525
+ setDepthAfter("");
526
+ $("quote-out").textContent = "You receive —";
527
+ setNote(
528
+ "filled " + filled.amountInFormatted + " " + filled.inSymbol +
529
+ " → " + filled.amountOutFormatted + " " + filled.outSymbol,
530
+ "good"
531
+ );
532
+ })
533
+ .catch(function (error) {
534
+ setNote(String(error.message || error), "bad");
535
+ $("swap").disabled = !quote;
536
+ });
537
+ });
538
+
539
+ $("unlock-go").addEventListener("click", unlock);
540
+ $("key").addEventListener("keydown", function (event) {
541
+ if (event.key === "Enter") { event.preventDefault(); unlock(); }
542
+ });
543
+
544
+ if (key) rememberKey(key);
545
+ loadState().then(function (ok) { if (ok) setSide("buy"); });
546
+
547
+ setInterval(function () {
548
+ if (document.visibilityState === "visible") loadState();
549
+ }, 5000);
550
+ })();
551
+ </script>
552
+ </body>
553
+ </html>
554
+ `;
@@ -0,0 +1,46 @@
1
+ import { MarketPool, type MarketServeOptions } from "./market.ts";
2
+ /** An error that maps straight to an HTTP status. */
3
+ export declare class MarketApiError extends Error {
4
+ readonly status: number;
5
+ readonly code: string | null;
6
+ constructor(status: number, message: string, code?: string | null);
7
+ }
8
+ export interface MarketServerInfo {
9
+ /** URL to call, with the port actually bound (useful when `port: 0`). */
10
+ url: string;
11
+ port: number;
12
+ }
13
+ export declare class MarketServer {
14
+ private readonly pool;
15
+ private readonly poolPath;
16
+ private readonly options;
17
+ /** Where counters are kept between runs; null keeps them in memory only. */
18
+ private readonly limitsPath;
19
+ private limitsWriteFailed;
20
+ private server;
21
+ constructor(pool: MarketPool, poolPath: string, options: MarketServeOptions);
22
+ listen(): Promise<MarketServerInfo>;
23
+ close(): Promise<void>;
24
+ /** True while the listener is accepting requests. */
25
+ get listening(): boolean;
26
+ /** Requests and failed key attempts per client. */
27
+ private readonly clients;
28
+ /**
29
+ * Read the counters a previous run left behind. A missing, corrupt or expired
30
+ * file is not an error: the API simply starts with a clean slate.
31
+ */
32
+ private loadLimits;
33
+ /** Write the counters back, pruning anything that can no longer matter. */
34
+ private saveLimits;
35
+ private corsHeaders;
36
+ /** True when no key is configured, or the request carries the right one. */
37
+ private authorized;
38
+ private clientRecord;
39
+ /** Seconds this client must wait for guessing the key, or 0 when it may try. */
40
+ private blockedSeconds;
41
+ private noteFailure;
42
+ /** Seconds until this client may make another request, or 0 for this one. */
43
+ private rateLimitWait;
44
+ private amountFor;
45
+ private handle;
46
+ }