@umbra-privacy/ceremony 0.2.3 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +226 -104
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { render } from "ink";
5
5
 
6
6
  // src/components/App.tsx
7
- import { useEffect as useEffect4, useState as useState4 } from "react";
7
+ import { useEffect as useEffect5, useState as useState5 } from "react";
8
8
  import { Box as Box7, Text as Text7, useApp, useInput as useInput2 } from "ink";
9
9
 
10
10
  // src/cleanup.ts
@@ -90,26 +90,20 @@ var api = {
90
90
  },
91
91
  // Slot polling
92
92
  myTurn(ceremonyId2, trackId, token) {
93
- return request(
94
- `/api/ceremonies/${ceremonyId2}/tracks/${trackId}/my-turn`,
95
- { headers: bearer(token) }
96
- );
93
+ return request(`/api/ceremonies/${ceremonyId2}/tracks/${trackId}/my-turn`, {
94
+ headers: bearer(token)
95
+ });
97
96
  },
98
97
  // Contribution
99
98
  signalUploaded(ceremonyId2, trackId, contributionId, token) {
100
- return request(
101
- `/api/ceremonies/${ceremonyId2}/tracks/${trackId}/signal-uploaded`,
102
- {
103
- method: "POST",
104
- headers: bearer(token),
105
- body: JSON.stringify({ contribution_id: contributionId })
106
- }
107
- );
99
+ return request(`/api/ceremonies/${ceremonyId2}/tracks/${trackId}/signal-uploaded`, {
100
+ method: "POST",
101
+ headers: bearer(token),
102
+ body: JSON.stringify({ contribution_id: contributionId })
103
+ });
108
104
  },
109
105
  getReceipt(ceremonyId2, contributionId) {
110
- return request(
111
- `/api/ceremonies/${ceremonyId2}/contributions/${contributionId}/receipt`
112
- );
106
+ return request(`/api/ceremonies/${ceremonyId2}/contributions/${contributionId}/receipt`);
113
107
  },
114
108
  // Admin — signed-request flow. Caller supplies the 64-byte Solana keypair
115
109
  // bytes (32 secret + 32 public). The helper fetches a one-time challenge,
@@ -252,6 +246,38 @@ async function recordContribution(trackId, contribution) {
252
246
 
253
247
  // src/components/Header.tsx
254
248
  import { Box, Text } from "ink";
249
+
250
+ // src/labels.ts
251
+ function ceremonyStatusLabel(status) {
252
+ switch (status) {
253
+ case "open":
254
+ return "open";
255
+ case "initialized":
256
+ return "not yet open";
257
+ case "finalizing":
258
+ return "closing";
259
+ case "completed":
260
+ return "complete";
261
+ default:
262
+ return status;
263
+ }
264
+ }
265
+ function trackStatusLabel(status) {
266
+ switch (status) {
267
+ case "open":
268
+ return "open";
269
+ case "initializing":
270
+ return "preparing";
271
+ case "awaiting_beacon":
272
+ return "awaiting beacon";
273
+ case "finalized":
274
+ return "complete";
275
+ default:
276
+ return status;
277
+ }
278
+ }
279
+
280
+ // src/components/Header.tsx
255
281
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
256
282
  var STATUS_COLOR = {
257
283
  open: "green",
@@ -264,10 +290,10 @@ function Header({ ceremony, subtitle }) {
264
290
  /* @__PURE__ */ jsxs(Box, { children: [
265
291
  /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "\u25C6 Umbra Ceremony TUI" }),
266
292
  ceremony && /* @__PURE__ */ jsxs(Fragment, { children: [
267
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
293
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
268
294
  /* @__PURE__ */ jsx(Text, { bold: true, children: ceremony.name }),
269
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " [" }),
270
- /* @__PURE__ */ jsx(Text, { color: STATUS_COLOR[ceremony.status] ?? "white", children: ceremony.status }),
295
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " [" }),
296
+ /* @__PURE__ */ jsx(Text, { color: STATUS_COLOR[ceremony.status] ?? "white", children: ceremonyStatusLabel(ceremony.status) }),
271
297
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "]" })
272
298
  ] })
273
299
  ] }),
@@ -289,22 +315,61 @@ function Header({ ceremony, subtitle }) {
289
315
  }
290
316
 
291
317
  // src/components/QueueView.tsx
292
- import { useEffect, useRef, useState } from "react";
318
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
293
319
  import { Box as Box2, Text as Text2 } from "ink";
294
320
  import Spinner from "ink-spinner";
295
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
321
+
322
+ // src/hooks/useCyclingMessage.ts
323
+ import { useEffect, useState } from "react";
324
+ function useCyclingMessage(messages, intervalMs = 2500, active = true) {
325
+ const [idx, setIdx] = useState(0);
326
+ useEffect(() => {
327
+ if (!active || messages.length <= 1) return;
328
+ const t = setInterval(() => setIdx((i) => (i + 1) % messages.length), intervalMs);
329
+ return () => clearInterval(t);
330
+ }, [active, intervalMs, messages.length]);
331
+ return messages[idx] ?? messages[0] ?? "";
332
+ }
333
+ var COMPUTING_MESSAGES = [
334
+ "summoning fresh entropy",
335
+ "shuffling response bytes",
336
+ "tickling bn128 pairings",
337
+ "applying tau locally",
338
+ "folding contribution proof",
339
+ "blinding the trapdoor",
340
+ "sealing the bellman response"
341
+ ];
342
+ var VERIFYING_MESSAGES = [
343
+ "worker pulling your response from S3",
344
+ "importing bellman contribution into a new zkey",
345
+ "checking the contribution's pairing proof",
346
+ "writing the next zkey",
347
+ "uploading the new zkey",
348
+ "anchoring contribution hash in the audit chain",
349
+ "almost there \u2014 finalising your receipt"
350
+ ];
351
+ var EXPORTING_MESSAGES = [
352
+ "preparing your challenge file",
353
+ "extracting bellman params from the current zkey",
354
+ "wrapping the zkey for handoff",
355
+ "stamping a sha256 over the challenge bundle",
356
+ "presigning your download URL"
357
+ ];
358
+
359
+ // src/components/QueueView.tsx
360
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
296
361
  var POLL_FAST_MS = 5e3;
297
362
  var POLL_SLOW_MS = 15e3;
298
363
  function QueueView({ ceremonyId: ceremonyId2, trackId, token, onReady, onError }) {
299
- const [status, setStatus] = useState(null);
300
- const [pollErr, setPollErr] = useState(null);
301
- const [tick, setTick] = useState(0);
364
+ const [status, setStatus] = useState2(null);
365
+ const [pollErr, setPollErr] = useState2(null);
366
+ const [tick, setTick] = useState2(0);
302
367
  const timeoutRef = useRef(null);
303
- useEffect(() => {
368
+ useEffect2(() => {
304
369
  const id = setInterval(() => setTick((t) => (t + 1) % 4), 500);
305
370
  return () => clearInterval(id);
306
371
  }, []);
307
- useEffect(() => {
372
+ useEffect2(() => {
308
373
  let cancelled = false;
309
374
  async function poll() {
310
375
  try {
@@ -342,7 +407,10 @@ function QueueView({ ceremonyId: ceremonyId2, trackId, token, onReady, onError }
342
407
  ] });
343
408
  }
344
409
  const waitMins = Math.ceil((status.estimated_wait_secs ?? 0) / 60);
345
- const expiresAt = status.slot_expires_at ? new Date(status.slot_expires_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : null;
410
+ const expiresAt = status.slot_expires_at ? new Date(status.slot_expires_at).toLocaleTimeString([], {
411
+ hour: "2-digit",
412
+ minute: "2-digit"
413
+ }) : null;
346
414
  const fastPoll = status.queue_position <= 2;
347
415
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
348
416
  /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
@@ -366,17 +434,13 @@ function QueueView({ ceremonyId: ceremonyId2, trackId, token, onReady, onError }
366
434
  ] })
367
435
  ] })
368
436
  ] }),
369
- status.status === "exporting" || status.status === "your_turn" || status.status === "ready_to_download" ? /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: /* @__PURE__ */ jsx2(Text2, { color: "green", children: status.status === "exporting" ? "Preparing your challenge file \u2014 hang tight..." : "Challenge ready \u2014 loading contribution flow..." }) }) : status.active_since ? /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
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: [
370
438
  "Another contributor is active",
371
439
  expiresAt ? ` \xB7 slot expires at ${expiresAt}` : ""
372
440
  ] }) }) : status.queue_position > 1 ? (
373
441
  // Slot is idle but people are ahead — they joined and left without releasing.
374
442
  // timeout_watchdog will clear each stale slot after contribution_timeout_secs.
375
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
376
- "Slot is idle \u2014 waiting for positions ahead to respond or time out",
377
- " ",
378
- "(up to ~5 min each)"
379
- ] })
443
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Slot is idle \u2014 waiting for positions ahead to respond or time out (up to ~5 min each)" })
380
444
  ) : (
381
445
  // Position 1, slot idle — advance_queue should fire shortly.
382
446
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Slot is idle \u2014 your turn is being prepared..." })
@@ -394,9 +458,21 @@ function QueueView({ ceremonyId: ceremonyId2, trackId, token, onReady, onError }
394
458
  ] })
395
459
  ] });
396
460
  }
461
+ function ExportingMessage({ status }) {
462
+ const exportingMsg = useCyclingMessage(EXPORTING_MESSAGES, 2500, status === "exporting");
463
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: status === "exporting" ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
464
+ /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
465
+ /* @__PURE__ */ jsx2(Spinner, { type: "dots" }),
466
+ " ",
467
+ exportingMsg,
468
+ "\u2026"
469
+ ] }),
470
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Your slot is active. The worker is preparing the challenge file just for you." })
471
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: "green", children: "Challenge ready \u2014 loading contribution flow\u2026" }) });
472
+ }
397
473
 
398
474
  // src/components/EntropyCollector.tsx
399
- import { useRef as useRef2, useState as useState2 } from "react";
475
+ import { useRef as useRef2, useState as useState3 } from "react";
400
476
  import { Box as Box3, Text as Text3, useInput } from "ink";
401
477
 
402
478
  // src/entropy.ts
@@ -417,8 +493,8 @@ function buildEntropyFromKeystrokes(chars, timingsNs) {
417
493
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
418
494
  var TARGET = 20;
419
495
  function EntropyCollector({ onComplete, onError }) {
420
- const [count, setCount] = useState2(0);
421
- const [done, setDone] = useState2(false);
496
+ const [count, setCount] = useState3(0);
497
+ const [done, setDone] = useState3(false);
422
498
  const charsRef = useRef2([]);
423
499
  const timingsRef = useRef2([]);
424
500
  const lastRef = useRef2(process.hrtime.bigint());
@@ -460,7 +536,7 @@ function EntropyCollector({ onComplete, onError }) {
460
536
  /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Type anything to generate entropy:" }),
461
537
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Keystroke timing (nanosecond intervals) is the randomness source." }),
462
538
  /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
463
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " > " }),
539
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " > " }),
464
540
  /* @__PURE__ */ jsx3(Text3, { color: "green", children: stars }),
465
541
  /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "\u2588" })
466
542
  ] }),
@@ -475,7 +551,7 @@ function EntropyCollector({ onComplete, onError }) {
475
551
  /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
476
552
  "/",
477
553
  TARGET,
478
- " keystrokes (",
554
+ " keystrokes (",
479
555
  pct,
480
556
  "%)"
481
557
  ] })
@@ -486,7 +562,7 @@ function EntropyCollector({ onComplete, onError }) {
486
562
  }
487
563
 
488
564
  // src/components/ContributeFlow.tsx
489
- import { useEffect as useEffect3, useState as useState3 } from "react";
565
+ import { useEffect as useEffect4, useState as useState4 } from "react";
490
566
  import { Box as Box4, Text as Text4 } from "ink";
491
567
  import Spinner2 from "ink-spinner";
492
568
  import { tmpdir as tmpdir2 } from "os";
@@ -525,8 +601,8 @@ var STEP_INDEX = {
525
601
  };
526
602
  function ContributeFlow(props) {
527
603
  const { ceremonyId: ceremonyId2, trackId, token, slotStatus, entropy, displayName: displayName2 } = props;
528
- const [step, setStep] = useState3({ name: "downloading", bytesReceived: 0, total: null });
529
- useEffect3(() => {
604
+ const [step, setStep] = useState4({ name: "downloading", bytesReceived: 0, total: null });
605
+ useEffect4(() => {
530
606
  let cancelled = false;
531
607
  const challengePath = join4(tmpdir2(), `ceremony-challenge-${Date.now()}.mpcparams`);
532
608
  let responsePath = null;
@@ -572,9 +648,11 @@ function ContributeFlow(props) {
572
648
  await api.signalUploaded(ceremonyId2, trackId, slotStatus.contribution_id, token);
573
649
  if (cancelled) return;
574
650
  setStep({ name: "verifying", attempt: 1 });
651
+ const TOTAL_POLLS = 100;
652
+ const FAST_POLLS = 10;
575
653
  let receipt = null;
576
654
  let lastErr = null;
577
- for (let i = 0; i < 30; i++) {
655
+ for (let i = 0; i < TOTAL_POLLS; i++) {
578
656
  if (cancelled) return;
579
657
  setStep({ name: "verifying", attempt: i + 1 });
580
658
  try {
@@ -583,7 +661,8 @@ function ContributeFlow(props) {
583
661
  break;
584
662
  } catch (err) {
585
663
  lastErr = err instanceof Error ? err : new Error(String(err));
586
- await new Promise((r) => setTimeout(r, 3e3));
664
+ const delay = i < FAST_POLLS ? 3e3 : 1e4;
665
+ await new Promise((r) => setTimeout(r, delay));
587
666
  }
588
667
  }
589
668
  if (cancelled) return;
@@ -604,6 +683,8 @@ function ContributeFlow(props) {
604
683
  };
605
684
  }, []);
606
685
  const currentIdx = STEP_INDEX[step.name] ?? 0;
686
+ const computingMsg = useCyclingMessage(COMPUTING_MESSAGES, 2500, step.name === "computing");
687
+ const verifyingMsg = useCyclingMessage(VERIFYING_MESSAGES, 3e3, step.name === "verifying");
607
688
  return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", gap: 1, children: STEP_LABELS.map((label, i) => {
608
689
  const isDone = i < currentIdx;
609
690
  const isActive = i === currentIdx;
@@ -618,7 +699,8 @@ function ContributeFlow(props) {
618
699
  const filled = Math.round(pct / 5);
619
700
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(20 - filled);
620
701
  detail = /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
621
- " [",
702
+ " ",
703
+ "[",
622
704
  bar,
623
705
  "] ",
624
706
  pct,
@@ -626,10 +708,23 @@ function ContributeFlow(props) {
626
708
  ] });
627
709
  }
628
710
  if (isActive && step.name === "verifying") {
711
+ const elapsed = step.attempt <= 10 ? step.attempt * 3 : 10 * 3 + (step.attempt - 10) * 10;
712
+ const mins = Math.floor(elapsed / 60);
713
+ const secs = elapsed % 60;
714
+ const time = mins > 0 ? `${mins}m ${secs.toString().padStart(2, "0")}s` : `${secs}s`;
629
715
  detail = /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
630
- " worker verifying... (",
631
- step.attempt,
632
- "/30)"
716
+ " ",
717
+ verifyingMsg,
718
+ "\u2026 (",
719
+ time,
720
+ " elapsed)"
721
+ ] });
722
+ }
723
+ if (isActive && step.name === "computing") {
724
+ detail = /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
725
+ " ",
726
+ computingMsg,
727
+ "\u2026"
633
728
  ] });
634
729
  }
635
730
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
@@ -644,8 +739,7 @@ function ContributeFlow(props) {
644
739
  children: [
645
740
  label.charAt(0).toUpperCase() + label.slice(1),
646
741
  " ",
647
- isActive && label === "computing" && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "(entropy stays local)" }),
648
- isActive && label === "uploading" && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "(~544 bytes)" })
742
+ isActive && label === "computing" && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "(entropy stays local)" })
649
743
  ]
650
744
  }
651
745
  )
@@ -662,25 +756,25 @@ function Attestation({ contribution }) {
662
756
  const hashShort = contribution.contributionHash ? `${contribution.contributionHash.slice(0, 16)}...${contribution.contributionHash.slice(-8)}` : "(pending verification)";
663
757
  const verifiedAt = contribution.verifiedAt ? new Date(contribution.verifiedAt).toLocaleString() : null;
664
758
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
665
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "green", children: "\u2713 Contribution verified!" }),
759
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "green", children: "\u2713 Contribution verified!" }),
666
760
  /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 0, children: [
667
761
  /* @__PURE__ */ jsxs5(Box5, { gap: 2, children: [
668
762
  /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Circuit" }),
669
763
  /* @__PURE__ */ jsx5(Text5, { bold: true, children: contribution.circuitName })
670
764
  ] }),
671
765
  /* @__PURE__ */ jsxs5(Box5, { gap: 2, children: [
672
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Round " }),
766
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Round " }),
673
767
  /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
674
768
  "#",
675
769
  contribution.sequenceNumber
676
770
  ] })
677
771
  ] }),
678
772
  /* @__PURE__ */ jsxs5(Box5, { gap: 2, children: [
679
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Hash " }),
773
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Hash " }),
680
774
  /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: hashShort })
681
775
  ] }),
682
776
  verifiedAt && /* @__PURE__ */ jsxs5(Box5, { gap: 2, children: [
683
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Time " }),
777
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Time " }),
684
778
  /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: verifiedAt })
685
779
  ] })
686
780
  ] }),
@@ -721,19 +815,19 @@ var NAME_MAX_LEN = 100;
721
815
  var NAME_VALID_RE = /^[\p{L}\p{N} _.\-]*$/u;
722
816
  function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName }) {
723
817
  const { exit } = useApp();
724
- const [activeCeremonyId, setActiveCeremonyId] = useState4(initialCeremonyId);
725
- const [displayName2, setDisplayName] = useState4(initialDisplayName ?? "anonymous");
726
- const [nameSet, setNameSet] = useState4(initialDisplayName !== void 0);
727
- const [screen, setScreen] = useState4(
818
+ const [activeCeremonyId, setActiveCeremonyId] = useState5(initialCeremonyId);
819
+ const [displayName2, setDisplayName] = useState5(initialDisplayName ?? "anonymous");
820
+ const [nameSet, setNameSet] = useState5(initialDisplayName !== void 0);
821
+ const [screen, setScreen] = useState5(
728
822
  initialDisplayName === void 0 ? { name: "name-input", value: "" } : initialCeremonyId ? { name: "loading" } : { name: "ceremony-picker", ceremonies: [], loading: true }
729
823
  );
730
- const [ceremony, setCeremony] = useState4(null);
731
- const [session, setSession] = useState4(null);
732
- const [contributed, setContributed] = useState4({});
733
- const [selectedIdx, setSelectedIdx] = useState4(0);
734
- const [tab, setTab] = useState4(0);
735
- const [showInfo, setShowInfo] = useState4(false);
736
- useEffect4(() => {
824
+ const [ceremony, setCeremony] = useState5(null);
825
+ const [session, setSession] = useState5(null);
826
+ const [contributed, setContributed] = useState5({});
827
+ const [selectedIdx, setSelectedIdx] = useState5(0);
828
+ const [tab, setTab] = useState5(0);
829
+ const [showInfo, setShowInfo] = useState5(false);
830
+ useEffect5(() => {
737
831
  if (!nameSet) return;
738
832
  if (!initialCeremonyId) {
739
833
  loadCeremonies();
@@ -747,7 +841,11 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
747
841
  setScreen({ name: "ceremony-picker", ceremonies, loading: false });
748
842
  setSelectedIdx(0);
749
843
  } catch (e) {
750
- setScreen({ name: "error", message: e.message ?? "Failed to load ceremonies.", recoverable: false });
844
+ setScreen({
845
+ name: "error",
846
+ message: e.message ?? "Failed to load ceremonies.",
847
+ recoverable: false
848
+ });
751
849
  }
752
850
  }
753
851
  async function boot(cId) {
@@ -769,7 +867,11 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
769
867
  try {
770
868
  const { tracks } = await api.listTracks(cId, s.session_token);
771
869
  if (tracks.length === 0) {
772
- setScreen({ name: "error", message: "No tracks found in this ceremony.", recoverable: false });
870
+ setScreen({
871
+ name: "error",
872
+ message: "No tracks found in this ceremony.",
873
+ recoverable: false
874
+ });
773
875
  return;
774
876
  }
775
877
  setSelectedIdx(0);
@@ -782,17 +884,29 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
782
884
  setSession(fresh);
783
885
  const { tracks } = await api.listTracks(cId, fresh.session_token);
784
886
  if (tracks.length === 0) {
785
- setScreen({ name: "error", message: "No tracks found in this ceremony.", recoverable: false });
887
+ setScreen({
888
+ name: "error",
889
+ message: "No tracks found in this ceremony.",
890
+ recoverable: false
891
+ });
786
892
  return;
787
893
  }
788
894
  setSelectedIdx(0);
789
895
  setScreen({ name: "tracks", tracks });
790
896
  } catch (e2) {
791
- setScreen({ name: "error", message: e2.message ?? "Failed to load tracks.", recoverable: false });
897
+ setScreen({
898
+ name: "error",
899
+ message: e2.message ?? "Failed to load tracks.",
900
+ recoverable: false
901
+ });
792
902
  }
793
903
  return;
794
904
  }
795
- setScreen({ name: "error", message: e.message ?? "Failed to load tracks.", recoverable: false });
905
+ setScreen({
906
+ name: "error",
907
+ message: e.message ?? "Failed to load tracks.",
908
+ recoverable: false
909
+ });
796
910
  }
797
911
  }
798
912
  function commitName(raw) {
@@ -805,10 +919,9 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
805
919
  );
806
920
  }
807
921
  function randomAnonName() {
808
- const hex = Array.from(
809
- { length: 6 },
810
- () => Math.floor(Math.random() * 16).toString(16)
811
- ).join("");
922
+ const hex = Array.from({ length: 6 }, () => Math.floor(Math.random() * 16).toString(16)).join(
923
+ ""
924
+ );
812
925
  return `anon-${hex}`;
813
926
  }
814
927
  async function joinTrack(track) {
@@ -818,7 +931,11 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
818
931
  await api.joinQueue(activeCeremonyId, track.id, session.session_token);
819
932
  } catch (e) {
820
933
  if (e.code !== "ALREADY_IN_QUEUE") {
821
- setScreen({ name: "error", message: e.message ?? "Failed to join queue.", recoverable: true });
934
+ setScreen({
935
+ name: "error",
936
+ message: e.message ?? "Failed to join queue.",
937
+ recoverable: true
938
+ });
822
939
  return;
823
940
  }
824
941
  }
@@ -833,7 +950,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
833
950
  setScreen({ name: "ceremony-picker", ceremonies: [], loading: true });
834
951
  loadCeremonies();
835
952
  }
836
- useEffect4(() => {
953
+ useEffect5(() => {
837
954
  if (screen.name === "queue" && session) {
838
955
  const { trackId } = screen;
839
956
  setQueueCleanup(() => {
@@ -990,6 +1107,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
990
1107
  const isSelected = i === selectedIdx;
991
1108
  const isOpen = c.status === "open";
992
1109
  const statusColor = c.status === "open" ? "green" : c.status === "completed" ? "cyan" : "yellow";
1110
+ const statusLabel = ceremonyStatusLabel(c.status);
993
1111
  return /* @__PURE__ */ jsxs7(Box7, { gap: 2, children: [
994
1112
  /* @__PURE__ */ jsxs7(Text7, { color: isSelected ? "cyan" : isOpen ? void 0 : "gray", children: [
995
1113
  isSelected ? "\u25B6 " : " ",
@@ -997,7 +1115,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
997
1115
  ] }),
998
1116
  /* @__PURE__ */ jsxs7(Text7, { color: statusColor, children: [
999
1117
  "[",
1000
- c.status,
1118
+ statusLabel,
1001
1119
  "]"
1002
1120
  ] }),
1003
1121
  /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
@@ -1017,20 +1135,23 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1017
1135
  if (!c) return null;
1018
1136
  if (c.status === "initialized") {
1019
1137
  return /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
1020
- " Not open yet \u2014 admin: run initialize ",
1021
- "<id>",
1022
- " then open ",
1023
- "<id>",
1024
- " to start contributions."
1138
+ " ",
1139
+ "This ceremony is not yet open for contributions. The Umbra team is still preparing the circuits \u2014 please check back shortly."
1025
1140
  ] });
1026
1141
  }
1027
1142
  if (c.status === "finalizing") {
1028
- return /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: " Contribution phase is closed \u2014 ceremony is applying the final beacon." });
1143
+ return /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
1144
+ " ",
1145
+ "Contributions are closed. The ceremony is computing the final verification key \u2014 no further contributions can be added."
1146
+ ] });
1029
1147
  }
1030
1148
  if (c.status === "completed") {
1031
- return /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: " Ceremony complete \u2014 verification keys are available." });
1149
+ return /* @__PURE__ */ jsxs7(Text7, { color: "cyan", children: [
1150
+ " ",
1151
+ "Ceremony complete. The verification keys are finalised \u2014 thank you to everyone who contributed."
1152
+ ] });
1032
1153
  }
1033
- return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2191/\u2193 select \xB7 Enter join \xB7 Q quit" });
1154
+ return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2191/\u2193 select \xB7 Enter join \xB7 Q quit" });
1034
1155
  })()
1035
1156
  ] })
1036
1157
  ] });
@@ -1046,7 +1167,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1046
1167
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1047
1168
  /* @__PURE__ */ jsx7(Header, { ceremony }),
1048
1169
  /* @__PURE__ */ jsxs7(Text7, { color: "red", bold: true, children: [
1049
- "\u2717 ",
1170
+ "\u2717 ",
1050
1171
  screen.message
1051
1172
  ] }),
1052
1173
  /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: backHint })
@@ -1055,12 +1176,14 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1055
1176
  if (screen.name === "tracks") {
1056
1177
  const { tracks } = screen;
1057
1178
  const openTracks = tracks.filter((t) => t.status === "open");
1058
- const myContributions = Object.values(contributed).filter((c) => c.ceremonyId === activeCeremonyId);
1179
+ const myContributions = Object.values(contributed).filter(
1180
+ (c) => c.ceremonyId === activeCeremonyId
1181
+ );
1059
1182
  const TabBar = () => /* @__PURE__ */ jsxs7(Box7, { gap: 1, marginBottom: 1, children: [
1060
1183
  /* @__PURE__ */ jsx7(Text7, { bold: tab === 0, color: tab === 0 ? "cyan" : void 0, dimColor: tab !== 0, children: tab === 0 ? "[ Dashboard ]" : " Dashboard " }),
1061
1184
  /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "|" }),
1062
1185
  /* @__PURE__ */ jsx7(Text7, { bold: tab === 1, color: tab === 1 ? "cyan" : void 0, dimColor: tab !== 1, children: tab === 1 ? `[ My Contributions (${myContributions.length}) ]` : ` My Contributions (${myContributions.length}) ` }),
1063
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " Tab to switch" })
1186
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " Tab to switch" })
1064
1187
  ] });
1065
1188
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1066
1189
  /* @__PURE__ */ jsx7(Header, { ceremony }),
@@ -1072,10 +1195,10 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1072
1195
  " CIRCUIT".padEnd(30),
1073
1196
  "TOTAL".padEnd(10),
1074
1197
  "QUEUE".padEnd(8),
1075
- "STATUS".padEnd(14),
1198
+ "STATUS".padEnd(16),
1076
1199
  "MY CONTRIBUTIONS"
1077
1200
  ] }),
1078
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(68) }),
1201
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(70) }),
1079
1202
  tracks.map((t, i) => {
1080
1203
  const isSelected = i === selectedIdx;
1081
1204
  const canContribute = t.status === "open";
@@ -1089,21 +1212,21 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1089
1212
  String(t.contribution_count).padEnd(10),
1090
1213
  String(t.queue_depth).padEnd(8)
1091
1214
  ] }),
1092
- /* @__PURE__ */ jsx7(Text7, { color: statusColor, children: t.status.padEnd(14) }),
1215
+ /* @__PURE__ */ jsx7(Text7, { color: statusColor, children: trackStatusLabel(t.status).padEnd(16) }),
1093
1216
  myContrib ? isSelected ? /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
1094
1217
  "\u2713 contributed (round #",
1095
1218
  myContrib.sequenceNumber,
1096
- ") \u2014 already done"
1219
+ ") \u2014 already done"
1097
1220
  ] }) : /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
1098
- "\u2713 contributed (round #",
1221
+ "\u2713 contributed (round #",
1099
1222
  myContrib.sequenceNumber,
1100
1223
  ")"
1101
1224
  ] }) : canContribute ? isSelected ? /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "\u2190 Enter to contribute" }) : /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "not contributed" }) : /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2014" })
1102
1225
  ] }, t.id);
1103
1226
  }),
1104
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(68) }),
1105
- openTracks.length === 0 ? /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "No open tracks \u2014 ceremony may be finalizing or complete." }) : /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1106
- "\u2191/\u2193 select \xB7 Enter contribute \xB7 R refresh \xB7 Q quit",
1227
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(70) }),
1228
+ openTracks.length === 0 ? /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "No circuits are open for contributions right now \u2014 the ceremony may be closing or already complete." }) : /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1229
+ "\u2191/\u2193 select \xB7 Enter contribute \xB7 R refresh \xB7 Q quit",
1107
1230
  !initialCeremonyId ? " \xB7 \u232B back to ceremony list" : ""
1108
1231
  ] })
1109
1232
  ] })
@@ -1122,7 +1245,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1122
1245
  /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(68) }),
1123
1246
  myContributions.map((c, i) => /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1124
1247
  /* @__PURE__ */ jsxs7(Box7, { children: [
1125
- /* @__PURE__ */ jsx7(Text7, { color: "green", children: " \u2713 " }),
1248
+ /* @__PURE__ */ jsx7(Text7, { color: "green", children: " \u2713 " }),
1126
1249
  /* @__PURE__ */ jsx7(Text7, { bold: true, children: c.circuitName.padEnd(24) }),
1127
1250
  /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "#" + c.sequenceNumber + " " }),
1128
1251
  /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: c.contributionHash ? c.contributionHash.slice(0, 16) + "..." : "(pending)" })
@@ -1139,7 +1262,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1139
1262
  " contribution",
1140
1263
  myContributions.length !== 1 ? "s" : "",
1141
1264
  " \xB7 ",
1142
- "Tab to switch \xB7 Q to quit"
1265
+ "Tab to switch \xB7 Q to quit"
1143
1266
  ] })
1144
1267
  ] }) })
1145
1268
  )
@@ -1157,7 +1280,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1157
1280
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1158
1281
  /* @__PURE__ */ jsx7(Header, { ceremony, subtitle: `Circuit: ${circuitName}` }),
1159
1282
  priorContrib && /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, paddingX: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
1160
- "\u26A0 You already contributed to this circuit (round #",
1283
+ "\u26A0 You already contributed to this circuit (round #",
1161
1284
  priorContrib.sequenceNumber,
1162
1285
  ").",
1163
1286
  " ",
@@ -1173,7 +1296,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1173
1296
  onError: (e) => setScreen({ name: "error", message: e.message, recoverable: true })
1174
1297
  }
1175
1298
  ),
1176
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u232B Backspace to go back \xB7 Q to quit" })
1299
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u232B Backspace to go back \xB7 Q to quit" })
1177
1300
  ] });
1178
1301
  }
1179
1302
  if (screen.name === "entropy") {
@@ -1225,7 +1348,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1225
1348
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1226
1349
  /* @__PURE__ */ jsx7(Header, { ceremony }),
1227
1350
  /* @__PURE__ */ jsx7(Attestation, { contribution }),
1228
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u232B Backspace / B = contribute to another circuit \xB7 Q to quit" }) })
1351
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u232B Backspace / B = contribute to another circuit \xB7 Q to quit" }) })
1229
1352
  ] });
1230
1353
  }
1231
1354
  return null;
@@ -1256,10 +1379,9 @@ process.on("SIGTERM", () => {
1256
1379
  process.exit(0);
1257
1380
  });
1258
1381
  });
1259
- var { waitUntilExit } = render(
1260
- /* @__PURE__ */ jsx8(App, { ceremonyId, displayName }),
1261
- { exitOnCtrlC: false }
1262
- );
1382
+ var { waitUntilExit } = render(/* @__PURE__ */ jsx8(App, { ceremonyId, displayName }), {
1383
+ exitOnCtrlC: false
1384
+ });
1263
1385
  await waitUntilExit();
1264
1386
  restoreScreen();
1265
1387
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umbra-privacy/ceremony",
3
- "version": "0.2.3",
3
+ "version": "0.2.6",
4
4
  "description": "Terminal UI for the Umbra Phase 2 trusted setup ceremony",
5
5
  "type": "module",
6
6
  "bin": {