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