@umbra-privacy/ceremony 0.2.6 → 0.2.9
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/dist/index.js +1477 -322
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,23 +4,38 @@
|
|
|
4
4
|
import { render } from "ink";
|
|
5
5
|
|
|
6
6
|
// src/components/App.tsx
|
|
7
|
-
import { useEffect as
|
|
8
|
-
import { Box as
|
|
7
|
+
import { useEffect as useEffect6, useState as useState6 } from "react";
|
|
8
|
+
import { Box as Box8, Text as Text8, useApp, useInput as useInput3 } from "ink";
|
|
9
9
|
|
|
10
10
|
// src/cleanup.ts
|
|
11
11
|
var pendingLeaveQueue = null;
|
|
12
|
+
var pendingClearSession = null;
|
|
12
13
|
function setQueueCleanup(fn) {
|
|
13
14
|
pendingLeaveQueue = fn;
|
|
14
15
|
}
|
|
15
16
|
function clearQueueCleanup() {
|
|
16
17
|
pendingLeaveQueue = null;
|
|
17
18
|
}
|
|
19
|
+
function setSessionCleanup(fn) {
|
|
20
|
+
pendingClearSession = fn;
|
|
21
|
+
}
|
|
22
|
+
function clearSessionCleanup() {
|
|
23
|
+
pendingClearSession = null;
|
|
24
|
+
}
|
|
18
25
|
async function runQueueCleanup() {
|
|
19
26
|
if (!pendingLeaveQueue) return;
|
|
20
27
|
pendingLeaveQueue();
|
|
21
28
|
pendingLeaveQueue = null;
|
|
22
29
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
23
30
|
}
|
|
31
|
+
async function runSessionCleanup() {
|
|
32
|
+
if (!pendingClearSession) return;
|
|
33
|
+
try {
|
|
34
|
+
await pendingClearSession();
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
pendingClearSession = null;
|
|
38
|
+
}
|
|
24
39
|
|
|
25
40
|
// src/session.ts
|
|
26
41
|
import { readFile as readFile2, unlink as unlink2, writeFile } from "fs/promises";
|
|
@@ -227,7 +242,16 @@ var STORE_FILE = process.env["CEREMONY_CONTRIBUTIONS_FILE"] ?? join2(homedir2(),
|
|
|
227
242
|
async function load() {
|
|
228
243
|
try {
|
|
229
244
|
const raw = await readFile3(STORE_FILE, "utf8");
|
|
230
|
-
|
|
245
|
+
const parsed = JSON.parse(raw);
|
|
246
|
+
const out = {};
|
|
247
|
+
for (const [trackId, value] of Object.entries(parsed)) {
|
|
248
|
+
if (Array.isArray(value)) {
|
|
249
|
+
out[trackId] = value;
|
|
250
|
+
} else if (value && typeof value === "object") {
|
|
251
|
+
out[trackId] = [value];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
231
255
|
} catch {
|
|
232
256
|
return {};
|
|
233
257
|
}
|
|
@@ -240,9 +264,21 @@ async function getContributions() {
|
|
|
240
264
|
}
|
|
241
265
|
async function recordContribution(trackId, contribution) {
|
|
242
266
|
const store = await load();
|
|
243
|
-
store[trackId]
|
|
267
|
+
const existing = store[trackId] ?? [];
|
|
268
|
+
if (existing.some((c) => c.contributionId === contribution.contributionId)) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
store[trackId] = [...existing, contribution];
|
|
244
272
|
await save(store);
|
|
245
273
|
}
|
|
274
|
+
function latestContribution(store, trackId) {
|
|
275
|
+
const list = store[trackId];
|
|
276
|
+
if (!list || list.length === 0) return null;
|
|
277
|
+
return list.reduce((a, b) => b.sequenceNumber > a.sequenceNumber ? b : a);
|
|
278
|
+
}
|
|
279
|
+
function roundCount(store, trackId) {
|
|
280
|
+
return store[trackId]?.length ?? 0;
|
|
281
|
+
}
|
|
246
282
|
|
|
247
283
|
// src/components/Header.tsx
|
|
248
284
|
import { Box, Text } from "ink";
|
|
@@ -337,7 +373,32 @@ var COMPUTING_MESSAGES = [
|
|
|
337
373
|
"applying tau locally",
|
|
338
374
|
"folding contribution proof",
|
|
339
375
|
"blinding the trapdoor",
|
|
340
|
-
"sealing the bellman response"
|
|
376
|
+
"sealing the bellman response",
|
|
377
|
+
"asking the curve where it lives",
|
|
378
|
+
"weaving G1 points into the new delta",
|
|
379
|
+
"negotiating with the pairing engine",
|
|
380
|
+
"rotating the keypair commitment",
|
|
381
|
+
"checking that tau is non-zero (just in case)",
|
|
382
|
+
"evaluating polynomials over your secret",
|
|
383
|
+
"scrubbing the entropy buffer mid-flight",
|
|
384
|
+
"ratifying the contribution sequence",
|
|
385
|
+
"computing s \xB7 \u03C4 in G1",
|
|
386
|
+
"computing t \xB7 \u03C4 in G2",
|
|
387
|
+
"running the signature-of-knowledge",
|
|
388
|
+
"binding the transcript to your contribution",
|
|
389
|
+
"double-checking nothing is at infinity",
|
|
390
|
+
"compressing the response footprint",
|
|
391
|
+
"verifying your math one more time",
|
|
392
|
+
"stamping the public key bundle",
|
|
393
|
+
"writing the response file to disk",
|
|
394
|
+
"preparing to hand the response back to the worker",
|
|
395
|
+
"almost done \u2014 finalising your tau",
|
|
396
|
+
"this is the cryptography working hard for you",
|
|
397
|
+
"your secret never leaves this machine",
|
|
398
|
+
"if your power-21 circuit feels slow, blame BN254",
|
|
399
|
+
"snarkjs is single-threaded, give it a moment",
|
|
400
|
+
"trusted setup ceremonies are a team sport",
|
|
401
|
+
"the worker will verify everything you did"
|
|
341
402
|
];
|
|
342
403
|
var VERIFYING_MESSAGES = [
|
|
343
404
|
"worker pulling your response from S3",
|
|
@@ -346,20 +407,75 @@ var VERIFYING_MESSAGES = [
|
|
|
346
407
|
"writing the next zkey",
|
|
347
408
|
"uploading the new zkey",
|
|
348
409
|
"anchoring contribution hash in the audit chain",
|
|
349
|
-
"almost there \u2014 finalising your receipt"
|
|
410
|
+
"almost there \u2014 finalising your receipt",
|
|
411
|
+
"parsing your response's wire format",
|
|
412
|
+
"validating every G1 point is on the curve",
|
|
413
|
+
"validating every G2 point is in the right subgroup",
|
|
414
|
+
"rebuilding the transcript hash chain",
|
|
415
|
+
"running the knowledge proof pairing",
|
|
416
|
+
"verifying the new delta_g1 matches its evidence",
|
|
417
|
+
"verifying the new delta_g2 matches its evidence",
|
|
418
|
+
"checking the L array against your contribution",
|
|
419
|
+
"checking the H array against your contribution",
|
|
420
|
+
"deriving Fiat-Shamir scalars for the batched check",
|
|
421
|
+
"running the random-linear-combination same-ratio",
|
|
422
|
+
"confirming the vKey fields were not altered",
|
|
423
|
+
"sequencing the new zkey for the next contributor",
|
|
424
|
+
"this is the part where every byte gets re-checked",
|
|
425
|
+
"soundness of the whole ceremony rides on this step",
|
|
426
|
+
"exporting the new bellman params for the next round",
|
|
427
|
+
"writing your contribution into the ceremony transcript",
|
|
428
|
+
"publishing the new audit chain entry",
|
|
429
|
+
"any tampering would have failed by now",
|
|
430
|
+
"if you see this, your contribution is provably honest",
|
|
431
|
+
"computing your contribution receipt",
|
|
432
|
+
"the math agrees \u2014 wrapping things up",
|
|
433
|
+
"finalising the verified state in Postgres",
|
|
434
|
+
"you did the cryptographic work \u2014 we just had to check"
|
|
350
435
|
];
|
|
351
436
|
var EXPORTING_MESSAGES = [
|
|
352
437
|
"preparing your challenge file",
|
|
353
438
|
"extracting bellman params from the current zkey",
|
|
354
439
|
"wrapping the zkey for handoff",
|
|
355
440
|
"stamping a sha256 over the challenge bundle",
|
|
356
|
-
"presigning your download URL"
|
|
441
|
+
"presigning your download URL",
|
|
442
|
+
"fetching the latest verified state for this circuit",
|
|
443
|
+
"downloading the previous contribution from S3",
|
|
444
|
+
"checking the previous contribution's hash",
|
|
445
|
+
"carving out a personal challenge for your tau",
|
|
446
|
+
"the worker is queuing your slot",
|
|
447
|
+
"uploading the challenge so you can download it",
|
|
448
|
+
"rotating the worker's S3 connection",
|
|
449
|
+
"binding your challenge to the audit chain",
|
|
450
|
+
"stamping the challenge metadata in Postgres",
|
|
451
|
+
"AWS SDK is recycling connection pools",
|
|
452
|
+
"your slot timer starts the moment this finishes",
|
|
453
|
+
"writing the challenge to a fresh S3 key",
|
|
454
|
+
"double-checking the integrity hash",
|
|
455
|
+
"if this takes >1 min, S3 is having a bad time",
|
|
456
|
+
"Fargate is parsing the current bellman params",
|
|
457
|
+
"verifying the previous contribution's chain link",
|
|
458
|
+
"this step has no contributor-side analogue",
|
|
459
|
+
"the worker does the boring work so you don't have to",
|
|
460
|
+
"any second now \u2014 challenge is almost ready",
|
|
461
|
+
"encoding the challenge in Bellman wire format",
|
|
462
|
+
"making sure the file size matches the spec",
|
|
463
|
+
"presign URL is signed for 1 hour",
|
|
464
|
+
"your download will be tamper-evident",
|
|
465
|
+
"the challenge SHA-256 will be on the public transcript",
|
|
466
|
+
"patience \u2014 this is the longest server-side step",
|
|
467
|
+
"almost there \u2014 your turn is moments away"
|
|
357
468
|
];
|
|
358
469
|
|
|
359
470
|
// src/components/QueueView.tsx
|
|
360
471
|
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
361
472
|
var POLL_FAST_MS = 5e3;
|
|
362
473
|
var POLL_SLOW_MS = 15e3;
|
|
474
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
475
|
+
"timed_out",
|
|
476
|
+
"failed",
|
|
477
|
+
"verified"
|
|
478
|
+
]);
|
|
363
479
|
function QueueView({ ceremonyId: ceremonyId2, trackId, token, onReady, onError }) {
|
|
364
480
|
const [status, setStatus] = useState2(null);
|
|
365
481
|
const [pollErr, setPollErr] = useState2(null);
|
|
@@ -381,6 +497,9 @@ function QueueView({ ceremonyId: ceremonyId2, trackId, token, onReady, onError }
|
|
|
381
497
|
onReady(s);
|
|
382
498
|
return;
|
|
383
499
|
}
|
|
500
|
+
if (TERMINAL_STATUSES.has(s.status)) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
384
503
|
const interval = s.queue_position <= 2 ? POLL_FAST_MS : POLL_SLOW_MS;
|
|
385
504
|
timeoutRef.current = setTimeout(poll, interval);
|
|
386
505
|
} catch (err) {
|
|
@@ -406,43 +525,55 @@ function QueueView({ ceremonyId: ceremonyId2, trackId, token, onReady, onError }
|
|
|
406
525
|
] })
|
|
407
526
|
] });
|
|
408
527
|
}
|
|
528
|
+
if (TERMINAL_STATUSES.has(status.status)) {
|
|
529
|
+
return /* @__PURE__ */ jsx2(TerminalSlotMessage, { status: status.status });
|
|
530
|
+
}
|
|
409
531
|
const waitMins = Math.ceil((status.estimated_wait_secs ?? 0) / 60);
|
|
410
532
|
const expiresAt = status.slot_expires_at ? new Date(status.slot_expires_at).toLocaleTimeString([], {
|
|
411
533
|
hour: "2-digit",
|
|
412
534
|
minute: "2-digit"
|
|
413
535
|
}) : null;
|
|
414
536
|
const fastPoll = status.queue_position <= 2;
|
|
537
|
+
const aheadCount = Math.max(0, status.queue_position - 1);
|
|
538
|
+
const peopleWord = aheadCount === 1 ? "person" : "people";
|
|
415
539
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
|
|
416
540
|
/* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
|
|
417
|
-
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
418
|
-
"
|
|
541
|
+
aheadCount === 0 ? /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
542
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "green", children: "You're next" }),
|
|
543
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " in line" })
|
|
544
|
+
] }) : /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
545
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: aheadCount }),
|
|
419
546
|
" ",
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
547
|
+
peopleWord,
|
|
548
|
+
" ahead of you"
|
|
549
|
+
] }),
|
|
550
|
+
status.queue_depth > 1 && /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
551
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\xB7" }),
|
|
552
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
553
|
+
status.queue_depth,
|
|
554
|
+
" in queue"
|
|
424
555
|
] })
|
|
425
556
|
] }),
|
|
426
557
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\xB7" }),
|
|
427
558
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
428
559
|
"Estimated wait:",
|
|
429
560
|
" ",
|
|
430
|
-
/* @__PURE__ */
|
|
431
|
-
"~",
|
|
432
|
-
waitMins,
|
|
433
|
-
" min"
|
|
434
|
-
] })
|
|
561
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: aheadCount === 0 ? "any moment now" : `~${waitMins} min` })
|
|
435
562
|
] })
|
|
436
563
|
] }),
|
|
437
|
-
status.status === "exporting" || status.status === "your_turn" || status.status === "ready_to_download" ? /* @__PURE__ */ jsx2(ExportingMessage, { status: status.status }) : status.active_since ? /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
564
|
+
status.status === "exporting" || status.status === "your_turn" || status.status === "ready_to_download" ? /* @__PURE__ */ jsx2(ExportingMessage, { status: status.status, slotExpiresAt: expiresAt }) : status.active_since ? /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
438
565
|
"Another contributor is active",
|
|
439
566
|
expiresAt ? ` \xB7 slot expires at ${expiresAt}` : ""
|
|
440
|
-
] }) }) :
|
|
567
|
+
] }) }) : aheadCount > 0 ? (
|
|
441
568
|
// Slot is idle but people are ahead — they joined and left without releasing.
|
|
442
569
|
// timeout_watchdog will clear each stale slot after contribution_timeout_secs.
|
|
443
|
-
/* @__PURE__ */
|
|
570
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
571
|
+
"Slot is idle \u2014 waiting for ",
|
|
572
|
+
peopleWord,
|
|
573
|
+
" ahead to respond or time out (up to ~5 min each)"
|
|
574
|
+
] })
|
|
444
575
|
) : (
|
|
445
|
-
//
|
|
576
|
+
// At the front of the line, slot idle — advance_queue should fire shortly.
|
|
446
577
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Slot is idle \u2014 your turn is being prepared..." })
|
|
447
578
|
),
|
|
448
579
|
pollErr ? /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
|
|
@@ -458,25 +589,63 @@ function QueueView({ ceremonyId: ceremonyId2, trackId, token, onReady, onError }
|
|
|
458
589
|
] })
|
|
459
590
|
] });
|
|
460
591
|
}
|
|
461
|
-
function
|
|
592
|
+
function TerminalSlotMessage({ status }) {
|
|
593
|
+
const lines = {
|
|
594
|
+
timed_out: {
|
|
595
|
+
headline: "Your slot expired before the challenge file was ready.",
|
|
596
|
+
detail: "This is usually a transient S3 hiccup on the worker side, not a problem with your machine. Press B or Backspace to return to the track list and rejoin the queue \u2014 it almost always succeeds the second time."
|
|
597
|
+
},
|
|
598
|
+
failed: {
|
|
599
|
+
headline: "The worker reported a failure on this contribution.",
|
|
600
|
+
detail: "Press B or Backspace to return to the track list. If this happens repeatedly on the same track, the admin needs to investigate."
|
|
601
|
+
},
|
|
602
|
+
verified: {
|
|
603
|
+
headline: "This contribution is already recorded as verified.",
|
|
604
|
+
detail: "Press B or Backspace to return to the track list and pick another track."
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
const key = status;
|
|
608
|
+
const msg = lines[key] ?? {
|
|
609
|
+
headline: `Contribution ended with status: ${status}.`,
|
|
610
|
+
detail: "Press B or Backspace to return to the track list."
|
|
611
|
+
};
|
|
612
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
|
|
613
|
+
/* @__PURE__ */ jsx2(Text2, { color: "yellow", bold: true, children: msg.headline }),
|
|
614
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: msg.detail })
|
|
615
|
+
] });
|
|
616
|
+
}
|
|
617
|
+
function ExportingMessage({
|
|
618
|
+
status,
|
|
619
|
+
slotExpiresAt
|
|
620
|
+
}) {
|
|
462
621
|
const exportingMsg = useCyclingMessage(EXPORTING_MESSAGES, 2500, status === "exporting");
|
|
463
|
-
return /* @__PURE__ */
|
|
464
|
-
/* @__PURE__ */ jsxs2(
|
|
465
|
-
/* @__PURE__ */
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
622
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
623
|
+
status === "exporting" ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
624
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
|
|
625
|
+
/* @__PURE__ */ jsx2(Spinner, { type: "dots" }),
|
|
626
|
+
" ",
|
|
627
|
+
exportingMsg,
|
|
628
|
+
"\u2026"
|
|
629
|
+
] }),
|
|
630
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Your slot is active. The worker is preparing the challenge file just for you." })
|
|
631
|
+
] }) : /* @__PURE__ */ jsx2(Text2, { color: "green", children: "Challenge ready \u2014 loading contribution flow\u2026" }),
|
|
632
|
+
slotExpiresAt && // Visible countdown is critical on power-18 circuits where the
|
|
633
|
+
// contributor compute window can run several minutes — without
|
|
634
|
+
// this they have no way to tell if they're about to be reaped.
|
|
635
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
636
|
+
"Your slot expires at ",
|
|
637
|
+
slotExpiresAt,
|
|
638
|
+
" (server time)."
|
|
639
|
+
] })
|
|
640
|
+
] });
|
|
472
641
|
}
|
|
473
642
|
|
|
474
|
-
// src/components/
|
|
475
|
-
import { useRef as useRef2, useState as useState3 } from "react";
|
|
643
|
+
// src/components/MouseEntropyCollector.tsx
|
|
644
|
+
import { useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
|
|
476
645
|
import { Box as Box3, Text as Text3, useInput } from "ink";
|
|
477
646
|
|
|
478
647
|
// src/entropy.ts
|
|
479
|
-
import { createHash as createHash2, randomBytes } from "crypto";
|
|
648
|
+
import { createHash as createHash2, hkdfSync, randomBytes } from "crypto";
|
|
480
649
|
function sha512(buf) {
|
|
481
650
|
return createHash2("sha512").update(buf).digest();
|
|
482
651
|
}
|
|
@@ -488,18 +657,405 @@ function buildEntropyFromKeystrokes(chars, timingsNs) {
|
|
|
488
657
|
const osHash = sha512(randomBytes(64));
|
|
489
658
|
return createHash2("sha512").update(keystrokeHash).update(osHash).digest("hex");
|
|
490
659
|
}
|
|
660
|
+
var TRAIL_MIN_BYTES = 64;
|
|
661
|
+
function buildEntropyFromMouseTrail(strokes) {
|
|
662
|
+
const strokeBuf = Buffer.alloc(strokes.length * 12);
|
|
663
|
+
for (let i = 0; i < strokes.length; i++) {
|
|
664
|
+
const s = strokes[i];
|
|
665
|
+
strokeBuf.writeUInt16BE(s.x & 65535, i * 12);
|
|
666
|
+
strokeBuf.writeUInt16BE(s.y & 65535, i * 12 + 2);
|
|
667
|
+
strokeBuf.writeBigUInt64BE(s.dtNs, i * 12 + 4);
|
|
668
|
+
}
|
|
669
|
+
const padBytes = Math.max(0, TRAIL_MIN_BYTES - strokeBuf.length);
|
|
670
|
+
const trailInput = padBytes > 0 ? Buffer.concat([strokeBuf, randomBytes(padBytes)]) : strokeBuf;
|
|
671
|
+
const trailHash = sha512(trailInput);
|
|
672
|
+
const osHash = sha512(randomBytes(64));
|
|
673
|
+
return createHash2("sha512").update(trailHash).update(osHash).digest("hex");
|
|
674
|
+
}
|
|
675
|
+
function deriveCircuitEntropy(masterSeed, ceremonyId2, circuitName) {
|
|
676
|
+
const ikm = Buffer.from(masterSeed, "hex");
|
|
677
|
+
const salt = createHash2("sha256").update(ceremonyId2).digest();
|
|
678
|
+
const info = Buffer.from(`umbra-ceremony-${ceremonyId2}-circuit-${circuitName}`, "utf8");
|
|
679
|
+
const derived = Buffer.from(hkdfSync("sha512", ikm, salt, info, 64));
|
|
680
|
+
return derived.toString("hex");
|
|
681
|
+
}
|
|
491
682
|
|
|
492
|
-
// src/
|
|
683
|
+
// src/mouse.ts
|
|
684
|
+
var SGR_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
685
|
+
function classifyButton(buttonCode) {
|
|
686
|
+
if (buttonCode & 32) return "motion";
|
|
687
|
+
const base = buttonCode & 3;
|
|
688
|
+
if (base === 0) return "left";
|
|
689
|
+
if (base === 1) return "middle";
|
|
690
|
+
if (base === 2) return "right";
|
|
691
|
+
return "other";
|
|
692
|
+
}
|
|
693
|
+
function parseMouseChunk(chunk) {
|
|
694
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("binary");
|
|
695
|
+
const events = [];
|
|
696
|
+
SGR_RE.lastIndex = 0;
|
|
697
|
+
let m;
|
|
698
|
+
while ((m = SGR_RE.exec(s)) !== null) {
|
|
699
|
+
const buttonCode = parseInt(m[1], 10);
|
|
700
|
+
const col = parseInt(m[2], 10) - 1;
|
|
701
|
+
const row = parseInt(m[3], 10) - 1;
|
|
702
|
+
events.push({
|
|
703
|
+
button: classifyButton(buttonCode),
|
|
704
|
+
col,
|
|
705
|
+
row,
|
|
706
|
+
isRelease: m[4] === "m"
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
return events;
|
|
710
|
+
}
|
|
711
|
+
function enableMouseReporting() {
|
|
712
|
+
process.stdout.write("\x1B[?1003h\x1B[?1006h");
|
|
713
|
+
return () => {
|
|
714
|
+
process.stdout.write("\x1B[?1003l\x1B[?1006l");
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/components/MouseEntropyCollector.tsx
|
|
493
719
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
720
|
+
var LEFT_OFFSET = 1;
|
|
721
|
+
var TOP_OFFSET = 2;
|
|
722
|
+
var MIN_W = 18;
|
|
723
|
+
var MIN_H = 12;
|
|
724
|
+
var MAX_W = 80;
|
|
725
|
+
var MAX_H = 32;
|
|
726
|
+
var DEFAULT_ASPECT = 2.2;
|
|
727
|
+
var ASPECT = (() => {
|
|
728
|
+
const raw = process.env["CEREMONY_CANVAS_ASPECT"];
|
|
729
|
+
if (!raw) return DEFAULT_ASPECT;
|
|
730
|
+
const parsed = Number.parseFloat(raw);
|
|
731
|
+
if (!Number.isFinite(parsed) || parsed < 0.5 || parsed > 6) return DEFAULT_ASPECT;
|
|
732
|
+
return parsed;
|
|
733
|
+
})();
|
|
734
|
+
var CHROME_ROWS = 6;
|
|
735
|
+
var CHROME_COLS = 2;
|
|
736
|
+
var MIN_ELAPSED_MS = 3e3;
|
|
737
|
+
var RENDER_INTERVAL_MS = 33;
|
|
738
|
+
function deriveCanvasSize(cols, rows) {
|
|
739
|
+
const availH = rows - CHROME_ROWS;
|
|
740
|
+
const availW = cols - CHROME_COLS;
|
|
741
|
+
let h = Math.min(MAX_H, availH);
|
|
742
|
+
let w = Math.round(h * ASPECT);
|
|
743
|
+
const wCap = Math.min(MAX_W, availW);
|
|
744
|
+
if (w > wCap) {
|
|
745
|
+
w = wCap;
|
|
746
|
+
h = Math.round(w / ASPECT);
|
|
747
|
+
}
|
|
748
|
+
if (w < MIN_W || h < MIN_H) return null;
|
|
749
|
+
return { w, h };
|
|
750
|
+
}
|
|
751
|
+
function readTerminalDims() {
|
|
752
|
+
return {
|
|
753
|
+
cols: process.stdout.columns ?? 80,
|
|
754
|
+
rows: process.stdout.rows ?? 24
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function MouseEntropyCollector({ title, onComplete, onError }) {
|
|
758
|
+
const [dims, setDims] = useState3(() => {
|
|
759
|
+
const { cols, rows } = readTerminalDims();
|
|
760
|
+
return deriveCanvasSize(cols, rows);
|
|
761
|
+
});
|
|
762
|
+
const dimsRef = useRef2(dims);
|
|
763
|
+
useEffect3(() => {
|
|
764
|
+
dimsRef.current = dims;
|
|
765
|
+
}, [dims]);
|
|
766
|
+
const pixelsRef = useRef2(/* @__PURE__ */ new Set());
|
|
767
|
+
const strokesRef = useRef2([]);
|
|
768
|
+
const mouseDownRef = useRef2(false);
|
|
769
|
+
const lastPaintRef = useRef2(null);
|
|
770
|
+
const cursorRef = useRef2(null);
|
|
771
|
+
const lastStrokeTsRef = useRef2(process.hrtime.bigint());
|
|
772
|
+
const startTsRef = useRef2(process.hrtime.bigint());
|
|
773
|
+
const completedRef = useRef2(false);
|
|
774
|
+
const [pixelCount, setPixelCount] = useState3(0);
|
|
775
|
+
const [elapsedMs, setElapsedMs] = useState3(0);
|
|
776
|
+
const [done, setDone] = useState3(false);
|
|
777
|
+
const [nudge, setNudge] = useState3(null);
|
|
778
|
+
const [grid, setGrid] = useState3(
|
|
779
|
+
() => dims ? Array.from({ length: dims.h }, () => " ".repeat(dims.w)) : []
|
|
780
|
+
);
|
|
781
|
+
const [cursor, setCursor] = useState3(null);
|
|
782
|
+
const canvasW = dims?.w ?? 0;
|
|
783
|
+
const canvasH = dims?.h ?? 0;
|
|
784
|
+
const subH = canvasH * 2;
|
|
785
|
+
function idx(x, y) {
|
|
786
|
+
return y * canvasW + x;
|
|
787
|
+
}
|
|
788
|
+
function paintLine(pixels, x0, y0, x1, y1, onNew) {
|
|
789
|
+
let dx = Math.abs(x1 - x0);
|
|
790
|
+
let dy = -Math.abs(y1 - y0);
|
|
791
|
+
const sx = x0 < x1 ? 1 : -1;
|
|
792
|
+
const sy = y0 < y1 ? 1 : -1;
|
|
793
|
+
let err = dx + dy;
|
|
794
|
+
let x = x0;
|
|
795
|
+
let y = y0;
|
|
796
|
+
while (true) {
|
|
797
|
+
if (x >= 0 && x < canvasW && y >= 0 && y < subH) {
|
|
798
|
+
const id = idx(x, y);
|
|
799
|
+
if (!pixels.has(id)) {
|
|
800
|
+
pixels.add(id);
|
|
801
|
+
onNew(x, y);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (x === x1 && y === y1) break;
|
|
805
|
+
const e2 = 2 * err;
|
|
806
|
+
if (e2 >= dy) {
|
|
807
|
+
if (x === x1) break;
|
|
808
|
+
err += dy;
|
|
809
|
+
x += sx;
|
|
810
|
+
}
|
|
811
|
+
if (e2 <= dx) {
|
|
812
|
+
if (y === y1) break;
|
|
813
|
+
err += dx;
|
|
814
|
+
y += sy;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
useEffect3(() => {
|
|
819
|
+
const onResize = () => {
|
|
820
|
+
const { cols, rows } = readTerminalDims();
|
|
821
|
+
const next = deriveCanvasSize(cols, rows);
|
|
822
|
+
const cur = dimsRef.current;
|
|
823
|
+
if (next === null && cur === null) return;
|
|
824
|
+
if (next !== null && cur !== null && next.w === cur.w && next.h === cur.h) return;
|
|
825
|
+
pixelsRef.current.clear();
|
|
826
|
+
strokesRef.current = [];
|
|
827
|
+
lastPaintRef.current = null;
|
|
828
|
+
cursorRef.current = null;
|
|
829
|
+
startTsRef.current = process.hrtime.bigint();
|
|
830
|
+
lastStrokeTsRef.current = startTsRef.current;
|
|
831
|
+
setNudge(null);
|
|
832
|
+
setPixelCount(0);
|
|
833
|
+
setElapsedMs(0);
|
|
834
|
+
setCursor(null);
|
|
835
|
+
setGrid(next ? Array.from({ length: next.h }, () => " ".repeat(next.w)) : []);
|
|
836
|
+
setDims(next);
|
|
837
|
+
};
|
|
838
|
+
process.stdout.on("resize", onResize);
|
|
839
|
+
return () => {
|
|
840
|
+
process.stdout.off("resize", onResize);
|
|
841
|
+
};
|
|
842
|
+
}, []);
|
|
843
|
+
useEffect3(() => {
|
|
844
|
+
if (!dims) return;
|
|
845
|
+
const disableMouse = enableMouseReporting();
|
|
846
|
+
const onData = (chunk) => {
|
|
847
|
+
if (completedRef.current) return;
|
|
848
|
+
const events = parseMouseChunk(chunk);
|
|
849
|
+
for (const ev of events) handleMouseEvent(ev);
|
|
850
|
+
};
|
|
851
|
+
process.stdin.on("data", onData);
|
|
852
|
+
const renderTick = setInterval(() => {
|
|
853
|
+
if (completedRef.current) return;
|
|
854
|
+
const size = pixelsRef.current.size;
|
|
855
|
+
const elapsed = Number((process.hrtime.bigint() - startTsRef.current) / 1000000n);
|
|
856
|
+
if (size !== pixelCount) setPixelCount(size);
|
|
857
|
+
if (elapsed !== elapsedMs) setElapsedMs(elapsed);
|
|
858
|
+
const next = [];
|
|
859
|
+
for (let r = 0; r < canvasH; r++) {
|
|
860
|
+
let line = "";
|
|
861
|
+
for (let c = 0; c < canvasW; c++) {
|
|
862
|
+
const top = pixelsRef.current.has(idx(c, r * 2));
|
|
863
|
+
const bot = pixelsRef.current.has(idx(c, r * 2 + 1));
|
|
864
|
+
line += top && bot ? "\u2588" : top ? "\u2580" : bot ? "\u2584" : " ";
|
|
865
|
+
}
|
|
866
|
+
next.push(line);
|
|
867
|
+
}
|
|
868
|
+
setGrid(next);
|
|
869
|
+
setCursor(cursorRef.current);
|
|
870
|
+
}, RENDER_INTERVAL_MS);
|
|
871
|
+
return () => {
|
|
872
|
+
clearInterval(renderTick);
|
|
873
|
+
process.stdin.off("data", onData);
|
|
874
|
+
disableMouse();
|
|
875
|
+
};
|
|
876
|
+
}, [canvasW, canvasH]);
|
|
877
|
+
function handleMouseEvent(ev) {
|
|
878
|
+
if (completedRef.current) return;
|
|
879
|
+
const canvasCol = ev.col - LEFT_OFFSET;
|
|
880
|
+
const canvasRow = ev.row - TOP_OFFSET;
|
|
881
|
+
const inBounds = canvasCol >= 0 && canvasCol < canvasW && canvasRow >= 0 && canvasRow < canvasH;
|
|
882
|
+
if (inBounds) cursorRef.current = { row: canvasRow, col: canvasCol };
|
|
883
|
+
else cursorRef.current = null;
|
|
884
|
+
if (ev.button === "left" && !ev.isRelease) {
|
|
885
|
+
mouseDownRef.current = true;
|
|
886
|
+
lastPaintRef.current = null;
|
|
887
|
+
if (inBounds) paintAt(canvasCol, canvasRow);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (ev.isRelease) {
|
|
891
|
+
mouseDownRef.current = false;
|
|
892
|
+
lastPaintRef.current = null;
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (ev.button === "motion" && mouseDownRef.current && inBounds) {
|
|
896
|
+
paintAt(canvasCol, canvasRow);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function paintAt(canvasCol, canvasRow) {
|
|
900
|
+
const x = canvasCol;
|
|
901
|
+
const yTop = canvasRow * 2;
|
|
902
|
+
const yBot = canvasRow * 2 + 1;
|
|
903
|
+
const now = process.hrtime.bigint();
|
|
904
|
+
const dt = now - lastStrokeTsRef.current;
|
|
905
|
+
lastStrokeTsRef.current = now;
|
|
906
|
+
if (lastPaintRef.current) {
|
|
907
|
+
const lp = lastPaintRef.current;
|
|
908
|
+
paintLine(pixelsRef.current, lp.x, lp.y, x, yTop, (px, py) => {
|
|
909
|
+
strokesRef.current.push({ x: px, y: py, dtNs: dt });
|
|
910
|
+
});
|
|
911
|
+
paintLine(pixelsRef.current, lp.x, lp.y + 1, x, yBot, () => {
|
|
912
|
+
});
|
|
913
|
+
} else {
|
|
914
|
+
if (!pixelsRef.current.has(idx(x, yTop))) {
|
|
915
|
+
pixelsRef.current.add(idx(x, yTop));
|
|
916
|
+
strokesRef.current.push({ x, y: yTop, dtNs: dt });
|
|
917
|
+
}
|
|
918
|
+
if (!pixelsRef.current.has(idx(x, yBot))) {
|
|
919
|
+
pixelsRef.current.add(idx(x, yBot));
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
lastPaintRef.current = { x, y: yTop };
|
|
923
|
+
}
|
|
924
|
+
useInput((input, key) => {
|
|
925
|
+
if (completedRef.current) return;
|
|
926
|
+
if (key.backspace || key.delete || input === "c" || input === "C") {
|
|
927
|
+
pixelsRef.current.clear();
|
|
928
|
+
strokesRef.current = [];
|
|
929
|
+
lastPaintRef.current = null;
|
|
930
|
+
cursorRef.current = null;
|
|
931
|
+
startTsRef.current = process.hrtime.bigint();
|
|
932
|
+
lastStrokeTsRef.current = startTsRef.current;
|
|
933
|
+
setNudge(null);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (key.return) {
|
|
937
|
+
const elapsed = Number((process.hrtime.bigint() - startTsRef.current) / 1000000n);
|
|
938
|
+
if (elapsed < MIN_ELAPSED_MS) {
|
|
939
|
+
const secLeft = Math.ceil((MIN_ELAPSED_MS - elapsed) / 1e3);
|
|
940
|
+
setNudge(`Wait ${secLeft}s before committing (anti-fat-finger).`);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
completedRef.current = true;
|
|
944
|
+
setDone(true);
|
|
945
|
+
try {
|
|
946
|
+
const entropy = buildEntropyFromMouseTrail(strokesRef.current);
|
|
947
|
+
setTimeout(() => onComplete(entropy), 250);
|
|
948
|
+
} catch (e) {
|
|
949
|
+
onError(e instanceof Error ? e : new Error(String(e)));
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
if (!dims) {
|
|
954
|
+
const { cols, rows } = readTerminalDims();
|
|
955
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
|
|
956
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: "\u26A0 Terminal too small for the mouse canvas" }),
|
|
957
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
958
|
+
"Need at least ",
|
|
959
|
+
MIN_W + CHROME_COLS,
|
|
960
|
+
" cols \xD7 ",
|
|
961
|
+
MIN_H + CHROME_ROWS,
|
|
962
|
+
" rows. Got ",
|
|
963
|
+
cols,
|
|
964
|
+
" \xD7 ",
|
|
965
|
+
rows,
|
|
966
|
+
"."
|
|
967
|
+
] }),
|
|
968
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
969
|
+
"Resize this window larger, or press ",
|
|
970
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "\u232B Backspace" }),
|
|
971
|
+
" to go back and then ",
|
|
972
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "magenta", children: "K" }),
|
|
973
|
+
" on the mode-select modal to use keyboard entropy instead."
|
|
974
|
+
] })
|
|
975
|
+
] });
|
|
976
|
+
}
|
|
977
|
+
const secsElapsed = Math.floor(elapsedMs / 1e3);
|
|
978
|
+
const minSecs = Math.ceil(MIN_ELAPSED_MS / 1e3);
|
|
979
|
+
const timePct = Math.min(100, Math.round(elapsedMs / MIN_ELAPSED_MS * 100));
|
|
980
|
+
const filled = Math.round(timePct / 100 * 20);
|
|
981
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(20 - filled);
|
|
982
|
+
const ready = elapsedMs >= MIN_ELAPSED_MS;
|
|
983
|
+
if (done) {
|
|
984
|
+
return /* @__PURE__ */ jsxs3(Box3, { gap: 2, children: [
|
|
985
|
+
/* @__PURE__ */ jsx3(Text3, { color: "green", bold: true, children: "\u2713" }),
|
|
986
|
+
/* @__PURE__ */ jsx3(Text3, { children: "Entropy committed." })
|
|
987
|
+
] });
|
|
988
|
+
}
|
|
989
|
+
const renderRow = (rowStr, rowIdx) => {
|
|
990
|
+
if (!cursor || cursor.row !== rowIdx) {
|
|
991
|
+
return /* @__PURE__ */ jsx3(Text3, { color: "white", children: rowStr });
|
|
992
|
+
}
|
|
993
|
+
const cIdx = cursor.col;
|
|
994
|
+
const before = rowStr.slice(0, cIdx);
|
|
995
|
+
const at = rowStr[cIdx] ?? " ";
|
|
996
|
+
const after = rowStr.slice(cIdx + 1);
|
|
997
|
+
return /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
998
|
+
/* @__PURE__ */ jsx3(Text3, { color: "white", children: before }),
|
|
999
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", inverse: true, children: at === " " ? "\xB7" : at }),
|
|
1000
|
+
/* @__PURE__ */ jsx3(Text3, { color: "white", children: after })
|
|
1001
|
+
] });
|
|
1002
|
+
};
|
|
1003
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
1004
|
+
/* @__PURE__ */ jsx3(Text3, { children: title ? /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: title.length > 70 ? title.slice(0, 67) + "\u2026" : title }) : /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "Draw to generate entropy \u2014 hold the LEFT mouse button and drag" }) }),
|
|
1005
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1006
|
+
"\u250C",
|
|
1007
|
+
"\u2500".repeat(canvasW),
|
|
1008
|
+
"\u2510"
|
|
1009
|
+
] }),
|
|
1010
|
+
grid.map((row, i) => /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1011
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2502" }),
|
|
1012
|
+
renderRow(row, i),
|
|
1013
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2502" })
|
|
1014
|
+
] }, i)),
|
|
1015
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1016
|
+
"\u2514",
|
|
1017
|
+
"\u2500".repeat(canvasW),
|
|
1018
|
+
"\u2518"
|
|
1019
|
+
] }),
|
|
1020
|
+
/* @__PURE__ */ jsxs3(Box3, { gap: 2, children: [
|
|
1021
|
+
/* @__PURE__ */ jsxs3(Text3, { color: ready ? "green" : "cyan", children: [
|
|
1022
|
+
"[",
|
|
1023
|
+
bar,
|
|
1024
|
+
"]"
|
|
1025
|
+
] }),
|
|
1026
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1027
|
+
/* @__PURE__ */ jsxs3(Text3, { bold: true, color: ready ? "green" : "yellow", children: [
|
|
1028
|
+
secsElapsed,
|
|
1029
|
+
"s"
|
|
1030
|
+
] }),
|
|
1031
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1032
|
+
" ",
|
|
1033
|
+
"/ ",
|
|
1034
|
+
minSecs,
|
|
1035
|
+
"s \xB7 ",
|
|
1036
|
+
pixelCount,
|
|
1037
|
+
" pixels drawn"
|
|
1038
|
+
] })
|
|
1039
|
+
] }),
|
|
1040
|
+
nudge && /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: nudge })
|
|
1041
|
+
] }),
|
|
1042
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Enter = commit \xB7 c / \u232B = clear \xB7 Ctrl+C / Ctrl+Z = abort" })
|
|
1043
|
+
] });
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/components/EntropyCollector.tsx
|
|
1047
|
+
import { useRef as useRef3, useState as useState4 } from "react";
|
|
1048
|
+
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
1049
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
494
1050
|
var TARGET = 20;
|
|
495
1051
|
function EntropyCollector({ onComplete, onError }) {
|
|
496
|
-
const [count, setCount] =
|
|
497
|
-
const [done, setDone] =
|
|
498
|
-
const charsRef =
|
|
499
|
-
const timingsRef =
|
|
500
|
-
const lastRef =
|
|
501
|
-
const completedRef =
|
|
502
|
-
|
|
1052
|
+
const [count, setCount] = useState4(0);
|
|
1053
|
+
const [done, setDone] = useState4(false);
|
|
1054
|
+
const charsRef = useRef3([]);
|
|
1055
|
+
const timingsRef = useRef3([]);
|
|
1056
|
+
const lastRef = useRef3(process.hrtime.bigint());
|
|
1057
|
+
const completedRef = useRef3(false);
|
|
1058
|
+
useInput2((input) => {
|
|
503
1059
|
if (completedRef.current) return;
|
|
504
1060
|
const now = process.hrtime.bigint();
|
|
505
1061
|
timingsRef.current.push(now - lastRef.current);
|
|
@@ -523,32 +1079,32 @@ function EntropyCollector({ onComplete, onError }) {
|
|
|
523
1079
|
const pct = Math.round(count / TARGET * 100);
|
|
524
1080
|
const stars = "*".repeat(Math.min(count, 32));
|
|
525
1081
|
if (done) {
|
|
526
|
-
return /* @__PURE__ */
|
|
527
|
-
/* @__PURE__ */
|
|
528
|
-
/* @__PURE__ */
|
|
1082
|
+
return /* @__PURE__ */ jsxs4(Box4, { gap: 2, children: [
|
|
1083
|
+
/* @__PURE__ */ jsx4(Text4, { color: "green", bold: true, children: "\u2713" }),
|
|
1084
|
+
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
529
1085
|
"Entropy collected \u2014 [",
|
|
530
1086
|
bar,
|
|
531
1087
|
"] 100%"
|
|
532
1088
|
] })
|
|
533
1089
|
] });
|
|
534
1090
|
}
|
|
535
|
-
return /* @__PURE__ */
|
|
536
|
-
/* @__PURE__ */
|
|
537
|
-
/* @__PURE__ */
|
|
538
|
-
/* @__PURE__ */
|
|
539
|
-
/* @__PURE__ */
|
|
540
|
-
/* @__PURE__ */
|
|
541
|
-
/* @__PURE__ */
|
|
1091
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
|
|
1092
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, children: "Type anything to generate entropy:" }),
|
|
1093
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Keystroke timing (nanosecond intervals) is the randomness source." }),
|
|
1094
|
+
/* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
|
|
1095
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " > " }),
|
|
1096
|
+
/* @__PURE__ */ jsx4(Text4, { color: "green", children: stars }),
|
|
1097
|
+
/* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "\u2588" })
|
|
542
1098
|
] }),
|
|
543
|
-
/* @__PURE__ */
|
|
544
|
-
/* @__PURE__ */
|
|
1099
|
+
/* @__PURE__ */ jsxs4(Box4, { gap: 2, marginTop: 1, children: [
|
|
1100
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "cyan", children: [
|
|
545
1101
|
"[",
|
|
546
1102
|
bar,
|
|
547
1103
|
"]"
|
|
548
1104
|
] }),
|
|
549
|
-
/* @__PURE__ */
|
|
550
|
-
/* @__PURE__ */
|
|
551
|
-
/* @__PURE__ */
|
|
1105
|
+
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
1106
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, color: count > 0 ? "green" : "yellow", children: count }),
|
|
1107
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
552
1108
|
"/",
|
|
553
1109
|
TARGET,
|
|
554
1110
|
" keystrokes (",
|
|
@@ -557,13 +1113,14 @@ function EntropyCollector({ onComplete, onError }) {
|
|
|
557
1113
|
] })
|
|
558
1114
|
] })
|
|
559
1115
|
] }),
|
|
560
|
-
/* @__PURE__ */
|
|
1116
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Shown as * \u2014 your actual input is never revealed." }),
|
|
1117
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "To quit without contributing: Ctrl+C or Ctrl+Z (Q is part of the entropy alphabet here)." })
|
|
561
1118
|
] });
|
|
562
1119
|
}
|
|
563
1120
|
|
|
564
1121
|
// src/components/ContributeFlow.tsx
|
|
565
|
-
import { useEffect as
|
|
566
|
-
import { Box as
|
|
1122
|
+
import { useEffect as useEffect5, useState as useState5 } from "react";
|
|
1123
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
567
1124
|
import Spinner2 from "ink-spinner";
|
|
568
1125
|
import { tmpdir as tmpdir2 } from "os";
|
|
569
1126
|
import { join as join4 } from "path";
|
|
@@ -590,7 +1147,7 @@ async function cleanupTemp(path) {
|
|
|
590
1147
|
}
|
|
591
1148
|
|
|
592
1149
|
// src/components/ContributeFlow.tsx
|
|
593
|
-
import { jsx as
|
|
1150
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
594
1151
|
var STEP_LABELS = ["downloading", "computing", "uploading", "signalling", "verifying"];
|
|
595
1152
|
var STEP_INDEX = {
|
|
596
1153
|
downloading: 0,
|
|
@@ -601,8 +1158,8 @@ var STEP_INDEX = {
|
|
|
601
1158
|
};
|
|
602
1159
|
function ContributeFlow(props) {
|
|
603
1160
|
const { ceremonyId: ceremonyId2, trackId, token, slotStatus, entropy, displayName: displayName2 } = props;
|
|
604
|
-
const [step, setStep] =
|
|
605
|
-
|
|
1161
|
+
const [step, setStep] = useState5({ name: "downloading", bytesReceived: 0, total: null });
|
|
1162
|
+
useEffect5(() => {
|
|
606
1163
|
let cancelled = false;
|
|
607
1164
|
const challengePath = join4(tmpdir2(), `ceremony-challenge-${Date.now()}.mpcparams`);
|
|
608
1165
|
let responsePath = null;
|
|
@@ -685,112 +1242,138 @@ function ContributeFlow(props) {
|
|
|
685
1242
|
const currentIdx = STEP_INDEX[step.name] ?? 0;
|
|
686
1243
|
const computingMsg = useCyclingMessage(COMPUTING_MESSAGES, 2500, step.name === "computing");
|
|
687
1244
|
const verifyingMsg = useCyclingMessage(VERIFYING_MESSAGES, 3e3, step.name === "verifying");
|
|
688
|
-
return /* @__PURE__ */
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1245
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
|
|
1246
|
+
STEP_LABELS.map((label, i) => {
|
|
1247
|
+
const isDone = i < currentIdx;
|
|
1248
|
+
const isActive = i === currentIdx;
|
|
1249
|
+
const isPending = i > currentIdx;
|
|
1250
|
+
let indicator;
|
|
1251
|
+
if (isDone) indicator = /* @__PURE__ */ jsx5(Text5, { color: "green", children: "\u2713" });
|
|
1252
|
+
else if (isActive) indicator = /* @__PURE__ */ jsx5(Spinner2, { type: "dots" });
|
|
1253
|
+
else indicator = /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u25CB" });
|
|
1254
|
+
let detail = null;
|
|
1255
|
+
if (isActive && step.name === "downloading" && step.total) {
|
|
1256
|
+
const pct = Math.round(step.bytesReceived / step.total * 100);
|
|
1257
|
+
const filled = Math.round(pct / 5);
|
|
1258
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(20 - filled);
|
|
1259
|
+
detail = /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
1260
|
+
" ",
|
|
1261
|
+
"[",
|
|
1262
|
+
bar,
|
|
1263
|
+
"] ",
|
|
1264
|
+
pct,
|
|
1265
|
+
"%"
|
|
1266
|
+
] });
|
|
1267
|
+
}
|
|
1268
|
+
if (isActive && step.name === "verifying") {
|
|
1269
|
+
const elapsed = step.attempt <= 10 ? step.attempt * 3 : 10 * 3 + (step.attempt - 10) * 10;
|
|
1270
|
+
const mins = Math.floor(elapsed / 60);
|
|
1271
|
+
const secs = elapsed % 60;
|
|
1272
|
+
const time = mins > 0 ? `${mins}m ${secs.toString().padStart(2, "0")}s` : `${secs}s`;
|
|
1273
|
+
detail = /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
1274
|
+
" ",
|
|
1275
|
+
verifyingMsg,
|
|
1276
|
+
"\u2026 (",
|
|
1277
|
+
time,
|
|
1278
|
+
" elapsed)"
|
|
1279
|
+
] });
|
|
1280
|
+
}
|
|
1281
|
+
if (isActive && step.name === "computing") {
|
|
1282
|
+
detail = /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
1283
|
+
" ",
|
|
1284
|
+
computingMsg,
|
|
1285
|
+
"\u2026"
|
|
1286
|
+
] });
|
|
1287
|
+
}
|
|
1288
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
1289
|
+
/* @__PURE__ */ jsxs5(Box5, { gap: 2, children: [
|
|
1290
|
+
indicator,
|
|
1291
|
+
/* @__PURE__ */ jsxs5(
|
|
1292
|
+
Text5,
|
|
1293
|
+
{
|
|
1294
|
+
color: isDone ? "green" : isActive ? "white" : void 0,
|
|
1295
|
+
bold: isActive,
|
|
1296
|
+
dimColor: isPending,
|
|
1297
|
+
children: [
|
|
1298
|
+
label.charAt(0).toUpperCase() + label.slice(1),
|
|
1299
|
+
" ",
|
|
1300
|
+
isActive && label === "computing" && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "(entropy stays local)" })
|
|
1301
|
+
]
|
|
1302
|
+
}
|
|
1303
|
+
)
|
|
1304
|
+
] }),
|
|
1305
|
+
detail
|
|
1306
|
+
] }, label);
|
|
1307
|
+
}),
|
|
1308
|
+
slotStatus.slot_expires_at && step.name !== "verifying" && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
1309
|
+
"Your slot expires at",
|
|
1310
|
+
" ",
|
|
1311
|
+
new Date(slotStatus.slot_expires_at).toLocaleTimeString([], {
|
|
1312
|
+
hour: "2-digit",
|
|
1313
|
+
minute: "2-digit"
|
|
1314
|
+
}),
|
|
1315
|
+
" ",
|
|
1316
|
+
"(server time)."
|
|
1317
|
+
] }) }),
|
|
1318
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1319
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
|
|
1320
|
+
/* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
1321
|
+
"Need to bail? Press",
|
|
725
1322
|
" ",
|
|
726
|
-
|
|
727
|
-
"
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
indicator,
|
|
733
|
-
/* @__PURE__ */ jsxs4(
|
|
734
|
-
Text4,
|
|
735
|
-
{
|
|
736
|
-
color: isDone ? "green" : isActive ? "white" : void 0,
|
|
737
|
-
bold: isActive,
|
|
738
|
-
dimColor: isPending,
|
|
739
|
-
children: [
|
|
740
|
-
label.charAt(0).toUpperCase() + label.slice(1),
|
|
741
|
-
" ",
|
|
742
|
-
isActive && label === "computing" && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "(entropy stays local)" })
|
|
743
|
-
]
|
|
744
|
-
}
|
|
745
|
-
)
|
|
1323
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Q" }),
|
|
1324
|
+
", ",
|
|
1325
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Ctrl+C" }),
|
|
1326
|
+
", or ",
|
|
1327
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Ctrl+Z" }),
|
|
1328
|
+
" \u2014 your slot is released immediately so the next contributor can take over."
|
|
746
1329
|
] }),
|
|
747
|
-
|
|
748
|
-
] }
|
|
749
|
-
|
|
1330
|
+
step.name === "computing" && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "(During this step snarkjs is busy \u2014 Ctrl+C / Ctrl+Z respond faster than Q.)" })
|
|
1331
|
+
] })
|
|
1332
|
+
] });
|
|
750
1333
|
}
|
|
751
1334
|
|
|
752
1335
|
// src/components/Attestation.tsx
|
|
753
|
-
import { Box as
|
|
754
|
-
import { jsx as
|
|
1336
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
1337
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
755
1338
|
function Attestation({ contribution }) {
|
|
756
1339
|
const hashShort = contribution.contributionHash ? `${contribution.contributionHash.slice(0, 16)}...${contribution.contributionHash.slice(-8)}` : "(pending verification)";
|
|
757
1340
|
const verifiedAt = contribution.verifiedAt ? new Date(contribution.verifiedAt).toLocaleString() : null;
|
|
758
|
-
return /* @__PURE__ */
|
|
759
|
-
/* @__PURE__ */
|
|
760
|
-
/* @__PURE__ */
|
|
761
|
-
/* @__PURE__ */
|
|
762
|
-
/* @__PURE__ */
|
|
763
|
-
/* @__PURE__ */
|
|
1341
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
|
|
1342
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "green", children: "\u2713 Contribution verified!" }),
|
|
1343
|
+
/* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 0, children: [
|
|
1344
|
+
/* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
|
|
1345
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Circuit" }),
|
|
1346
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, children: contribution.circuitName })
|
|
764
1347
|
] }),
|
|
765
|
-
/* @__PURE__ */
|
|
766
|
-
/* @__PURE__ */
|
|
767
|
-
/* @__PURE__ */
|
|
1348
|
+
/* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
|
|
1349
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Round " }),
|
|
1350
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
|
|
768
1351
|
"#",
|
|
769
1352
|
contribution.sequenceNumber
|
|
770
1353
|
] })
|
|
771
1354
|
] }),
|
|
772
|
-
/* @__PURE__ */
|
|
773
|
-
/* @__PURE__ */
|
|
774
|
-
/* @__PURE__ */
|
|
1355
|
+
/* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
|
|
1356
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Hash " }),
|
|
1357
|
+
/* @__PURE__ */ jsx6(Text6, { color: "cyan", children: hashShort })
|
|
775
1358
|
] }),
|
|
776
|
-
verifiedAt && /* @__PURE__ */
|
|
777
|
-
/* @__PURE__ */
|
|
778
|
-
/* @__PURE__ */
|
|
1359
|
+
verifiedAt && /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
|
|
1360
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Time " }),
|
|
1361
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: verifiedAt })
|
|
779
1362
|
] })
|
|
780
1363
|
] }),
|
|
781
|
-
contribution.contributionHash && /* @__PURE__ */
|
|
782
|
-
/* @__PURE__ */
|
|
783
|
-
/* @__PURE__ */
|
|
1364
|
+
contribution.contributionHash && /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
|
|
1365
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Full hash (share this to prove participation):" }),
|
|
1366
|
+
/* @__PURE__ */ jsx6(Text6, { color: "cyan", wrap: "wrap", children: contribution.contributionHash })
|
|
784
1367
|
] })
|
|
785
1368
|
] });
|
|
786
1369
|
}
|
|
787
1370
|
|
|
788
1371
|
// src/components/InfoModal.tsx
|
|
789
|
-
import { Box as
|
|
790
|
-
import { jsx as
|
|
1372
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
1373
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
791
1374
|
function InfoModal() {
|
|
792
|
-
return /* @__PURE__ */
|
|
793
|
-
|
|
1375
|
+
return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, children: /* @__PURE__ */ jsxs7(
|
|
1376
|
+
Box7,
|
|
794
1377
|
{
|
|
795
1378
|
flexDirection: "column",
|
|
796
1379
|
borderStyle: "round",
|
|
@@ -800,34 +1383,66 @@ function InfoModal() {
|
|
|
800
1383
|
width: 84,
|
|
801
1384
|
gap: 1,
|
|
802
1385
|
children: [
|
|
803
|
-
/* @__PURE__ */
|
|
804
|
-
/* @__PURE__ */
|
|
805
|
-
/* @__PURE__ */
|
|
806
|
-
/* @__PURE__ */
|
|
1386
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "Umbra Phase 2 Trusted-Setup Ceremony" }),
|
|
1387
|
+
/* @__PURE__ */ jsx7(Text7, { children: "Umbra uses Groth16 zero-knowledge proofs to give Solana users on-chain privacy. Every circuit needs a one-time multi-party ceremony to generate its proving key safely \u2014 that is Phase 2. Each contributor adds their own secret entropy and destroys it afterward. The setup stays secure as long as AT LEAST ONE contributor erased theirs, which is why your single contribution genuinely matters." }),
|
|
1388
|
+
/* @__PURE__ */ jsx7(Text7, { children: "What you will do: tap 20 keys to seed your entropy, download the latest challenge file, run snarkjs locally to combine your secret with the parameters, and upload the response. Your secret never leaves your machine. Erase it when you are done." }),
|
|
1389
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press B, Esc or ? to close" })
|
|
807
1390
|
]
|
|
808
1391
|
}
|
|
809
1392
|
) });
|
|
810
1393
|
}
|
|
811
1394
|
|
|
812
1395
|
// src/components/App.tsx
|
|
813
|
-
import { jsx as
|
|
1396
|
+
import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
814
1397
|
var NAME_MAX_LEN = 100;
|
|
815
1398
|
var NAME_VALID_RE = /^[\p{L}\p{N} _.\-]*$/u;
|
|
1399
|
+
function elideMiddle(s, maxLen) {
|
|
1400
|
+
if (s.length <= maxLen) return s;
|
|
1401
|
+
const keepHead = Math.ceil((maxLen - 1) / 2);
|
|
1402
|
+
const keepTail = Math.floor((maxLen - 1) / 2);
|
|
1403
|
+
return s.slice(0, keepHead) + "\u2026" + s.slice(s.length - keepTail);
|
|
1404
|
+
}
|
|
1405
|
+
function orderTracksForAuto(tracks, contributed) {
|
|
1406
|
+
const openTracks = tracks.filter((t) => t.status === "open");
|
|
1407
|
+
const fresh = [];
|
|
1408
|
+
const alreadyDone = [];
|
|
1409
|
+
for (const t of openTracks) {
|
|
1410
|
+
if ((contributed[t.id]?.length ?? 0) > 0) alreadyDone.push(t);
|
|
1411
|
+
else fresh.push(t);
|
|
1412
|
+
}
|
|
1413
|
+
return [...fresh, ...alreadyDone];
|
|
1414
|
+
}
|
|
1415
|
+
function copyToClipboardOSC52(value) {
|
|
1416
|
+
const payload = Buffer.from(value, "utf8").toString("base64");
|
|
1417
|
+
process.stdout.write(`\x1B]52;c;${payload}\x07`);
|
|
1418
|
+
}
|
|
816
1419
|
function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName }) {
|
|
817
1420
|
const { exit } = useApp();
|
|
818
|
-
const [activeCeremonyId, setActiveCeremonyId] =
|
|
819
|
-
const [displayName2, setDisplayName] =
|
|
820
|
-
const [nameSet, setNameSet] =
|
|
821
|
-
const [screen,
|
|
1421
|
+
const [activeCeremonyId, setActiveCeremonyId] = useState6(initialCeremonyId);
|
|
1422
|
+
const [displayName2, setDisplayName] = useState6(initialDisplayName ?? "anonymous");
|
|
1423
|
+
const [nameSet, setNameSet] = useState6(initialDisplayName !== void 0);
|
|
1424
|
+
const [screen, setScreenRaw] = useState6(
|
|
822
1425
|
initialDisplayName === void 0 ? { name: "name-input", value: "" } : initialCeremonyId ? { name: "loading" } : { name: "ceremony-picker", ceremonies: [], loading: true }
|
|
823
1426
|
);
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1427
|
+
const setScreen = (next) => {
|
|
1428
|
+
setScreenRaw((prev) => {
|
|
1429
|
+
const resolved = typeof next === "function" ? next(prev) : next;
|
|
1430
|
+
if (prev.name !== resolved.name) {
|
|
1431
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
1432
|
+
}
|
|
1433
|
+
return resolved;
|
|
1434
|
+
});
|
|
1435
|
+
};
|
|
1436
|
+
const [ceremony, setCeremony] = useState6(null);
|
|
1437
|
+
const [session, setSession] = useState6(null);
|
|
1438
|
+
const [contributed, setContributed] = useState6({});
|
|
1439
|
+
const [selectedIdx, setSelectedIdx] = useState6(0);
|
|
1440
|
+
const [tab, setTab] = useState6(0);
|
|
1441
|
+
const [contribCursor, setContribCursor] = useState6(0);
|
|
1442
|
+
const [copyToast, setCopyToast] = useState6(null);
|
|
1443
|
+
const [showInfo, setShowInfo] = useState6(false);
|
|
1444
|
+
const [entropyMode, setEntropyMode] = useState6("mouse");
|
|
1445
|
+
useEffect6(() => {
|
|
831
1446
|
if (!nameSet) return;
|
|
832
1447
|
if (!initialCeremonyId) {
|
|
833
1448
|
loadCeremonies();
|
|
@@ -875,7 +1490,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
|
|
|
875
1490
|
return;
|
|
876
1491
|
}
|
|
877
1492
|
setSelectedIdx(0);
|
|
878
|
-
setScreen({ name: "
|
|
1493
|
+
setScreen({ name: "mode-select", tracks });
|
|
879
1494
|
} catch (e) {
|
|
880
1495
|
if (e.code === "INVALID_SESSION" || e.status === 401) {
|
|
881
1496
|
try {
|
|
@@ -892,7 +1507,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
|
|
|
892
1507
|
return;
|
|
893
1508
|
}
|
|
894
1509
|
setSelectedIdx(0);
|
|
895
|
-
setScreen({ name: "
|
|
1510
|
+
setScreen({ name: "mode-select", tracks });
|
|
896
1511
|
} catch (e2) {
|
|
897
1512
|
setScreen({
|
|
898
1513
|
name: "error",
|
|
@@ -950,9 +1565,25 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
|
|
|
950
1565
|
setScreen({ name: "ceremony-picker", ceremonies: [], loading: true });
|
|
951
1566
|
loadCeremonies();
|
|
952
1567
|
}
|
|
953
|
-
|
|
954
|
-
if (
|
|
955
|
-
|
|
1568
|
+
useEffect6(() => {
|
|
1569
|
+
if (session) {
|
|
1570
|
+
setSessionCleanup(clearSession);
|
|
1571
|
+
} else {
|
|
1572
|
+
clearSessionCleanup();
|
|
1573
|
+
}
|
|
1574
|
+
}, [session]);
|
|
1575
|
+
useEffect6(() => {
|
|
1576
|
+
let trackId = null;
|
|
1577
|
+
if (screen.name === "queue" || screen.name === "entropy" || screen.name === "contribute") {
|
|
1578
|
+
trackId = screen.trackId;
|
|
1579
|
+
} else if (screen.name === "auto-running") {
|
|
1580
|
+
if (screen.subPhase.kind === "waiting" || screen.subPhase.kind === "contributing" || screen.subPhase.kind === "releasing") {
|
|
1581
|
+
trackId = screen.subPhase.trackId;
|
|
1582
|
+
} else if (screen.current) {
|
|
1583
|
+
trackId = screen.current.id;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
if (trackId && session) {
|
|
956
1587
|
setQueueCleanup(() => {
|
|
957
1588
|
api.leaveQueue(activeCeremonyId, trackId, session.session_token).catch(() => {
|
|
958
1589
|
});
|
|
@@ -960,8 +1591,69 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
|
|
|
960
1591
|
} else {
|
|
961
1592
|
clearQueueCleanup();
|
|
962
1593
|
}
|
|
963
|
-
}, [screen.name]);
|
|
964
|
-
|
|
1594
|
+
}, [screen.name, screen.name === "auto-running" ? screen.subPhase.kind : null]);
|
|
1595
|
+
useEffect6(() => {
|
|
1596
|
+
if (screen.name !== "auto-running") return;
|
|
1597
|
+
if (screen.current !== null) return;
|
|
1598
|
+
if (screen.subPhase.kind !== "queueing") return;
|
|
1599
|
+
if (!session) return;
|
|
1600
|
+
const currentScreen = screen;
|
|
1601
|
+
(async () => {
|
|
1602
|
+
let next = null;
|
|
1603
|
+
let nextRemaining = currentScreen.remaining;
|
|
1604
|
+
let nextRetryQueue = currentScreen.retryQueue;
|
|
1605
|
+
let nextRetryingPhase = currentScreen.retryingPhase;
|
|
1606
|
+
if (currentScreen.remaining.length > 0) {
|
|
1607
|
+
next = currentScreen.remaining[0];
|
|
1608
|
+
nextRemaining = currentScreen.remaining.slice(1);
|
|
1609
|
+
} else if (currentScreen.retryQueue.length > 0 && !currentScreen.retryingPhase) {
|
|
1610
|
+
nextRemaining = currentScreen.retryQueue.slice(1);
|
|
1611
|
+
next = currentScreen.retryQueue[0];
|
|
1612
|
+
nextRetryQueue = [];
|
|
1613
|
+
nextRetryingPhase = true;
|
|
1614
|
+
}
|
|
1615
|
+
if (!next) {
|
|
1616
|
+
setScreen({ name: "auto-summary", outcomes: currentScreen.done });
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
try {
|
|
1620
|
+
await api.joinQueue(activeCeremonyId, next.id, session.session_token);
|
|
1621
|
+
} catch (e) {
|
|
1622
|
+
const reason = e?.message ?? String(e);
|
|
1623
|
+
setScreen({
|
|
1624
|
+
...currentScreen,
|
|
1625
|
+
remaining: nextRemaining,
|
|
1626
|
+
retryQueue: nextRetryQueue,
|
|
1627
|
+
retryingPhase: nextRetryingPhase,
|
|
1628
|
+
current: null,
|
|
1629
|
+
subPhase: { kind: "queueing" },
|
|
1630
|
+
done: [
|
|
1631
|
+
...currentScreen.done,
|
|
1632
|
+
{
|
|
1633
|
+
trackId: next.id,
|
|
1634
|
+
circuitName: next.circuit_name,
|
|
1635
|
+
result: "failed",
|
|
1636
|
+
reason: `joinQueue: ${reason}`
|
|
1637
|
+
}
|
|
1638
|
+
]
|
|
1639
|
+
});
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
setScreen({
|
|
1643
|
+
...currentScreen,
|
|
1644
|
+
remaining: nextRemaining,
|
|
1645
|
+
retryQueue: nextRetryQueue,
|
|
1646
|
+
retryingPhase: nextRetryingPhase,
|
|
1647
|
+
current: next,
|
|
1648
|
+
subPhase: { kind: "waiting", trackId: next.id, circuitName: next.circuit_name }
|
|
1649
|
+
});
|
|
1650
|
+
})();
|
|
1651
|
+
}, [
|
|
1652
|
+
screen.name,
|
|
1653
|
+
screen.name === "auto-running" ? screen.subPhase.kind : null,
|
|
1654
|
+
screen.name === "auto-running" ? screen.current?.id ?? null : null
|
|
1655
|
+
]);
|
|
1656
|
+
useInput3((input, key) => {
|
|
965
1657
|
const q = input.toLowerCase();
|
|
966
1658
|
if (showInfo) {
|
|
967
1659
|
if (key.escape || input === "?" || q === "b") {
|
|
@@ -974,7 +1666,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
|
|
|
974
1666
|
}
|
|
975
1667
|
return;
|
|
976
1668
|
}
|
|
977
|
-
if (input === "?" && screen.name !== "entropy") {
|
|
1669
|
+
if (input === "?" && screen.name !== "entropy" && screen.name !== "auto-entropy") {
|
|
978
1670
|
setShowInfo(true);
|
|
979
1671
|
return;
|
|
980
1672
|
}
|
|
@@ -1001,23 +1693,84 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
|
|
|
1001
1693
|
}
|
|
1002
1694
|
return;
|
|
1003
1695
|
}
|
|
1004
|
-
if (q === "q" && screen.name !== "entropy") {
|
|
1005
|
-
|
|
1696
|
+
if (q === "q" && screen.name !== "entropy" && screen.name !== "auto-entropy") {
|
|
1697
|
+
let trackId = null;
|
|
1698
|
+
if (screen.name === "queue" || screen.name === "contribute") {
|
|
1699
|
+
trackId = screen.trackId;
|
|
1700
|
+
} else if (screen.name === "auto-running") {
|
|
1701
|
+
if (screen.subPhase.kind === "waiting" || screen.subPhase.kind === "contributing" || screen.subPhase.kind === "releasing") {
|
|
1702
|
+
trackId = screen.subPhase.trackId;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
if (trackId && session) {
|
|
1006
1706
|
clearQueueCleanup();
|
|
1007
|
-
|
|
1707
|
+
clearSessionCleanup();
|
|
1708
|
+
api.leaveQueue(activeCeremonyId, trackId, session.session_token).catch(() => {
|
|
1709
|
+
});
|
|
1710
|
+
clearSession().catch(() => {
|
|
1008
1711
|
});
|
|
1009
1712
|
setTimeout(() => exit(), 500);
|
|
1713
|
+
} else if (session) {
|
|
1714
|
+
clearSessionCleanup();
|
|
1715
|
+
clearSession().catch(() => {
|
|
1716
|
+
});
|
|
1717
|
+
setTimeout(() => exit(), 100);
|
|
1010
1718
|
} else {
|
|
1011
1719
|
exit();
|
|
1012
1720
|
}
|
|
1013
1721
|
return;
|
|
1014
1722
|
}
|
|
1015
1723
|
if (key.backspace || key.delete) {
|
|
1016
|
-
if (!initialCeremonyId && (screen.name === "tracks" || screen.name === "error")) {
|
|
1724
|
+
if (!initialCeremonyId && (screen.name === "tracks" || screen.name === "error" || screen.name === "mode-select" || screen.name === "auto-summary")) {
|
|
1017
1725
|
goCeremonyPicker();
|
|
1018
1726
|
return;
|
|
1019
1727
|
}
|
|
1020
|
-
if (screen.name
|
|
1728
|
+
if (screen.name === "auto-running" && (screen.subPhase.kind === "waiting" || screen.subPhase.kind === "contributing") && screen.current && session) {
|
|
1729
|
+
const skipped = screen.current;
|
|
1730
|
+
const skippedTrackId = screen.subPhase.trackId;
|
|
1731
|
+
const skippedCircuitName = screen.subPhase.circuitName;
|
|
1732
|
+
const sessionToken = session.session_token;
|
|
1733
|
+
setScreen({
|
|
1734
|
+
...screen,
|
|
1735
|
+
subPhase: {
|
|
1736
|
+
kind: "releasing",
|
|
1737
|
+
trackId: skippedTrackId,
|
|
1738
|
+
circuitName: skippedCircuitName
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
api.leaveQueue(activeCeremonyId, skippedTrackId, sessionToken).catch((err) => {
|
|
1742
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1743
|
+
process.stderr.write(
|
|
1744
|
+
`[ceremony-tui] leaveQueue(${skippedTrackId}) failed during skip: ${msg}
|
|
1745
|
+
`
|
|
1746
|
+
);
|
|
1747
|
+
}).finally(() => {
|
|
1748
|
+
setScreen((prev) => {
|
|
1749
|
+
if (prev.name !== "auto-running" || prev.subPhase.kind !== "releasing" || prev.subPhase.trackId !== skippedTrackId) {
|
|
1750
|
+
return prev;
|
|
1751
|
+
}
|
|
1752
|
+
return {
|
|
1753
|
+
...prev,
|
|
1754
|
+
current: null,
|
|
1755
|
+
subPhase: { kind: "queueing" },
|
|
1756
|
+
done: [
|
|
1757
|
+
...prev.done,
|
|
1758
|
+
{
|
|
1759
|
+
trackId: skipped.id,
|
|
1760
|
+
circuitName: skipped.circuit_name,
|
|
1761
|
+
result: "failed",
|
|
1762
|
+
reason: "user skipped"
|
|
1763
|
+
}
|
|
1764
|
+
]
|
|
1765
|
+
// Don't queue skipped circuits for retry — the user
|
|
1766
|
+
// actively chose to skip, so a silent retry would
|
|
1767
|
+
// defeat that intent.
|
|
1768
|
+
};
|
|
1769
|
+
});
|
|
1770
|
+
});
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
if (screen.name !== "tracks" && screen.name !== "loading" && screen.name !== "joining" && screen.name !== "entropy" && screen.name !== "auto-entropy" && screen.name !== "ceremony-picker") {
|
|
1021
1774
|
goHome();
|
|
1022
1775
|
}
|
|
1023
1776
|
return;
|
|
@@ -1042,10 +1795,27 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
|
|
|
1042
1795
|
}
|
|
1043
1796
|
return;
|
|
1044
1797
|
}
|
|
1798
|
+
if (screen.name === "mode-select") {
|
|
1799
|
+
if (q === "k") {
|
|
1800
|
+
setEntropyMode((m) => m === "mouse" ? "keyboard" : "mouse");
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
if (key.return) {
|
|
1804
|
+
setScreen({ name: "auto-entropy", tracks: screen.tracks });
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (q === "n" || q === "m") {
|
|
1808
|
+
setSelectedIdx(0);
|
|
1809
|
+
setScreen({ name: "tracks", tracks: screen.tracks });
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1045
1814
|
if (screen.name === "tracks") {
|
|
1046
1815
|
const { tracks } = screen;
|
|
1047
1816
|
if (key.tab) {
|
|
1048
1817
|
setTab((t) => (t + 1) % 2);
|
|
1818
|
+
setContribCursor(0);
|
|
1049
1819
|
return;
|
|
1050
1820
|
}
|
|
1051
1821
|
if (tab === 0) {
|
|
@@ -1059,7 +1829,33 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
|
|
|
1059
1829
|
}
|
|
1060
1830
|
if (key.return) {
|
|
1061
1831
|
const t = tracks[selectedIdx];
|
|
1062
|
-
if (t && t.status === "open"
|
|
1832
|
+
if (t && t.status === "open") joinTrack(t);
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
} else if (tab === 1) {
|
|
1836
|
+
const myContribs = Object.values(contributed).flat().filter((c) => c.ceremonyId === activeCeremonyId).sort((a, b) => {
|
|
1837
|
+
const ta = a.verifiedAt ? Date.parse(a.verifiedAt) : 0;
|
|
1838
|
+
const tb = b.verifiedAt ? Date.parse(b.verifiedAt) : 0;
|
|
1839
|
+
return tb - ta;
|
|
1840
|
+
});
|
|
1841
|
+
if (key.upArrow) {
|
|
1842
|
+
setContribCursor((i) => Math.max(0, i - 1));
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
if (key.downArrow) {
|
|
1846
|
+
setContribCursor((i) => Math.min(myContribs.length - 1, i + 1));
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
if (q === "c") {
|
|
1850
|
+
const target = myContribs[contribCursor];
|
|
1851
|
+
if (target && target.contributionHash) {
|
|
1852
|
+
copyToClipboardOSC52(target.contributionHash);
|
|
1853
|
+
setCopyToast(`\u2713 Hash copied (round #${target.sequenceNumber})`);
|
|
1854
|
+
setTimeout(() => setCopyToast(null), 2e3);
|
|
1855
|
+
} else if (target) {
|
|
1856
|
+
setCopyToast("Nothing to copy \u2014 hash is still pending");
|
|
1857
|
+
setTimeout(() => setCopyToast(null), 2e3);
|
|
1858
|
+
}
|
|
1063
1859
|
return;
|
|
1064
1860
|
}
|
|
1065
1861
|
}
|
|
@@ -1073,52 +1869,55 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
|
|
|
1073
1869
|
}
|
|
1074
1870
|
});
|
|
1075
1871
|
if (showInfo) {
|
|
1076
|
-
return /* @__PURE__ */
|
|
1872
|
+
return /* @__PURE__ */ jsx8(InfoModal, {});
|
|
1077
1873
|
}
|
|
1078
1874
|
if (screen.name === "name-input") {
|
|
1079
1875
|
const { value } = screen;
|
|
1080
1876
|
const canSubmit = value.trim().length > 0;
|
|
1081
|
-
return /* @__PURE__ */
|
|
1082
|
-
/* @__PURE__ */
|
|
1083
|
-
/* @__PURE__ */
|
|
1084
|
-
/* @__PURE__ */
|
|
1085
|
-
/* @__PURE__ */
|
|
1086
|
-
/* @__PURE__ */
|
|
1087
|
-
/* @__PURE__ */
|
|
1088
|
-
/* @__PURE__ */
|
|
1089
|
-
/* @__PURE__ */
|
|
1877
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1878
|
+
/* @__PURE__ */ jsx8(Header, { ceremony: null }),
|
|
1879
|
+
/* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, gap: 1, children: [
|
|
1880
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, children: "Who are you contributing as?" }),
|
|
1881
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Your display name appears next to your contributions in the public transcript. Pick anything \u2014 your real name, a handle, or hit Tab for a random anonymous name." }),
|
|
1882
|
+
/* @__PURE__ */ jsxs8(Box8, { children: [
|
|
1883
|
+
/* @__PURE__ */ jsx8(Text8, { color: "cyan", children: " > " }),
|
|
1884
|
+
/* @__PURE__ */ jsx8(Text8, { children: value }),
|
|
1885
|
+
/* @__PURE__ */ jsx8(Text8, { color: "cyan", inverse: true, children: " " })
|
|
1090
1886
|
] }),
|
|
1091
|
-
/* @__PURE__ */
|
|
1887
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: canSubmit ? "Enter to continue \xB7 Tab rerolls random name \xB7 \u232B backspace \xB7 Esc to quit" : "Type a name, or press Tab to suggest a random anonymous one \xB7 Esc to quit" })
|
|
1092
1888
|
] })
|
|
1093
1889
|
] });
|
|
1094
1890
|
}
|
|
1095
1891
|
if (screen.name === "ceremony-picker") {
|
|
1096
1892
|
const { ceremonies, loading } = screen;
|
|
1097
|
-
return /* @__PURE__ */
|
|
1098
|
-
/* @__PURE__ */
|
|
1099
|
-
loading ? /* @__PURE__ */
|
|
1100
|
-
/* @__PURE__ */
|
|
1101
|
-
/* @__PURE__ */
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
/* @__PURE__ */
|
|
1105
|
-
/* @__PURE__ */
|
|
1893
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1894
|
+
/* @__PURE__ */ jsx8(Header, { ceremony: null }),
|
|
1895
|
+
loading ? /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1896
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Loading ceremonies..." }),
|
|
1897
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Q / Ctrl+C / Ctrl+Z to quit" }) })
|
|
1898
|
+
] }) : ceremonies.length === 0 ? /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
|
|
1899
|
+
/* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "No ceremonies found." }),
|
|
1900
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "The server may not have any active ceremonies yet." }),
|
|
1901
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Q to quit" })
|
|
1902
|
+
] }) : /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1903
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, children: "Select a ceremony:" }),
|
|
1904
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " " + "\u2500".repeat(60) }),
|
|
1106
1905
|
ceremonies.map((c, i) => {
|
|
1107
1906
|
const isSelected = i === selectedIdx;
|
|
1108
1907
|
const isOpen = c.status === "open";
|
|
1109
1908
|
const statusColor = c.status === "open" ? "green" : c.status === "completed" ? "cyan" : "yellow";
|
|
1110
1909
|
const statusLabel = ceremonyStatusLabel(c.status);
|
|
1111
|
-
return /* @__PURE__ */
|
|
1112
|
-
/* @__PURE__ */
|
|
1910
|
+
return /* @__PURE__ */ jsxs8(Box8, { gap: 2, children: [
|
|
1911
|
+
/* @__PURE__ */ jsxs8(Text8, { color: isSelected ? "cyan" : isOpen ? void 0 : "gray", children: [
|
|
1113
1912
|
isSelected ? "\u25B6 " : " ",
|
|
1114
1913
|
c.name.padEnd(30)
|
|
1115
1914
|
] }),
|
|
1116
|
-
/* @__PURE__ */
|
|
1915
|
+
/* @__PURE__ */ jsxs8(Text8, { color: statusColor, children: [
|
|
1117
1916
|
"[",
|
|
1118
1917
|
statusLabel,
|
|
1119
1918
|
"]"
|
|
1120
1919
|
] }),
|
|
1121
|
-
/* @__PURE__ */
|
|
1920
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
1122
1921
|
c.track_count,
|
|
1123
1922
|
" track",
|
|
1124
1923
|
c.track_count !== 1 ? "s" : "",
|
|
@@ -1129,164 +1928,496 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
|
|
|
1129
1928
|
] })
|
|
1130
1929
|
] }, c.id);
|
|
1131
1930
|
}),
|
|
1132
|
-
/* @__PURE__ */
|
|
1931
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " " + "\u2500".repeat(60) }),
|
|
1133
1932
|
(() => {
|
|
1134
1933
|
const c = ceremonies[selectedIdx];
|
|
1135
1934
|
if (!c) return null;
|
|
1935
|
+
const navHint = /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2191/\u2193 select \xB7 Enter join \xB7 Q / Ctrl+C / Ctrl+Z to quit" });
|
|
1136
1936
|
if (c.status === "initialized") {
|
|
1137
|
-
return /* @__PURE__ */
|
|
1138
|
-
"
|
|
1139
|
-
|
|
1937
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1938
|
+
/* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
|
|
1939
|
+
" ",
|
|
1940
|
+
"This ceremony is not yet open for contributions. The Umbra team is still preparing the circuits \u2014 please check back shortly."
|
|
1941
|
+
] }),
|
|
1942
|
+
navHint
|
|
1140
1943
|
] });
|
|
1141
1944
|
}
|
|
1142
1945
|
if (c.status === "finalizing") {
|
|
1143
|
-
return /* @__PURE__ */
|
|
1144
|
-
"
|
|
1145
|
-
|
|
1946
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1947
|
+
/* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
|
|
1948
|
+
" ",
|
|
1949
|
+
"Contributions are closed. The ceremony is computing the final verification key \u2014 no further contributions can be added."
|
|
1950
|
+
] }),
|
|
1951
|
+
navHint
|
|
1146
1952
|
] });
|
|
1147
1953
|
}
|
|
1148
1954
|
if (c.status === "completed") {
|
|
1149
|
-
return /* @__PURE__ */
|
|
1150
|
-
"
|
|
1151
|
-
|
|
1955
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1956
|
+
/* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
|
|
1957
|
+
" ",
|
|
1958
|
+
"Ceremony complete. The verification keys are finalised \u2014 thank you to everyone who contributed."
|
|
1959
|
+
] }),
|
|
1960
|
+
navHint
|
|
1152
1961
|
] });
|
|
1153
1962
|
}
|
|
1154
|
-
return
|
|
1963
|
+
return navHint;
|
|
1155
1964
|
})()
|
|
1156
1965
|
] })
|
|
1157
1966
|
] });
|
|
1158
1967
|
}
|
|
1159
1968
|
if (screen.name === "loading") {
|
|
1160
|
-
return /* @__PURE__ */
|
|
1161
|
-
/* @__PURE__ */
|
|
1162
|
-
/* @__PURE__ */
|
|
1969
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1970
|
+
/* @__PURE__ */ jsx8(Header, { ceremony }),
|
|
1971
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Loading tracks..." }),
|
|
1972
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Q / Ctrl+C / Ctrl+Z to quit" }) })
|
|
1973
|
+
] });
|
|
1974
|
+
}
|
|
1975
|
+
if (screen.name === "mode-select") {
|
|
1976
|
+
const { tracks } = screen;
|
|
1977
|
+
const openCount = tracks.filter((t) => t.status === "open").length;
|
|
1978
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1979
|
+
/* @__PURE__ */ jsx8(Header, { ceremony }),
|
|
1980
|
+
/* @__PURE__ */ jsxs8(
|
|
1981
|
+
Box8,
|
|
1982
|
+
{
|
|
1983
|
+
flexDirection: "column",
|
|
1984
|
+
borderStyle: "round",
|
|
1985
|
+
borderColor: "cyan",
|
|
1986
|
+
paddingX: 2,
|
|
1987
|
+
paddingY: 0,
|
|
1988
|
+
children: [
|
|
1989
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: "Automate?" }),
|
|
1990
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
1991
|
+
openCount,
|
|
1992
|
+
" circuit",
|
|
1993
|
+
openCount === 1 ? "" : "s",
|
|
1994
|
+
" open \xB7 auto runs the whole loop"
|
|
1995
|
+
] }),
|
|
1996
|
+
/* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
|
|
1997
|
+
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
1998
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "green", children: "Enter" }),
|
|
1999
|
+
" ",
|
|
2000
|
+
"auto \xB7 entropy once, every circuit in order"
|
|
2001
|
+
] }),
|
|
2002
|
+
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
2003
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "yellow", children: "N" }),
|
|
2004
|
+
" ",
|
|
2005
|
+
"manual \xB7 pick circuits one at a time"
|
|
2006
|
+
] }),
|
|
2007
|
+
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
2008
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "magenta", children: "K" }),
|
|
2009
|
+
" ",
|
|
2010
|
+
"input:",
|
|
2011
|
+
" ",
|
|
2012
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, color: entropyMode === "mouse" ? "cyan" : "yellow", children: entropyMode }),
|
|
2013
|
+
" ",
|
|
2014
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "(press K to toggle)" })
|
|
2015
|
+
] })
|
|
2016
|
+
] })
|
|
2017
|
+
]
|
|
2018
|
+
}
|
|
2019
|
+
),
|
|
2020
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u232B back \xB7 Q quit" }) })
|
|
2021
|
+
] });
|
|
2022
|
+
}
|
|
2023
|
+
if (screen.name === "auto-entropy") {
|
|
2024
|
+
const { tracks } = screen;
|
|
2025
|
+
const onComplete = (masterSeed) => {
|
|
2026
|
+
const ordered = orderTracksForAuto(tracks, contributed);
|
|
2027
|
+
setScreen({
|
|
2028
|
+
name: "auto-running",
|
|
2029
|
+
tracks,
|
|
2030
|
+
masterSeed,
|
|
2031
|
+
remaining: ordered,
|
|
2032
|
+
retryQueue: [],
|
|
2033
|
+
retryingPhase: false,
|
|
2034
|
+
done: [],
|
|
2035
|
+
current: null,
|
|
2036
|
+
subPhase: { kind: "queueing" }
|
|
2037
|
+
});
|
|
2038
|
+
};
|
|
2039
|
+
const onError = (e) => setScreen({ name: "error", message: e.message, recoverable: false });
|
|
2040
|
+
if (entropyMode === "mouse") {
|
|
2041
|
+
return /* @__PURE__ */ jsx8(
|
|
2042
|
+
MouseEntropyCollector,
|
|
2043
|
+
{
|
|
2044
|
+
title: `Auto-mode entropy \xB7 one drawing seeds every circuit (HKDF per circuit)`,
|
|
2045
|
+
onComplete,
|
|
2046
|
+
onError
|
|
2047
|
+
}
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2051
|
+
/* @__PURE__ */ jsx8(Header, { ceremony, subtitle: "Auto-mode entropy \xB7 seeds every circuit (HKDF)" }),
|
|
2052
|
+
/* @__PURE__ */ jsx8(EntropyCollector, { onComplete, onError })
|
|
2053
|
+
] });
|
|
2054
|
+
}
|
|
2055
|
+
if (screen.name === "auto-running") {
|
|
2056
|
+
const totalCount = screen.done.length + screen.remaining.length + screen.retryQueue.length + (screen.current ? 1 : 0);
|
|
2057
|
+
const verifiedCount = screen.done.filter((d) => d.result === "verified").length;
|
|
2058
|
+
const failedCount = screen.done.filter((d) => d.result === "failed").length;
|
|
2059
|
+
const currentLabel = screen.current ? `current: ${screen.current.circuit_name}` : "advancing to next circuit\u2026";
|
|
2060
|
+
const progressStrip = /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginBottom: 1, children: [
|
|
2061
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2500".repeat(60) }),
|
|
2062
|
+
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
2063
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: "Auto-mode" }),
|
|
2064
|
+
" \xB7 ",
|
|
2065
|
+
/* @__PURE__ */ jsx8(Text8, { children: verifiedCount }),
|
|
2066
|
+
"/",
|
|
2067
|
+
/* @__PURE__ */ jsx8(Text8, { children: totalCount }),
|
|
2068
|
+
" verified",
|
|
2069
|
+
failedCount > 0 && /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
2070
|
+
" \xB7 ",
|
|
2071
|
+
/* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
|
|
2072
|
+
failedCount,
|
|
2073
|
+
" failed"
|
|
2074
|
+
] }),
|
|
2075
|
+
screen.retryingPhase ? " (retrying)" : ""
|
|
2076
|
+
] }),
|
|
2077
|
+
screen.retryQueue.length > 0 && !screen.retryingPhase && /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
2078
|
+
" \xB7 ",
|
|
2079
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2080
|
+
screen.retryQueue.length,
|
|
2081
|
+
" queued for retry"
|
|
2082
|
+
] })
|
|
2083
|
+
] })
|
|
2084
|
+
] }),
|
|
2085
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: currentLabel }),
|
|
2086
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2500".repeat(60) })
|
|
2087
|
+
] });
|
|
2088
|
+
if (screen.subPhase.kind === "releasing") {
|
|
2089
|
+
const sub = screen.subPhase;
|
|
2090
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2091
|
+
/* @__PURE__ */ jsx8(
|
|
2092
|
+
Header,
|
|
2093
|
+
{
|
|
2094
|
+
ceremony,
|
|
2095
|
+
subtitle: `Auto-mode \xB7 releasing slot for ${sub.circuitName}`
|
|
2096
|
+
}
|
|
2097
|
+
),
|
|
2098
|
+
progressStrip,
|
|
2099
|
+
/* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
|
|
2100
|
+
"\u23F3 Releasing slot for ",
|
|
2101
|
+
sub.circuitName,
|
|
2102
|
+
"\u2026"
|
|
2103
|
+
] }),
|
|
2104
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Waiting for the server to confirm the slot is free before joining the next circuit's queue." }),
|
|
2105
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Q / Ctrl+C / Ctrl+Z abort run" })
|
|
2106
|
+
] });
|
|
2107
|
+
}
|
|
2108
|
+
if (screen.subPhase.kind === "queueing") {
|
|
2109
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2110
|
+
/* @__PURE__ */ jsx8(Header, { ceremony, subtitle: "Auto-mode \xB7 joining next queue" }),
|
|
2111
|
+
progressStrip,
|
|
2112
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Picking the next circuit\u2026" }),
|
|
2113
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Q / Ctrl+C / Ctrl+Z to abort (releases any active slot)" })
|
|
2114
|
+
] });
|
|
2115
|
+
}
|
|
2116
|
+
if (screen.subPhase.kind === "waiting") {
|
|
2117
|
+
const sub = screen.subPhase;
|
|
2118
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2119
|
+
/* @__PURE__ */ jsx8(
|
|
2120
|
+
Header,
|
|
2121
|
+
{
|
|
2122
|
+
ceremony,
|
|
2123
|
+
subtitle: `Auto-mode \xB7 waiting for slot \xB7 ${sub.circuitName}`
|
|
2124
|
+
}
|
|
2125
|
+
),
|
|
2126
|
+
progressStrip,
|
|
2127
|
+
/* @__PURE__ */ jsx8(
|
|
2128
|
+
QueueView,
|
|
2129
|
+
{
|
|
2130
|
+
ceremonyId: activeCeremonyId,
|
|
2131
|
+
trackId: sub.trackId,
|
|
2132
|
+
token: session.session_token,
|
|
2133
|
+
onReady: (status) => {
|
|
2134
|
+
const entropy = deriveCircuitEntropy(
|
|
2135
|
+
screen.masterSeed,
|
|
2136
|
+
activeCeremonyId,
|
|
2137
|
+
sub.circuitName
|
|
2138
|
+
);
|
|
2139
|
+
setScreen({
|
|
2140
|
+
...screen,
|
|
2141
|
+
subPhase: {
|
|
2142
|
+
kind: "contributing",
|
|
2143
|
+
trackId: sub.trackId,
|
|
2144
|
+
circuitName: sub.circuitName,
|
|
2145
|
+
slotStatus: status,
|
|
2146
|
+
entropy
|
|
2147
|
+
}
|
|
2148
|
+
});
|
|
2149
|
+
},
|
|
2150
|
+
onError: (e) => {
|
|
2151
|
+
const failedTrack = screen.current;
|
|
2152
|
+
const newDone = {
|
|
2153
|
+
trackId: failedTrack.id,
|
|
2154
|
+
circuitName: failedTrack.circuit_name,
|
|
2155
|
+
result: "failed",
|
|
2156
|
+
reason: e.message
|
|
2157
|
+
};
|
|
2158
|
+
setScreen({
|
|
2159
|
+
...screen,
|
|
2160
|
+
current: null,
|
|
2161
|
+
subPhase: { kind: "queueing" },
|
|
2162
|
+
done: [...screen.done, newDone],
|
|
2163
|
+
retryQueue: screen.retryingPhase ? screen.retryQueue : [...screen.retryQueue, failedTrack]
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
),
|
|
2168
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u232B Backspace skip this circuit \xB7 Q / Ctrl+C / Ctrl+Z abort run" })
|
|
2169
|
+
] });
|
|
2170
|
+
}
|
|
2171
|
+
if (screen.subPhase.kind === "contributing") {
|
|
2172
|
+
const sub = screen.subPhase;
|
|
2173
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2174
|
+
/* @__PURE__ */ jsx8(
|
|
2175
|
+
Header,
|
|
2176
|
+
{
|
|
2177
|
+
ceremony,
|
|
2178
|
+
subtitle: `Auto-mode \xB7 contributing \xB7 ${sub.circuitName}`
|
|
2179
|
+
}
|
|
2180
|
+
),
|
|
2181
|
+
progressStrip,
|
|
2182
|
+
/* @__PURE__ */ jsx8(
|
|
2183
|
+
ContributeFlow,
|
|
2184
|
+
{
|
|
2185
|
+
ceremonyId: activeCeremonyId,
|
|
2186
|
+
trackId: sub.trackId,
|
|
2187
|
+
token: session.session_token,
|
|
2188
|
+
slotStatus: sub.slotStatus,
|
|
2189
|
+
entropy: sub.entropy,
|
|
2190
|
+
displayName: displayName2,
|
|
2191
|
+
onComplete: async (contributionId, receipt) => {
|
|
2192
|
+
const local = {
|
|
2193
|
+
contributionId,
|
|
2194
|
+
sequenceNumber: receipt?.sequence_number ?? 0,
|
|
2195
|
+
contributionHash: receipt?.contribution_hash ?? "",
|
|
2196
|
+
circuitName: sub.circuitName,
|
|
2197
|
+
ceremonyId: activeCeremonyId,
|
|
2198
|
+
verifiedAt: receipt?.verified_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2199
|
+
};
|
|
2200
|
+
await recordContribution(sub.trackId, local);
|
|
2201
|
+
setContributed((prev) => {
|
|
2202
|
+
const existing = prev[sub.trackId] ?? [];
|
|
2203
|
+
if (existing.some((c) => c.contributionId === local.contributionId)) return prev;
|
|
2204
|
+
return { ...prev, [sub.trackId]: [...existing, local] };
|
|
2205
|
+
});
|
|
2206
|
+
const successTrack = screen.current;
|
|
2207
|
+
setScreen({
|
|
2208
|
+
...screen,
|
|
2209
|
+
current: null,
|
|
2210
|
+
subPhase: { kind: "queueing" },
|
|
2211
|
+
done: [
|
|
2212
|
+
...screen.done,
|
|
2213
|
+
{
|
|
2214
|
+
trackId: successTrack.id,
|
|
2215
|
+
circuitName: successTrack.circuit_name,
|
|
2216
|
+
result: "verified",
|
|
2217
|
+
hash: local.contributionHash
|
|
2218
|
+
}
|
|
2219
|
+
]
|
|
2220
|
+
});
|
|
2221
|
+
},
|
|
2222
|
+
onError: (e) => {
|
|
2223
|
+
const failedTrack = screen.current;
|
|
2224
|
+
setScreen({
|
|
2225
|
+
...screen,
|
|
2226
|
+
current: null,
|
|
2227
|
+
subPhase: { kind: "queueing" },
|
|
2228
|
+
done: [
|
|
2229
|
+
...screen.done,
|
|
2230
|
+
{
|
|
2231
|
+
trackId: failedTrack.id,
|
|
2232
|
+
circuitName: failedTrack.circuit_name,
|
|
2233
|
+
result: "failed",
|
|
2234
|
+
reason: e.message
|
|
2235
|
+
}
|
|
2236
|
+
],
|
|
2237
|
+
retryQueue: screen.retryingPhase ? screen.retryQueue : [...screen.retryQueue, failedTrack]
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
)
|
|
2242
|
+
] });
|
|
2243
|
+
}
|
|
2244
|
+
return null;
|
|
2245
|
+
}
|
|
2246
|
+
if (screen.name === "auto-summary") {
|
|
2247
|
+
const verified = screen.outcomes.filter((o) => o.result === "verified");
|
|
2248
|
+
const failed = screen.outcomes.filter((o) => o.result === "failed");
|
|
2249
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2250
|
+
/* @__PURE__ */ jsx8(Header, { ceremony, subtitle: "Auto-mode \xB7 complete" }),
|
|
2251
|
+
/* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
|
|
2252
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, color: failed.length === 0 ? "green" : "yellow", children: failed.length === 0 ? `\u2713 Auto-mode complete \u2014 ${verified.length} circuits verified` : `\u26A0 Auto-mode complete with ${failed.length} failure${failed.length === 1 ? "" : "s"}` }),
|
|
2253
|
+
verified.length > 0 && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2254
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Verified contributions:" }),
|
|
2255
|
+
verified.map((o, i) => /* @__PURE__ */ jsxs8(Box8, { paddingLeft: 2, children: [
|
|
2256
|
+
/* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u2713 " }),
|
|
2257
|
+
/* @__PURE__ */ jsx8(Text8, { children: elideMiddle(o.circuitName, 42).padEnd(44) }),
|
|
2258
|
+
/* @__PURE__ */ jsx8(Text8, { color: "cyan", children: o.hash ? o.hash.slice(0, 16) + "\u2026" : "" })
|
|
2259
|
+
] }, i))
|
|
2260
|
+
] }),
|
|
2261
|
+
failed.length > 0 && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2262
|
+
/* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "Failed (retry already attempted):" }),
|
|
2263
|
+
failed.map((o, i) => /* @__PURE__ */ jsxs8(Box8, { paddingLeft: 2, flexDirection: "column", children: [
|
|
2264
|
+
/* @__PURE__ */ jsxs8(Box8, { children: [
|
|
2265
|
+
/* @__PURE__ */ jsx8(Text8, { color: "red", children: "\u2717 " }),
|
|
2266
|
+
/* @__PURE__ */ jsx8(Text8, { children: elideMiddle(o.circuitName, 42) })
|
|
2267
|
+
] }),
|
|
2268
|
+
/* @__PURE__ */ jsx8(Box8, { paddingLeft: 4, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: o.reason ?? "(no reason recorded)" }) })
|
|
2269
|
+
] }, i)),
|
|
2270
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u232B Backspace / B \u2014 back to track list (you can retry failed circuits manually)" })
|
|
2271
|
+
] }),
|
|
2272
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Q / Ctrl+C / Ctrl+Z \u2014 quit" })
|
|
2273
|
+
] })
|
|
1163
2274
|
] });
|
|
1164
2275
|
}
|
|
1165
2276
|
if (screen.name === "error") {
|
|
1166
2277
|
const backHint = !initialCeremonyId ? "\u232B Backspace \u2014 back to ceremony list \xB7 Q to quit" : screen.recoverable ? "\u232B Backspace / B to go back \xB7 Q to quit" : "Q to quit";
|
|
1167
|
-
return /* @__PURE__ */
|
|
1168
|
-
/* @__PURE__ */
|
|
1169
|
-
/* @__PURE__ */
|
|
2278
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2279
|
+
/* @__PURE__ */ jsx8(Header, { ceremony }),
|
|
2280
|
+
/* @__PURE__ */ jsxs8(Text8, { color: "red", bold: true, children: [
|
|
1170
2281
|
"\u2717 ",
|
|
1171
2282
|
screen.message
|
|
1172
2283
|
] }),
|
|
1173
|
-
/* @__PURE__ */
|
|
2284
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: backHint })
|
|
1174
2285
|
] });
|
|
1175
2286
|
}
|
|
1176
2287
|
if (screen.name === "tracks") {
|
|
1177
2288
|
const { tracks } = screen;
|
|
1178
2289
|
const openTracks = tracks.filter((t) => t.status === "open");
|
|
1179
|
-
const myContributions = Object.values(contributed).filter(
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
/* @__PURE__ */
|
|
1186
|
-
/* @__PURE__ */
|
|
2290
|
+
const myContributions = Object.values(contributed).flat().filter((c) => c.ceremonyId === activeCeremonyId).sort((a, b) => {
|
|
2291
|
+
const ta = a.verifiedAt ? Date.parse(a.verifiedAt) : 0;
|
|
2292
|
+
const tb = b.verifiedAt ? Date.parse(b.verifiedAt) : 0;
|
|
2293
|
+
return tb - ta;
|
|
2294
|
+
});
|
|
2295
|
+
const TabBar = () => /* @__PURE__ */ jsxs8(Box8, { gap: 1, marginBottom: 1, children: [
|
|
2296
|
+
/* @__PURE__ */ jsx8(Text8, { bold: tab === 0, color: tab === 0 ? "cyan" : void 0, dimColor: tab !== 0, children: tab === 0 ? "[ Dashboard ]" : " Dashboard " }),
|
|
2297
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "|" }),
|
|
2298
|
+
/* @__PURE__ */ jsx8(Text8, { bold: tab === 1, color: tab === 1 ? "cyan" : void 0, dimColor: tab !== 1, children: tab === 1 ? `[ My Contributions (${myContributions.length}) ]` : ` My Contributions (${myContributions.length}) ` }),
|
|
2299
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " Tab to switch" })
|
|
1187
2300
|
] });
|
|
1188
|
-
return /* @__PURE__ */
|
|
1189
|
-
/* @__PURE__ */
|
|
1190
|
-
/* @__PURE__ */
|
|
2301
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2302
|
+
/* @__PURE__ */ jsx8(Header, { ceremony }),
|
|
2303
|
+
/* @__PURE__ */ jsx8(TabBar, {}),
|
|
1191
2304
|
tab === 0 ? (
|
|
1192
2305
|
// ── Dashboard tab ────────────────────────────────────────────────
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
2306
|
+
// CIRCUIT column is sized to fit the longest mainnet name
|
|
2307
|
+
// (`claim-deposit-into-confidential-amount-n4` = 41 chars). Names
|
|
2308
|
+
// longer than that are middle-elided so the disambiguating suffix
|
|
2309
|
+
// (-n1/-n2/-n4) stays visible — previously a hard slice(0,24)
|
|
2310
|
+
// showed every claim variant as `claim-deposit-into-confi..`.
|
|
2311
|
+
/* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2312
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2313
|
+
" CIRCUIT".padEnd(46),
|
|
2314
|
+
"TOTAL".padEnd(8),
|
|
1197
2315
|
"QUEUE".padEnd(8),
|
|
1198
|
-
"STATUS".padEnd(
|
|
2316
|
+
"STATUS".padEnd(14),
|
|
1199
2317
|
"MY CONTRIBUTIONS"
|
|
1200
2318
|
] }),
|
|
1201
|
-
/* @__PURE__ */
|
|
2319
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " " + "\u2500".repeat(90) }),
|
|
1202
2320
|
tracks.map((t, i) => {
|
|
1203
2321
|
const isSelected = i === selectedIdx;
|
|
1204
2322
|
const canContribute = t.status === "open";
|
|
1205
|
-
const
|
|
2323
|
+
const nameDisplay = elideMiddle(t.circuit_name, 42);
|
|
1206
2324
|
const statusColor = t.status === "open" ? "green" : t.status === "finalized" ? "cyan" : "yellow";
|
|
1207
|
-
const
|
|
1208
|
-
return /* @__PURE__ */
|
|
1209
|
-
/* @__PURE__ */
|
|
2325
|
+
const myRounds = roundCount(contributed, t.id);
|
|
2326
|
+
return /* @__PURE__ */ jsxs8(Box8, { children: [
|
|
2327
|
+
/* @__PURE__ */ jsxs8(Text8, { color: isSelected ? "cyan" : canContribute ? void 0 : "gray", children: [
|
|
1210
2328
|
isSelected ? "\u25B6 " : " ",
|
|
1211
|
-
|
|
1212
|
-
String(t.contribution_count).padEnd(
|
|
2329
|
+
nameDisplay.padEnd(44),
|
|
2330
|
+
String(t.contribution_count).padEnd(8),
|
|
1213
2331
|
String(t.queue_depth).padEnd(8)
|
|
1214
2332
|
] }),
|
|
1215
|
-
/* @__PURE__ */
|
|
1216
|
-
|
|
1217
|
-
"\u2713 contributed (
|
|
1218
|
-
|
|
1219
|
-
"
|
|
1220
|
-
|
|
1221
|
-
"\
|
|
1222
|
-
|
|
2333
|
+
/* @__PURE__ */ jsx8(Text8, { color: statusColor, children: trackStatusLabel(t.status).padEnd(14) }),
|
|
2334
|
+
myRounds > 0 ? canContribute && isSelected ? /* @__PURE__ */ jsxs8(Text8, { color: "green", children: [
|
|
2335
|
+
"\u2713 contributed (",
|
|
2336
|
+
myRounds,
|
|
2337
|
+
" round",
|
|
2338
|
+
myRounds === 1 ? "" : "s",
|
|
2339
|
+
") \u2014",
|
|
2340
|
+
" ",
|
|
2341
|
+
/* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "Enter to add more" })
|
|
2342
|
+
] }) : /* @__PURE__ */ jsxs8(Text8, { color: "green", children: [
|
|
2343
|
+
"\u2713 contributed (",
|
|
2344
|
+
myRounds,
|
|
2345
|
+
" round",
|
|
2346
|
+
myRounds === 1 ? "" : "s",
|
|
1223
2347
|
")"
|
|
1224
|
-
] }) : canContribute ? isSelected ? /* @__PURE__ */
|
|
2348
|
+
] }) : canContribute ? isSelected ? /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u2190 Enter to contribute" }) : /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "not contributed" }) : /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2014" })
|
|
1225
2349
|
] }, t.id);
|
|
1226
2350
|
}),
|
|
1227
|
-
/* @__PURE__ */
|
|
1228
|
-
openTracks.length === 0 ? /* @__PURE__ */
|
|
2351
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " " + "\u2500".repeat(70) }),
|
|
2352
|
+
openTracks.length === 0 ? /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "No circuits are open for contributions right now \u2014 the ceremony may be closing or already complete." }) : /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
1229
2353
|
"\u2191/\u2193 select \xB7 Enter contribute \xB7 R refresh \xB7 Q quit",
|
|
1230
2354
|
!initialCeremonyId ? " \xB7 \u232B back to ceremony list" : ""
|
|
1231
2355
|
] })
|
|
1232
2356
|
] })
|
|
1233
2357
|
) : (
|
|
1234
2358
|
// ── My Contributions tab ─────────────────────────────────────────
|
|
1235
|
-
/* @__PURE__ */
|
|
1236
|
-
/* @__PURE__ */
|
|
1237
|
-
/* @__PURE__ */
|
|
1238
|
-
] }) : /* @__PURE__ */
|
|
1239
|
-
/* @__PURE__ */
|
|
1240
|
-
" CIRCUIT".padEnd(
|
|
1241
|
-
"ROUND".padEnd(
|
|
1242
|
-
"
|
|
1243
|
-
"TIME"
|
|
2359
|
+
/* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: myContributions.length === 0 ? /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
|
|
2360
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "No contributions yet." }),
|
|
2361
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Switch to Dashboard tab and press Enter on a circuit to contribute." })
|
|
2362
|
+
] }) : /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2363
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2364
|
+
" CIRCUIT".padEnd(44),
|
|
2365
|
+
"ROUND".padEnd(7),
|
|
2366
|
+
"VERIFIED AT".padEnd(22)
|
|
1244
2367
|
] }),
|
|
1245
|
-
/* @__PURE__ */
|
|
1246
|
-
myContributions.map((c, i) =>
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
/* @__PURE__ */
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
2368
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " " + "\u2500".repeat(90) }),
|
|
2369
|
+
myContributions.map((c, i) => {
|
|
2370
|
+
const isSel = i === contribCursor;
|
|
2371
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2372
|
+
/* @__PURE__ */ jsxs8(Box8, { children: [
|
|
2373
|
+
/* @__PURE__ */ jsx8(Text8, { color: isSel ? "cyan" : "green", children: isSel ? "\u25B6 " : " " }),
|
|
2374
|
+
/* @__PURE__ */ jsx8(Text8, { bold: isSel, children: elideMiddle(c.circuitName, 40).padEnd(42) }),
|
|
2375
|
+
/* @__PURE__ */ jsx8(Text8, { color: "yellow", children: ("#" + c.sequenceNumber).padEnd(7) }),
|
|
2376
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: c.verifiedAt ? new Date(c.verifiedAt).toLocaleString() : "\u2014" })
|
|
2377
|
+
] }),
|
|
2378
|
+
/* @__PURE__ */ jsx8(Box8, { paddingLeft: 4, children: /* @__PURE__ */ jsxs8(Text8, { color: isSel ? "cyan" : "gray", dimColor: !isSel, children: [
|
|
2379
|
+
"hash:",
|
|
2380
|
+
" ",
|
|
2381
|
+
c.contributionHash ? c.contributionHash : "(pending \u2014 verify still in flight)"
|
|
2382
|
+
] }) })
|
|
2383
|
+
] }, i);
|
|
2384
|
+
}),
|
|
2385
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " " + "\u2500".repeat(90) }),
|
|
2386
|
+
copyToast ? /* @__PURE__ */ jsx8(Text8, { color: "green", children: copyToast }) : /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
1260
2387
|
"Total: ",
|
|
1261
2388
|
myContributions.length,
|
|
1262
2389
|
" contribution",
|
|
1263
2390
|
myContributions.length !== 1 ? "s" : "",
|
|
1264
2391
|
" \xB7 ",
|
|
1265
|
-
"Tab
|
|
2392
|
+
"\u2191/\u2193 select \xB7 C copy hash \xB7 Tab switch \xB7 Q quit"
|
|
1266
2393
|
] })
|
|
1267
2394
|
] }) })
|
|
1268
2395
|
)
|
|
1269
2396
|
] });
|
|
1270
2397
|
}
|
|
1271
2398
|
if (screen.name === "joining") {
|
|
1272
|
-
return /* @__PURE__ */
|
|
1273
|
-
/* @__PURE__ */
|
|
1274
|
-
/* @__PURE__ */
|
|
2399
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2400
|
+
/* @__PURE__ */ jsx8(Header, { ceremony }),
|
|
2401
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Joining queue..." }),
|
|
2402
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Q / Ctrl+C / Ctrl+Z to quit" }) })
|
|
1275
2403
|
] });
|
|
1276
2404
|
}
|
|
1277
2405
|
if (screen.name === "queue") {
|
|
1278
2406
|
const { trackId, circuitName } = screen;
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
" ",
|
|
1287
|
-
|
|
2407
|
+
const priorRounds = roundCount(contributed, trackId);
|
|
2408
|
+
const lastContrib = latestContribution(contributed, trackId);
|
|
2409
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2410
|
+
/* @__PURE__ */ jsx8(Header, { ceremony, subtitle: `Circuit: ${circuitName}` }),
|
|
2411
|
+
priorRounds > 0 && lastContrib && /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, paddingX: 1, children: /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
|
|
2412
|
+
"\u26A0 You've contributed to this circuit ",
|
|
2413
|
+
priorRounds,
|
|
2414
|
+
" time",
|
|
2415
|
+
priorRounds === 1 ? "" : "s",
|
|
2416
|
+
" (latest: round #",
|
|
2417
|
+
lastContrib.sequenceNumber,
|
|
2418
|
+
"). Each additional round adds more entropy and strengthens the ceremony \u2014 go ahead."
|
|
1288
2419
|
] }) }),
|
|
1289
|
-
/* @__PURE__ */
|
|
2420
|
+
/* @__PURE__ */ jsx8(
|
|
1290
2421
|
QueueView,
|
|
1291
2422
|
{
|
|
1292
2423
|
ceremonyId: activeCeremonyId,
|
|
@@ -1296,27 +2427,39 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
|
|
|
1296
2427
|
onError: (e) => setScreen({ name: "error", message: e.message, recoverable: true })
|
|
1297
2428
|
}
|
|
1298
2429
|
),
|
|
1299
|
-
/* @__PURE__ */
|
|
2430
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u232B Backspace to go back \xB7 Q to quit" })
|
|
1300
2431
|
] });
|
|
1301
2432
|
}
|
|
1302
2433
|
if (screen.name === "entropy") {
|
|
1303
2434
|
const { trackId, circuitName, slotStatus } = screen;
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
2435
|
+
const onComplete = (entropy) => setScreen({ name: "contribute", trackId, circuitName, slotStatus, entropy });
|
|
2436
|
+
const onError = (e) => setScreen({ name: "error", message: e.message, recoverable: false });
|
|
2437
|
+
if (entropyMode === "mouse") {
|
|
2438
|
+
return /* @__PURE__ */ jsx8(
|
|
2439
|
+
MouseEntropyCollector,
|
|
1308
2440
|
{
|
|
1309
|
-
|
|
1310
|
-
|
|
2441
|
+
title: `Your turn \xB7 ${circuitName} \u2014 draw entropy for this contribution`,
|
|
2442
|
+
onComplete,
|
|
2443
|
+
onError
|
|
1311
2444
|
}
|
|
1312
|
-
)
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2448
|
+
/* @__PURE__ */ jsx8(
|
|
2449
|
+
Header,
|
|
2450
|
+
{
|
|
2451
|
+
ceremony,
|
|
2452
|
+
subtitle: `Circuit: ${circuitName} \xB7 Type to collect entropy`
|
|
2453
|
+
}
|
|
2454
|
+
),
|
|
2455
|
+
/* @__PURE__ */ jsx8(EntropyCollector, { onComplete, onError })
|
|
1313
2456
|
] });
|
|
1314
2457
|
}
|
|
1315
2458
|
if (screen.name === "contribute") {
|
|
1316
2459
|
const { trackId, circuitName, slotStatus, entropy } = screen;
|
|
1317
|
-
return /* @__PURE__ */
|
|
1318
|
-
/* @__PURE__ */
|
|
1319
|
-
/* @__PURE__ */
|
|
2460
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2461
|
+
/* @__PURE__ */ jsx8(Header, { ceremony, subtitle: `Circuit: ${circuitName} \xB7 Contributing` }),
|
|
2462
|
+
/* @__PURE__ */ jsx8(
|
|
1320
2463
|
ContributeFlow,
|
|
1321
2464
|
{
|
|
1322
2465
|
ceremonyId: activeCeremonyId,
|
|
@@ -1335,7 +2478,13 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
|
|
|
1335
2478
|
verifiedAt: receipt?.verified_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1336
2479
|
};
|
|
1337
2480
|
await recordContribution(trackId, local);
|
|
1338
|
-
setContributed((prev) =>
|
|
2481
|
+
setContributed((prev) => {
|
|
2482
|
+
const existing = prev[trackId] ?? [];
|
|
2483
|
+
if (existing.some((c) => c.contributionId === local.contributionId)) {
|
|
2484
|
+
return prev;
|
|
2485
|
+
}
|
|
2486
|
+
return { ...prev, [trackId]: [...existing, local] };
|
|
2487
|
+
});
|
|
1339
2488
|
setScreen({ name: "done", contribution: local });
|
|
1340
2489
|
},
|
|
1341
2490
|
onError: (e) => setScreen({ name: "error", message: e.message, recoverable: false })
|
|
@@ -1345,41 +2494,47 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
|
|
|
1345
2494
|
}
|
|
1346
2495
|
if (screen.name === "done") {
|
|
1347
2496
|
const { contribution } = screen;
|
|
1348
|
-
return /* @__PURE__ */
|
|
1349
|
-
/* @__PURE__ */
|
|
1350
|
-
/* @__PURE__ */
|
|
1351
|
-
/* @__PURE__ */
|
|
2497
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
2498
|
+
/* @__PURE__ */ jsx8(Header, { ceremony }),
|
|
2499
|
+
/* @__PURE__ */ jsx8(Attestation, { contribution }),
|
|
2500
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u232B Backspace / B = contribute to another circuit \xB7 Q to quit" }) })
|
|
1352
2501
|
] });
|
|
1353
2502
|
}
|
|
1354
2503
|
return null;
|
|
1355
2504
|
}
|
|
1356
2505
|
|
|
1357
2506
|
// src/index.tsx
|
|
1358
|
-
import { jsx as
|
|
2507
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
1359
2508
|
var ceremonyId = process.env["CEREMONY_ID"] ?? "";
|
|
1360
2509
|
var displayName = process.env["CONTRIBUTOR_NAME"];
|
|
1361
2510
|
process.stdout.write("\x1B[?1049h\x1B[H");
|
|
1362
2511
|
function restoreScreen() {
|
|
1363
2512
|
process.stdout.write("\x1B[?1049l");
|
|
1364
2513
|
}
|
|
1365
|
-
|
|
2514
|
+
var exiting = false;
|
|
2515
|
+
async function gracefulExit(code = 0, deliberate = false) {
|
|
2516
|
+
if (exiting) {
|
|
2517
|
+
process.exit(code);
|
|
2518
|
+
}
|
|
2519
|
+
exiting = true;
|
|
1366
2520
|
restoreScreen();
|
|
1367
2521
|
await runQueueCleanup();
|
|
2522
|
+
if (deliberate) {
|
|
2523
|
+
await runSessionCleanup();
|
|
2524
|
+
}
|
|
1368
2525
|
process.exit(code);
|
|
1369
2526
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
process.
|
|
1377
|
-
|
|
1378
|
-
restoreScreen();
|
|
1379
|
-
process.exit(0);
|
|
2527
|
+
globalThis.__ceremonyGracefulExit = gracefulExit;
|
|
2528
|
+
var DELIBERATE_SIGNALS = /* @__PURE__ */ new Set(["SIGINT", "SIGTSTP"]);
|
|
2529
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGTSTP", "SIGHUP"]) {
|
|
2530
|
+
process.on(signal, () => {
|
|
2531
|
+
gracefulExit(0, DELIBERATE_SIGNALS.has(signal)).catch(() => {
|
|
2532
|
+
restoreScreen();
|
|
2533
|
+
process.exit(0);
|
|
2534
|
+
});
|
|
1380
2535
|
});
|
|
1381
|
-
}
|
|
1382
|
-
var { waitUntilExit } = render(/* @__PURE__ */
|
|
2536
|
+
}
|
|
2537
|
+
var { waitUntilExit } = render(/* @__PURE__ */ jsx9(App, { ceremonyId, displayName }), {
|
|
1383
2538
|
exitOnCtrlC: false
|
|
1384
2539
|
});
|
|
1385
2540
|
await waitUntilExit();
|