@toon-protocol/hub 0.34.3

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 { createRequire } from 'module'; const require = createRequire(import.meta.url);
2
+ import {
3
+ formatRelativeTime,
4
+ formatUsdc,
5
+ formatUsdcMicro
6
+ } from "./chunk-JCOFMUPL.js";
7
+ import "./chunk-I2R4CRUX.js";
8
+
9
+ // src/tui/index.ts
10
+ import { createElement } from "react";
11
+ import { render } from "ink";
12
+
13
+ // src/tui/App.tsx
14
+ import { useState as useState4 } from "react";
15
+ import { Box as Box4, Text as Text10, useInput as useInput2 } from "ink";
16
+
17
+ // src/tui/use-earnings.ts
18
+ import { useEffect, useRef, useState } from "react";
19
+
20
+ // src/tui/constants.ts
21
+ var DEFAULT_REFRESH_INTERVAL_MS = 2e3;
22
+ var DEFAULT_API_URL = "http://127.0.0.1:28090";
23
+ var STARTING_UP_GRACE_FETCHES = 3;
24
+
25
+ // src/tui/use-earnings.ts
26
+ var EMPTY_EARNINGS = {
27
+ status: "connector_unavailable",
28
+ apex: { routingFees: {} },
29
+ peers: [],
30
+ recentClaims: [],
31
+ eventsRelayed: 0,
32
+ uptimeSeconds: 0
33
+ };
34
+ function useEarnings(opts = {}) {
35
+ const {
36
+ apiUrl = DEFAULT_API_URL,
37
+ refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS,
38
+ fetchImpl = globalThis.fetch
39
+ } = opts;
40
+ const [state, setState] = useState({
41
+ phase: "loading",
42
+ data: null,
43
+ bannerKey: null
44
+ });
45
+ const prevDataRef = useRef(null);
46
+ const warmupFailuresRef = useRef(0);
47
+ useEffect(() => {
48
+ let cancelled = false;
49
+ let abortController = null;
50
+ function failureBanner(specific) {
51
+ if (prevDataRef.current !== null) return specific;
52
+ warmupFailuresRef.current += 1;
53
+ return warmupFailuresRef.current <= STARTING_UP_GRACE_FETCHES ? "starting_up" : specific;
54
+ }
55
+ async function doFetch() {
56
+ if (cancelled) return;
57
+ const ac = new AbortController();
58
+ abortController = ac;
59
+ try {
60
+ const res = await fetchImpl(`${apiUrl}/api/earnings`, {
61
+ signal: ac.signal
62
+ });
63
+ if (cancelled) return;
64
+ if (!res.ok) {
65
+ const prev = prevDataRef.current;
66
+ setState({
67
+ phase: "stale",
68
+ data: prev ?? EMPTY_EARNINGS,
69
+ bannerKey: failureBanner("fetch_failed")
70
+ });
71
+ return;
72
+ }
73
+ const body = await res.json();
74
+ if (cancelled) return;
75
+ if (body.status === "connector_unavailable") {
76
+ const prev = prevDataRef.current;
77
+ setState({
78
+ phase: "stale",
79
+ data: prev ?? EMPTY_EARNINGS,
80
+ bannerKey: failureBanner("connector_unavailable")
81
+ });
82
+ return;
83
+ }
84
+ prevDataRef.current = body;
85
+ warmupFailuresRef.current = 0;
86
+ setState({ phase: "ok", data: body, bannerKey: null });
87
+ } catch (err) {
88
+ if (cancelled) return;
89
+ if (err instanceof Error && err.name === "AbortError") return;
90
+ setState({
91
+ phase: "stale",
92
+ data: prevDataRef.current ?? EMPTY_EARNINGS,
93
+ bannerKey: failureBanner("fetch_failed")
94
+ });
95
+ } finally {
96
+ abortController = null;
97
+ }
98
+ }
99
+ void doFetch();
100
+ const intervalId = setInterval(() => {
101
+ void doFetch();
102
+ }, refreshIntervalMs);
103
+ return () => {
104
+ cancelled = true;
105
+ clearInterval(intervalId);
106
+ if (abortController !== null) {
107
+ abortController.abort();
108
+ }
109
+ };
110
+ }, [apiUrl, refreshIntervalMs, fetchImpl]);
111
+ return state;
112
+ }
113
+
114
+ // src/tui/use-activity-buffer.ts
115
+ import { useState as useState2, useEffect as useEffect2 } from "react";
116
+ var MAX_BUFFER_SIZE = 200;
117
+ function claimKey(c) {
118
+ return `${c.peerId}|${c.at}|${c.amount}|${c.assetCode}|${c.direction}`;
119
+ }
120
+ function sortKey(c) {
121
+ const ms = Date.parse(c.at);
122
+ return Number.isFinite(ms) ? ms : -Infinity;
123
+ }
124
+ function useActivityBuffer(incoming) {
125
+ const [buffer, setBuffer] = useState2([]);
126
+ useEffect2(() => {
127
+ if (!Array.isArray(incoming)) return;
128
+ if (incoming.length === 0 && buffer.length === 0) return;
129
+ const seen = /* @__PURE__ */ new Map();
130
+ for (const c of buffer) seen.set(claimKey(c), c);
131
+ for (const c of incoming) seen.set(claimKey(c), c);
132
+ const merged = Array.from(seen.values());
133
+ merged.sort((a, b) => sortKey(b) - sortKey(a));
134
+ const trimmed = merged.slice(0, MAX_BUFFER_SIZE);
135
+ const same = trimmed.length === buffer.length && trimmed.every(
136
+ (c, i) => buffer[i] !== void 0 && claimKey(c) === claimKey(buffer[i])
137
+ );
138
+ if (!same) setBuffer(trimmed);
139
+ }, [incoming]);
140
+ return buffer;
141
+ }
142
+
143
+ // src/tui/components/HeroBand.tsx
144
+ import { Box, Text as Text3, useStdout } from "ink";
145
+
146
+ // src/tui/components/Sparkline.tsx
147
+ import { Text } from "ink";
148
+ import { jsxs } from "react/jsx-runtime";
149
+ var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
150
+ var PLACEHOLDER = "\xB7\xB7\xB7\xB7\xB7\xB7\xB7";
151
+ function Sparkline({ values, width }) {
152
+ if (width < 60) {
153
+ return null;
154
+ }
155
+ if (values.length === 0) {
156
+ return /* @__PURE__ */ jsxs(Text, { children: [
157
+ PLACEHOLDER,
158
+ " 7d"
159
+ ] });
160
+ }
161
+ const safe = values.filter((v) => Number.isFinite(v)).map((v) => v < 0 ? 0 : v);
162
+ if (safe.length === 0) {
163
+ return /* @__PURE__ */ jsxs(Text, { children: [
164
+ PLACEHOLDER,
165
+ " 7d"
166
+ ] });
167
+ }
168
+ const max = safe.reduce((m, v) => v > m ? v : m, 0);
169
+ const chars = safe.map((v) => {
170
+ if (max === 0) return BLOCKS[0] ?? "\u2581";
171
+ const idx = Math.floor(v / max * (BLOCKS.length - 1));
172
+ return BLOCKS[idx] ?? "\u2581";
173
+ }).join("");
174
+ return /* @__PURE__ */ jsxs(Text, { children: [
175
+ chars,
176
+ " 7d"
177
+ ] });
178
+ }
179
+
180
+ // src/tui/components/Qualifier.tsx
181
+ import { Text as Text2 } from "ink";
182
+
183
+ // src/tui/copy.ts
184
+ var COPY = {
185
+ heroEarly: `you're early`,
186
+ heroEarlyRotation: [
187
+ `you're early`,
188
+ `warming up`,
189
+ `first packet en route`
190
+ ],
191
+ loading: `Fetching earnings\u2026`,
192
+ qualifierPrefix: `MONTH $0.00`,
193
+ qualifierEventsWords: `events relayed`,
194
+ qualifierEvents: (n) => `${n} events relayed`,
195
+ banners: {
196
+ connectorUnavailable: `Connector not reachable \u2014 showing last known values. Retrying in 2s.`,
197
+ fetchFailed: `Last refresh failed \u2014 retrying.`,
198
+ // Shown only before the first successful fetch — a fresh node whose API is
199
+ // still warming up should read as "starting", not "failed".
200
+ startingUp: `Starting up \u2014 connecting to your node\u2026`
201
+ },
202
+ apex: {
203
+ routingPrefix: `\u21B3 apex routing: `,
204
+ routingEmpty: `(enable mill to route)`
205
+ },
206
+ peerTable: {
207
+ empty: `no peers yet \u2014 in a new terminal: townhouse node add town`
208
+ },
209
+ activityTicker: {
210
+ prefix: `recent: `,
211
+ empty: `no settlements yet \u2014 press [a] when activity arrives`,
212
+ keybind: ` [a] activity`
213
+ },
214
+ activityOverlay: {
215
+ titlePrefix: `Activity \u2014 last `,
216
+ emptyHint: `(no activity yet)`,
217
+ scrollHint: `j/k to scroll \xB7 q to close`,
218
+ scrollHintEmpty: `q to close`,
219
+ directionInbound: `in`,
220
+ directionOutbound: `out`,
221
+ directionUnknown: `?`
222
+ }
223
+ };
224
+
225
+ // src/tui/components/Qualifier.tsx
226
+ import { jsxs as jsxs2 } from "react/jsx-runtime";
227
+ function Qualifier({ eventsRelayed }) {
228
+ return /* @__PURE__ */ jsxs2(Text2, { color: "yellow", children: [
229
+ COPY.qualifierPrefix,
230
+ " \xB7 ",
231
+ COPY.qualifierEvents(eventsRelayed),
232
+ " \xB7 ",
233
+ COPY.heroEarly
234
+ ] });
235
+ }
236
+
237
+ // src/tui/components/HeroBand.tsx
238
+ import { jsx, jsxs as jsxs3 } from "react/jsx-runtime";
239
+ var USDC_SCALE = 6;
240
+ var ASSET = "USDC";
241
+ var DECIMAL_RE = /^-?\d+$/;
242
+ var MIN_COL_WIDTH = 8;
243
+ function addDecimalStrings(a, b) {
244
+ if (!DECIMAL_RE.test(b)) return a;
245
+ try {
246
+ return (BigInt(a) + BigInt(b)).toString();
247
+ } catch {
248
+ return a;
249
+ }
250
+ }
251
+ function computeScalars(apex, peers) {
252
+ let today = "0";
253
+ let month = "0";
254
+ let year = "0";
255
+ let lifetime = "0";
256
+ const apexUsdc = apex.routingFees[ASSET];
257
+ if (apexUsdc !== void 0) {
258
+ today = addDecimalStrings(today, apexUsdc.today);
259
+ month = addDecimalStrings(month, apexUsdc.month);
260
+ year = addDecimalStrings(year, apexUsdc.year);
261
+ lifetime = addDecimalStrings(lifetime, apexUsdc.lifetime);
262
+ }
263
+ for (const peer of peers) {
264
+ const peerUsdc = peer.byAsset[ASSET];
265
+ if (peerUsdc !== void 0) {
266
+ today = addDecimalStrings(today, peerUsdc.today);
267
+ month = addDecimalStrings(month, peerUsdc.month);
268
+ year = addDecimalStrings(year, peerUsdc.year);
269
+ lifetime = addDecimalStrings(lifetime, peerUsdc.lifetime);
270
+ }
271
+ }
272
+ return { today, month, year, lifetime };
273
+ }
274
+ function isEmptyState(apex, peers) {
275
+ const apexMonth = apex.routingFees[ASSET]?.month ?? "0";
276
+ if (apexMonth !== "0") return false;
277
+ for (const peer of peers) {
278
+ const peerMonth = peer.byAsset[ASSET]?.month ?? "0";
279
+ if (peerMonth !== "0") return false;
280
+ }
281
+ return true;
282
+ }
283
+ function HeroBand({ apex, peers, eventsRelayed }) {
284
+ const { stdout } = useStdout();
285
+ const columns = stdout?.columns ?? 80;
286
+ const scalars = computeScalars(apex, peers);
287
+ const showQualifier = isEmptyState(apex, peers);
288
+ const todayFmt = formatUsdc(scalars.today, USDC_SCALE);
289
+ const monthFmt = formatUsdc(scalars.month, USDC_SCALE);
290
+ const yearFmt = formatUsdc(scalars.year, USDC_SCALE);
291
+ const lifetimeFmt = formatUsdc(scalars.lifetime, USDC_SCALE);
292
+ const shortLabels = columns < 70;
293
+ const labelLifetime = shortLabels ? "LIFE" : "LIFETIME";
294
+ const colWidth = Math.max(Math.floor(columns / 4), MIN_COL_WIDTH);
295
+ return /* @__PURE__ */ jsxs3(Box, { flexDirection: "column", children: [
296
+ /* @__PURE__ */ jsxs3(Box, { children: [
297
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "TODAY" }) }),
298
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "MONTH" }) }),
299
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { dimColor: true, children: "YEAR" }) }),
300
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { dimColor: true, children: labelLifetime }) })
301
+ ] }),
302
+ /* @__PURE__ */ jsxs3(Box, { children: [
303
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { color: scalars.today !== "0" ? "green" : void 0, children: todayFmt }) }),
304
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { color: scalars.month !== "0" ? "green" : void 0, children: monthFmt }) }),
305
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { color: scalars.year !== "0" ? "green" : void 0, children: yearFmt }) }),
306
+ /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(Text3, { color: scalars.lifetime !== "0" ? "green" : void 0, children: lifetimeFmt }) })
307
+ ] }),
308
+ /* @__PURE__ */ jsx(Sparkline, { values: [], width: columns }),
309
+ showQualifier ? /* @__PURE__ */ jsx(Qualifier, { eventsRelayed }) : null
310
+ ] });
311
+ }
312
+
313
+ // src/tui/components/Banner.tsx
314
+ import { Text as Text4 } from "ink";
315
+ import { jsx as jsx2 } from "react/jsx-runtime";
316
+ function Banner({ bannerKey }) {
317
+ if (bannerKey === null) return null;
318
+ if (bannerKey === "starting_up") {
319
+ return /* @__PURE__ */ jsx2(Text4, { color: "cyan", children: COPY.banners.startingUp });
320
+ }
321
+ const isError = bannerKey === "fetch_failed";
322
+ const text = isError ? COPY.banners.fetchFailed : COPY.banners.connectorUnavailable;
323
+ return /* @__PURE__ */ jsx2(Text4, { color: isError ? "red" : "yellow", children: text });
324
+ }
325
+
326
+ // src/tui/components/ApexStrip.tsx
327
+ import { Text as Text5 } from "ink";
328
+ import { jsxs as jsxs4 } from "react/jsx-runtime";
329
+ var USDC_SCALE2 = 6;
330
+ var ASSET2 = "USDC";
331
+ var DECIMAL_RE2 = /^-?\d+$/;
332
+ function addDecimalStrings2(a, b) {
333
+ if (!DECIMAL_RE2.test(b)) return a;
334
+ try {
335
+ return (BigInt(a) + BigInt(b)).toString();
336
+ } catch {
337
+ return a;
338
+ }
339
+ }
340
+ function ApexStrip({ apex, peers }) {
341
+ const apexMonth = apex.routingFees[ASSET2]?.month ?? "0";
342
+ const apexValid = DECIMAL_RE2.test(apexMonth);
343
+ const apexMonthBig = apexValid ? BigInt(apexMonth) : 0n;
344
+ let totalMonth = apexMonthBig;
345
+ for (const peer of peers) {
346
+ const peerMonth = peer.byAsset[ASSET2]?.month ?? "0";
347
+ totalMonth = BigInt(addDecimalStrings2(totalMonth.toString(), peerMonth));
348
+ }
349
+ const apexFmt = formatUsdc(apexMonth, USDC_SCALE2);
350
+ const hasMillPeer = peers.some((p) => p.type === "mill");
351
+ if (!apexValid) {
352
+ return /* @__PURE__ */ jsxs4(Text5, { dimColor: true, italic: true, children: [
353
+ COPY.apex.routingPrefix,
354
+ apexFmt
355
+ ] });
356
+ }
357
+ if (apexMonthBig === 0n) {
358
+ const upsell = hasMillPeer ? "" : ` ${COPY.apex.routingEmpty}`;
359
+ return /* @__PURE__ */ jsxs4(Text5, { dimColor: true, italic: true, children: [
360
+ COPY.apex.routingPrefix,
361
+ apexFmt,
362
+ upsell
363
+ ] });
364
+ }
365
+ const pct = totalMonth === 0n ? null : Number(apexMonthBig * 100n / totalMonth);
366
+ return /* @__PURE__ */ jsxs4(Text5, { children: [
367
+ COPY.apex.routingPrefix,
368
+ apexFmt,
369
+ pct !== null ? ` (${pct}%)` : ""
370
+ ] });
371
+ }
372
+
373
+ // src/tui/components/PeerTable.tsx
374
+ import { Box as Box2, Text as Text6, useStdout as useStdout2 } from "ink";
375
+ import { jsx as jsx3, jsxs as jsxs5 } from "react/jsx-runtime";
376
+ var USDC_SCALE3 = 6;
377
+ var MAX_DATA_ROWS = 4;
378
+ var MIN_COL_WIDTH2 = 6;
379
+ function flattenPeers(peers) {
380
+ const out = [];
381
+ for (const peer of peers) {
382
+ const assetCodes = Object.keys(peer.byAsset).sort();
383
+ if (assetCodes.length === 0) continue;
384
+ let isFirst = true;
385
+ for (const assetCode of assetCodes) {
386
+ const perAsset = peer.byAsset[assetCode];
387
+ if (perAsset === void 0) continue;
388
+ out.push({
389
+ peerId: peer.id,
390
+ type: peer.type,
391
+ assetCode,
392
+ perAsset,
393
+ lastClaimAt: peer.lastClaimAt,
394
+ isFirstRowOfPeer: isFirst
395
+ });
396
+ isFirst = false;
397
+ }
398
+ }
399
+ return out;
400
+ }
401
+ function PeerTable({ peers, now = /* @__PURE__ */ new Date(), columns: columnsProp }) {
402
+ const { stdout } = useStdout2();
403
+ const columns = columnsProp ?? (stdout?.columns || 80);
404
+ const rows = flattenPeers(peers).slice(0, MAX_DATA_ROWS);
405
+ if (rows.length === 0) {
406
+ return /* @__PURE__ */ jsx3(Text6, { dimColor: true, children: COPY.peerTable.empty });
407
+ }
408
+ const showLastClaim = columns >= 60;
409
+ const shortType = columns < 70;
410
+ const dropAgoSuffix = columns < 70;
411
+ const totalCols = showLastClaim ? 5 : 4;
412
+ const colWidth = Math.max(Math.floor(columns / totalCols), MIN_COL_WIDTH2);
413
+ const header = /* @__PURE__ */ jsxs5(Box2, { children: [
414
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { dimColor: true, children: "PEER" }) }),
415
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { dimColor: true, children: "TYPE" }) }),
416
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { dimColor: true, children: "ASSET" }) }),
417
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { dimColor: true, children: "NET (MONTH)" }) }),
418
+ showLastClaim ? /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { dimColor: true, children: "LAST CLAIM" }) }) : null
419
+ ] });
420
+ return /* @__PURE__ */ jsxs5(Box2, { flexDirection: "column", children: [
421
+ header,
422
+ rows.map((row, i) => {
423
+ const peerCell = row.isFirstRowOfPeer ? row.peerId : "";
424
+ const typeRaw = row.isFirstRowOfPeer ? row.type : "";
425
+ const typeCell = shortType && typeRaw.length > 0 ? typeRaw.slice(0, 3) : typeRaw;
426
+ const netFmt = formatUsdc(row.perAsset.month, USDC_SCALE3);
427
+ let lastClaim = formatRelativeTime(row.lastClaimAt, now);
428
+ if (dropAgoSuffix && lastClaim.endsWith(" ago")) {
429
+ lastClaim = lastClaim.slice(0, -" ago".length);
430
+ }
431
+ return /* @__PURE__ */ jsxs5(Box2, { children: [
432
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { children: peerCell }) }),
433
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { children: typeCell }) }),
434
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { children: row.assetCode }) }),
435
+ /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { children: netFmt }) }),
436
+ showLastClaim ? /* @__PURE__ */ jsx3(Box2, { width: colWidth, children: /* @__PURE__ */ jsx3(Text6, { children: lastClaim }) }) : null
437
+ ] }, `${row.peerId}-${row.assetCode}-${i}`);
438
+ })
439
+ ] });
440
+ }
441
+
442
+ // src/tui/components/ActivityTicker.tsx
443
+ import { Text as Text7 } from "ink";
444
+ import { jsx as jsx4, jsxs as jsxs6 } from "react/jsx-runtime";
445
+ function sortKey2(c) {
446
+ const ms = Date.parse(c.at);
447
+ return Number.isFinite(ms) ? ms : -Infinity;
448
+ }
449
+ function arrowFor(direction) {
450
+ return direction === "inbound" ? "\u2190" : direction === "outbound" ? "\u2192" : COPY.activityOverlay.directionUnknown;
451
+ }
452
+ function ActivityTicker({ recentClaims, now = /* @__PURE__ */ new Date() }) {
453
+ if (recentClaims.length === 0) {
454
+ return /* @__PURE__ */ jsx4(Text7, { dimColor: true, children: COPY.activityTicker.empty });
455
+ }
456
+ const sorted = [...recentClaims].sort((a, b) => sortKey2(b) - sortKey2(a));
457
+ const claim = sorted[0];
458
+ if (!claim) {
459
+ return /* @__PURE__ */ jsx4(Text7, { dimColor: true, children: COPY.activityTicker.empty });
460
+ }
461
+ const arrow = arrowFor(claim.direction);
462
+ const amount = formatUsdcMicro(claim.amount, claim.assetScale);
463
+ const rel = formatRelativeTime(claim.at, now);
464
+ return /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
465
+ COPY.activityTicker.prefix,
466
+ claim.peerId,
467
+ " ",
468
+ arrow,
469
+ " ",
470
+ amount,
471
+ " ",
472
+ claim.assetCode,
473
+ " \xB7 ",
474
+ rel,
475
+ COPY.activityTicker.keybind
476
+ ] });
477
+ }
478
+
479
+ // src/tui/components/Badge.tsx
480
+ import { Text as Text8 } from "ink";
481
+ import { jsx as jsx5 } from "react/jsx-runtime";
482
+ var USDC_ASSET = "USDC";
483
+ var DECIMAL_RE3 = /^-?\d+$/;
484
+ var LIFETIME_USDC_THRESHOLD = 1000000n;
485
+ var UPTIME_SECONDS_THRESHOLD = 7 * 24 * 60 * 60;
486
+ var ROTATION_INTERVAL_MS = 3e4;
487
+ function parseDecimalOrZero(value) {
488
+ if (value === void 0 || !DECIMAL_RE3.test(value)) return 0n;
489
+ try {
490
+ return BigInt(value);
491
+ } catch {
492
+ return 0n;
493
+ }
494
+ }
495
+ function computeLifetimeUsdc(apex, peers) {
496
+ let total = parseDecimalOrZero(apex.routingFees[USDC_ASSET]?.lifetime);
497
+ for (const peer of peers) {
498
+ total += parseDecimalOrZero(peer.byAsset[USDC_ASSET]?.lifetime);
499
+ }
500
+ return total;
501
+ }
502
+ function Badge({
503
+ apex,
504
+ peers,
505
+ uptimeSeconds,
506
+ now = /* @__PURE__ */ new Date()
507
+ }) {
508
+ const lifetime = computeLifetimeUsdc(apex, peers);
509
+ const lifetimeTriggers = lifetime < LIFETIME_USDC_THRESHOLD;
510
+ const uptimeTriggers = uptimeSeconds < UPTIME_SECONDS_THRESHOLD;
511
+ if (!lifetimeTriggers && !uptimeTriggers) return null;
512
+ const index = Math.floor(now.getTime() / ROTATION_INTERVAL_MS) % COPY.heroEarlyRotation.length;
513
+ const text = COPY.heroEarlyRotation[index] ?? COPY.heroEarlyRotation[0];
514
+ return /* @__PURE__ */ jsx5(Text8, { color: "yellow", bold: true, children: text });
515
+ }
516
+
517
+ // src/tui/components/ActivityOverlay.tsx
518
+ import { Box as Box3, Text as Text9, useStdout as useStdout3, useInput } from "ink";
519
+ import { useEffect as useEffect3, useState as useState3 } from "react";
520
+ import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
521
+ var MIN_OVERLAY_WIDTH = 40;
522
+ var MAX_PEER_ID_WIDTH = 24;
523
+ var DEFAULT_MAX_BUFFER_SIZE = 200;
524
+ function formatTime(iso) {
525
+ const d = new Date(iso);
526
+ if (Number.isNaN(d.getTime())) return "--:--:--";
527
+ return d.toLocaleTimeString("en-GB", { hour12: false });
528
+ }
529
+ function truncatePeerId(id) {
530
+ if (id.length <= MAX_PEER_ID_WIDTH) return id;
531
+ return id.slice(0, MAX_PEER_ID_WIDTH - 1) + "\u2026";
532
+ }
533
+ function arrowFor2(direction) {
534
+ return direction === "inbound" ? "\u2190" : direction === "outbound" ? "\u2192" : COPY.activityOverlay.directionUnknown;
535
+ }
536
+ function directionLabel(direction) {
537
+ return direction === "inbound" ? COPY.activityOverlay.directionInbound : direction === "outbound" ? COPY.activityOverlay.directionOutbound : COPY.activityOverlay.directionUnknown;
538
+ }
539
+ function formatRow(claim) {
540
+ const time = formatTime(claim.at);
541
+ const peer = truncatePeerId(claim.peerId);
542
+ const arrow = arrowFor2(claim.direction);
543
+ const amount = formatUsdcMicro(claim.amount, claim.assetScale);
544
+ const dir = directionLabel(claim.direction);
545
+ return `${time} \xB7 ${peer} \xB7 ${arrow} ${amount} ${claim.assetCode} \xB7 ${dir}`;
546
+ }
547
+ function claimKeyForReact(c) {
548
+ return `${c.peerId}|${c.at}|${c.amount}|${c.assetCode}|${c.direction}`;
549
+ }
550
+ function ActivityOverlay({
551
+ claims,
552
+ onClose,
553
+ columns: columnsProp,
554
+ rows: rowsProp,
555
+ maxBufferSize = DEFAULT_MAX_BUFFER_SIZE
556
+ }) {
557
+ const { stdout } = useStdout3();
558
+ const columns = columnsProp ?? (stdout?.columns || 80);
559
+ const rows = rowsProp ?? (stdout?.rows || 24);
560
+ const modalWidth = Math.max(MIN_OVERLAY_WIDTH, Math.floor(columns * 0.7));
561
+ const visibleRows = Math.max(5, rows - 5);
562
+ const [scroll, setScroll] = useState3(0);
563
+ const maxScroll = Math.max(0, claims.length - visibleRows);
564
+ useEffect3(() => {
565
+ if (scroll > maxScroll) setScroll(maxScroll);
566
+ }, [maxScroll, scroll]);
567
+ useInput((input, key) => {
568
+ if (key.escape) {
569
+ onClose();
570
+ return;
571
+ }
572
+ if (key.ctrl || key.meta) return;
573
+ if (input === "q" || input === "Q") {
574
+ onClose();
575
+ return;
576
+ }
577
+ if (input === "j" || key.downArrow) {
578
+ setScroll((s) => Math.min(maxScroll, s + 1));
579
+ return;
580
+ }
581
+ if (input === "k" || key.upArrow) {
582
+ setScroll((s) => Math.max(0, s - 1));
583
+ }
584
+ });
585
+ const displayedCount = Math.min(claims.length, maxBufferSize);
586
+ const title = `${COPY.activityOverlay.titlePrefix}${displayedCount} of ${maxBufferSize}`;
587
+ const window = claims.slice(scroll, scroll + visibleRows);
588
+ const hint = claims.length === 0 ? COPY.activityOverlay.scrollHintEmpty : COPY.activityOverlay.scrollHint;
589
+ return /* @__PURE__ */ jsx6(Box3, { flexDirection: "column", alignItems: "center", width: columns, children: /* @__PURE__ */ jsxs7(Box3, { flexDirection: "column", borderStyle: "round", width: modalWidth, paddingX: 1, children: [
590
+ /* @__PURE__ */ jsx6(Text9, { bold: true, children: title }),
591
+ claims.length === 0 ? /* @__PURE__ */ jsx6(Text9, { dimColor: true, children: COPY.activityOverlay.emptyHint }) : window.map((c, i) => /* @__PURE__ */ jsx6(Text9, { children: formatRow(c) }, `${claimKeyForReact(c)}-${scroll + i}`)),
592
+ /* @__PURE__ */ jsx6(Text9, { dimColor: true, children: hint })
593
+ ] }) });
594
+ }
595
+
596
+ // src/tui/App.tsx
597
+ import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
598
+ function App(props) {
599
+ const state = useEarnings(props);
600
+ const recentClaims = state.phase !== "loading" ? state.data.recentClaims : void 0;
601
+ const buffer = useActivityBuffer(recentClaims);
602
+ const [overlayOpen, setOverlayOpen] = useState4(false);
603
+ useInput2(
604
+ (input, key) => {
605
+ if (key.ctrl || key.meta) return;
606
+ if (input === "a" || input === "A") setOverlayOpen(true);
607
+ },
608
+ { isActive: !overlayOpen && state.phase !== "loading" }
609
+ );
610
+ if (state.phase === "loading") {
611
+ return /* @__PURE__ */ jsx7(Text10, { children: COPY.loading });
612
+ }
613
+ if (overlayOpen) {
614
+ return /* @__PURE__ */ jsx7(ActivityOverlay, { claims: buffer, onClose: () => setOverlayOpen(false), maxBufferSize: MAX_BUFFER_SIZE });
615
+ }
616
+ const { data } = state;
617
+ const bannerKey = state.phase === "stale" ? state.bannerKey : null;
618
+ return /* @__PURE__ */ jsxs8(Box4, { flexDirection: "column", children: [
619
+ /* @__PURE__ */ jsx7(HeroBand, { apex: data.apex, peers: data.peers, eventsRelayed: data.eventsRelayed }),
620
+ /* @__PURE__ */ jsx7(Badge, { apex: data.apex, peers: data.peers, uptimeSeconds: data.uptimeSeconds }),
621
+ /* @__PURE__ */ jsx7(Banner, { bannerKey }),
622
+ /* @__PURE__ */ jsx7(ApexStrip, { apex: data.apex, peers: data.peers }),
623
+ /* @__PURE__ */ jsx7(PeerTable, { peers: data.peers }),
624
+ /* @__PURE__ */ jsx7(ActivityTicker, { recentClaims: data.recentClaims })
625
+ ] });
626
+ }
627
+
628
+ // src/tui/index.ts
629
+ function mountTui(opts = {}) {
630
+ return render(createElement(App, opts), {
631
+ exitOnCtrlC: true,
632
+ patchConsole: false
633
+ });
634
+ }
635
+ export {
636
+ mountTui
637
+ };
638
+ //# sourceMappingURL=tui-QE3ZRZO3.js.map