@umbra-privacy/ceremony 0.2.5 → 0.2.8

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 +277 -61
  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
@@ -315,22 +315,141 @@ function Header({ ceremony, subtitle }) {
315
315
  }
316
316
 
317
317
  // src/components/QueueView.tsx
318
- import { useEffect, useRef, useState } from "react";
318
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
319
319
  import { Box as Box2, Text as Text2 } from "ink";
320
320
  import Spinner from "ink-spinner";
321
- 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
+ "asking the curve where it lives",
342
+ "weaving G1 points into the new delta",
343
+ "negotiating with the pairing engine",
344
+ "rotating the keypair commitment",
345
+ "checking that tau is non-zero (just in case)",
346
+ "evaluating polynomials over your secret",
347
+ "scrubbing the entropy buffer mid-flight",
348
+ "ratifying the contribution sequence",
349
+ "computing s \xB7 \u03C4 in G1",
350
+ "computing t \xB7 \u03C4 in G2",
351
+ "running the signature-of-knowledge",
352
+ "binding the transcript to your contribution",
353
+ "double-checking nothing is at infinity",
354
+ "compressing the response footprint",
355
+ "verifying your math one more time",
356
+ "stamping the public key bundle",
357
+ "writing the response file to disk",
358
+ "preparing to hand the response back to the worker",
359
+ "almost done \u2014 finalising your tau",
360
+ "this is the cryptography working hard for you",
361
+ "your secret never leaves this machine",
362
+ "if your power-21 circuit feels slow, blame BN254",
363
+ "snarkjs is single-threaded, give it a moment",
364
+ "trusted setup ceremonies are a team sport",
365
+ "the worker will verify everything you did"
366
+ ];
367
+ var VERIFYING_MESSAGES = [
368
+ "worker pulling your response from S3",
369
+ "importing bellman contribution into a new zkey",
370
+ "checking the contribution's pairing proof",
371
+ "writing the next zkey",
372
+ "uploading the new zkey",
373
+ "anchoring contribution hash in the audit chain",
374
+ "almost there \u2014 finalising your receipt",
375
+ "parsing your response's wire format",
376
+ "validating every G1 point is on the curve",
377
+ "validating every G2 point is in the right subgroup",
378
+ "rebuilding the transcript hash chain",
379
+ "running the knowledge proof pairing",
380
+ "verifying the new delta_g1 matches its evidence",
381
+ "verifying the new delta_g2 matches its evidence",
382
+ "checking the L array against your contribution",
383
+ "checking the H array against your contribution",
384
+ "deriving Fiat-Shamir scalars for the batched check",
385
+ "running the random-linear-combination same-ratio",
386
+ "confirming the vKey fields were not altered",
387
+ "sequencing the new zkey for the next contributor",
388
+ "this is the part where every byte gets re-checked",
389
+ "soundness of the whole ceremony rides on this step",
390
+ "exporting the new bellman params for the next round",
391
+ "writing your contribution into the ceremony transcript",
392
+ "publishing the new audit chain entry",
393
+ "any tampering would have failed by now",
394
+ "if you see this, your contribution is provably honest",
395
+ "computing your contribution receipt",
396
+ "the math agrees \u2014 wrapping things up",
397
+ "finalising the verified state in Postgres",
398
+ "you did the cryptographic work \u2014 we just had to check"
399
+ ];
400
+ var EXPORTING_MESSAGES = [
401
+ "preparing your challenge file",
402
+ "extracting bellman params from the current zkey",
403
+ "wrapping the zkey for handoff",
404
+ "stamping a sha256 over the challenge bundle",
405
+ "presigning your download URL",
406
+ "fetching the latest verified state for this circuit",
407
+ "downloading the previous contribution from S3",
408
+ "checking the previous contribution's hash",
409
+ "carving out a personal challenge for your tau",
410
+ "the worker is queuing your slot",
411
+ "uploading the challenge so you can download it",
412
+ "rotating the worker's S3 connection",
413
+ "binding your challenge to the audit chain",
414
+ "stamping the challenge metadata in Postgres",
415
+ "AWS SDK is recycling connection pools",
416
+ "your slot timer starts the moment this finishes",
417
+ "writing the challenge to a fresh S3 key",
418
+ "double-checking the integrity hash",
419
+ "if this takes >1 min, S3 is having a bad time",
420
+ "Fargate is parsing the current bellman params",
421
+ "verifying the previous contribution's chain link",
422
+ "this step has no contributor-side analogue",
423
+ "the worker does the boring work so you don't have to",
424
+ "any second now \u2014 challenge is almost ready",
425
+ "encoding the challenge in Bellman wire format",
426
+ "making sure the file size matches the spec",
427
+ "presign URL is signed for 1 hour",
428
+ "your download will be tamper-evident",
429
+ "the challenge SHA-256 will be on the public transcript",
430
+ "patience \u2014 this is the longest server-side step",
431
+ "almost there \u2014 your turn is moments away"
432
+ ];
433
+
434
+ // src/components/QueueView.tsx
435
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
322
436
  var POLL_FAST_MS = 5e3;
323
437
  var POLL_SLOW_MS = 15e3;
438
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
439
+ "timed_out",
440
+ "failed",
441
+ "verified"
442
+ ]);
324
443
  function QueueView({ ceremonyId: ceremonyId2, trackId, token, onReady, onError }) {
325
- const [status, setStatus] = useState(null);
326
- const [pollErr, setPollErr] = useState(null);
327
- const [tick, setTick] = useState(0);
444
+ const [status, setStatus] = useState2(null);
445
+ const [pollErr, setPollErr] = useState2(null);
446
+ const [tick, setTick] = useState2(0);
328
447
  const timeoutRef = useRef(null);
329
- useEffect(() => {
448
+ useEffect2(() => {
330
449
  const id = setInterval(() => setTick((t) => (t + 1) % 4), 500);
331
450
  return () => clearInterval(id);
332
451
  }, []);
333
- useEffect(() => {
452
+ useEffect2(() => {
334
453
  let cancelled = false;
335
454
  async function poll() {
336
455
  try {
@@ -342,6 +461,9 @@ function QueueView({ ceremonyId: ceremonyId2, trackId, token, onReady, onError }
342
461
  onReady(s);
343
462
  return;
344
463
  }
464
+ if (TERMINAL_STATUSES.has(s.status)) {
465
+ return;
466
+ }
345
467
  const interval = s.queue_position <= 2 ? POLL_FAST_MS : POLL_SLOW_MS;
346
468
  timeoutRef.current = setTimeout(poll, interval);
347
469
  } catch (err) {
@@ -367,6 +489,9 @@ function QueueView({ ceremonyId: ceremonyId2, trackId, token, onReady, onError }
367
489
  ] })
368
490
  ] });
369
491
  }
492
+ if (TERMINAL_STATUSES.has(status.status)) {
493
+ return /* @__PURE__ */ jsx2(TerminalSlotMessage, { status: status.status });
494
+ }
370
495
  const waitMins = Math.ceil((status.estimated_wait_secs ?? 0) / 60);
371
496
  const expiresAt = status.slot_expires_at ? new Date(status.slot_expires_at).toLocaleTimeString([], {
372
497
  hour: "2-digit",
@@ -395,7 +520,7 @@ function QueueView({ ceremonyId: ceremonyId2, trackId, token, onReady, onError }
395
520
  ] })
396
521
  ] })
397
522
  ] }),
398
- 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: [
523
+ 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: [
399
524
  "Another contributor is active",
400
525
  expiresAt ? ` \xB7 slot expires at ${expiresAt}` : ""
401
526
  ] }) }) : status.queue_position > 1 ? (
@@ -419,9 +544,46 @@ function QueueView({ ceremonyId: ceremonyId2, trackId, token, onReady, onError }
419
544
  ] })
420
545
  ] });
421
546
  }
547
+ function TerminalSlotMessage({ status }) {
548
+ const lines = {
549
+ timed_out: {
550
+ headline: "Your slot expired before the challenge file was ready.",
551
+ 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."
552
+ },
553
+ failed: {
554
+ headline: "The worker reported a failure on this contribution.",
555
+ detail: "Press B or Backspace to return to the track list. If this happens repeatedly on the same track, the admin needs to investigate."
556
+ },
557
+ verified: {
558
+ headline: "This contribution is already recorded as verified.",
559
+ detail: "Press B or Backspace to return to the track list and pick another track."
560
+ }
561
+ };
562
+ const key = status;
563
+ const msg = lines[key] ?? {
564
+ headline: `Contribution ended with status: ${status}.`,
565
+ detail: "Press B or Backspace to return to the track list."
566
+ };
567
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
568
+ /* @__PURE__ */ jsx2(Text2, { color: "yellow", bold: true, children: msg.headline }),
569
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: msg.detail })
570
+ ] });
571
+ }
572
+ function ExportingMessage({ status }) {
573
+ const exportingMsg = useCyclingMessage(EXPORTING_MESSAGES, 2500, status === "exporting");
574
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: status === "exporting" ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
575
+ /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
576
+ /* @__PURE__ */ jsx2(Spinner, { type: "dots" }),
577
+ " ",
578
+ exportingMsg,
579
+ "\u2026"
580
+ ] }),
581
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Your slot is active. The worker is preparing the challenge file just for you." })
582
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: "green", children: "Challenge ready \u2014 loading contribution flow\u2026" }) });
583
+ }
422
584
 
423
585
  // src/components/EntropyCollector.tsx
424
- import { useRef as useRef2, useState as useState2 } from "react";
586
+ import { useRef as useRef2, useState as useState3 } from "react";
425
587
  import { Box as Box3, Text as Text3, useInput } from "ink";
426
588
 
427
589
  // src/entropy.ts
@@ -442,8 +604,8 @@ function buildEntropyFromKeystrokes(chars, timingsNs) {
442
604
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
443
605
  var TARGET = 20;
444
606
  function EntropyCollector({ onComplete, onError }) {
445
- const [count, setCount] = useState2(0);
446
- const [done, setDone] = useState2(false);
607
+ const [count, setCount] = useState3(0);
608
+ const [done, setDone] = useState3(false);
447
609
  const charsRef = useRef2([]);
448
610
  const timingsRef = useRef2([]);
449
611
  const lastRef = useRef2(process.hrtime.bigint());
@@ -511,7 +673,7 @@ function EntropyCollector({ onComplete, onError }) {
511
673
  }
512
674
 
513
675
  // src/components/ContributeFlow.tsx
514
- import { useEffect as useEffect3, useState as useState3 } from "react";
676
+ import { useEffect as useEffect4, useState as useState4 } from "react";
515
677
  import { Box as Box4, Text as Text4 } from "ink";
516
678
  import Spinner2 from "ink-spinner";
517
679
  import { tmpdir as tmpdir2 } from "os";
@@ -550,8 +712,8 @@ var STEP_INDEX = {
550
712
  };
551
713
  function ContributeFlow(props) {
552
714
  const { ceremonyId: ceremonyId2, trackId, token, slotStatus, entropy, displayName: displayName2 } = props;
553
- const [step, setStep] = useState3({ name: "downloading", bytesReceived: 0, total: null });
554
- useEffect3(() => {
715
+ const [step, setStep] = useState4({ name: "downloading", bytesReceived: 0, total: null });
716
+ useEffect4(() => {
555
717
  let cancelled = false;
556
718
  const challengePath = join4(tmpdir2(), `ceremony-challenge-${Date.now()}.mpcparams`);
557
719
  let responsePath = null;
@@ -597,8 +759,8 @@ function ContributeFlow(props) {
597
759
  await api.signalUploaded(ceremonyId2, trackId, slotStatus.contribution_id, token);
598
760
  if (cancelled) return;
599
761
  setStep({ name: "verifying", attempt: 1 });
600
- const TOTAL_POLLS = 80;
601
- const FAST_POLLS = 20;
762
+ const TOTAL_POLLS = 100;
763
+ const FAST_POLLS = 10;
602
764
  let receipt = null;
603
765
  let lastErr = null;
604
766
  for (let i = 0; i < TOTAL_POLLS; i++) {
@@ -632,6 +794,8 @@ function ContributeFlow(props) {
632
794
  };
633
795
  }, []);
634
796
  const currentIdx = STEP_INDEX[step.name] ?? 0;
797
+ const computingMsg = useCyclingMessage(COMPUTING_MESSAGES, 2500, step.name === "computing");
798
+ const verifyingMsg = useCyclingMessage(VERIFYING_MESSAGES, 3e3, step.name === "verifying");
635
799
  return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", gap: 1, children: STEP_LABELS.map((label, i) => {
636
800
  const isDone = i < currentIdx;
637
801
  const isActive = i === currentIdx;
@@ -655,17 +819,25 @@ function ContributeFlow(props) {
655
819
  ] });
656
820
  }
657
821
  if (isActive && step.name === "verifying") {
658
- const elapsed = step.attempt <= 20 ? step.attempt * 3 : 20 * 3 + (step.attempt - 20) * 10;
822
+ const elapsed = step.attempt <= 10 ? step.attempt * 3 : 10 * 3 + (step.attempt - 10) * 10;
659
823
  const mins = Math.floor(elapsed / 60);
660
824
  const secs = elapsed % 60;
661
825
  const time = mins > 0 ? `${mins}m ${secs.toString().padStart(2, "0")}s` : `${secs}s`;
662
826
  detail = /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
663
827
  " ",
664
- "worker verifying \u2014 large circuits can take a few minutes (",
828
+ verifyingMsg,
829
+ "\u2026 (",
665
830
  time,
666
831
  " elapsed)"
667
832
  ] });
668
833
  }
834
+ if (isActive && step.name === "computing") {
835
+ detail = /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
836
+ " ",
837
+ computingMsg,
838
+ "\u2026"
839
+ ] });
840
+ }
669
841
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
670
842
  /* @__PURE__ */ jsxs4(Box4, { gap: 2, children: [
671
843
  indicator,
@@ -678,8 +850,7 @@ function ContributeFlow(props) {
678
850
  children: [
679
851
  label.charAt(0).toUpperCase() + label.slice(1),
680
852
  " ",
681
- isActive && label === "computing" && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "(entropy stays local)" }),
682
- isActive && label === "uploading" && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "(~544 bytes)" })
853
+ isActive && label === "computing" && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "(entropy stays local)" })
683
854
  ]
684
855
  }
685
856
  )
@@ -753,21 +924,33 @@ function InfoModal() {
753
924
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
754
925
  var NAME_MAX_LEN = 100;
755
926
  var NAME_VALID_RE = /^[\p{L}\p{N} _.\-]*$/u;
927
+ function elideMiddle(s, maxLen) {
928
+ if (s.length <= maxLen) return s;
929
+ const keepHead = Math.ceil((maxLen - 1) / 2);
930
+ const keepTail = Math.floor((maxLen - 1) / 2);
931
+ return s.slice(0, keepHead) + "\u2026" + s.slice(s.length - keepTail);
932
+ }
933
+ function copyToClipboardOSC52(value) {
934
+ const payload = Buffer.from(value, "utf8").toString("base64");
935
+ process.stdout.write(`\x1B]52;c;${payload}\x07`);
936
+ }
756
937
  function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName }) {
757
938
  const { exit } = useApp();
758
- const [activeCeremonyId, setActiveCeremonyId] = useState4(initialCeremonyId);
759
- const [displayName2, setDisplayName] = useState4(initialDisplayName ?? "anonymous");
760
- const [nameSet, setNameSet] = useState4(initialDisplayName !== void 0);
761
- const [screen, setScreen] = useState4(
939
+ const [activeCeremonyId, setActiveCeremonyId] = useState5(initialCeremonyId);
940
+ const [displayName2, setDisplayName] = useState5(initialDisplayName ?? "anonymous");
941
+ const [nameSet, setNameSet] = useState5(initialDisplayName !== void 0);
942
+ const [screen, setScreen] = useState5(
762
943
  initialDisplayName === void 0 ? { name: "name-input", value: "" } : initialCeremonyId ? { name: "loading" } : { name: "ceremony-picker", ceremonies: [], loading: true }
763
944
  );
764
- const [ceremony, setCeremony] = useState4(null);
765
- const [session, setSession] = useState4(null);
766
- const [contributed, setContributed] = useState4({});
767
- const [selectedIdx, setSelectedIdx] = useState4(0);
768
- const [tab, setTab] = useState4(0);
769
- const [showInfo, setShowInfo] = useState4(false);
770
- useEffect4(() => {
945
+ const [ceremony, setCeremony] = useState5(null);
946
+ const [session, setSession] = useState5(null);
947
+ const [contributed, setContributed] = useState5({});
948
+ const [selectedIdx, setSelectedIdx] = useState5(0);
949
+ const [tab, setTab] = useState5(0);
950
+ const [contribCursor, setContribCursor] = useState5(0);
951
+ const [copyToast, setCopyToast] = useState5(null);
952
+ const [showInfo, setShowInfo] = useState5(false);
953
+ useEffect5(() => {
771
954
  if (!nameSet) return;
772
955
  if (!initialCeremonyId) {
773
956
  loadCeremonies();
@@ -890,7 +1073,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
890
1073
  setScreen({ name: "ceremony-picker", ceremonies: [], loading: true });
891
1074
  loadCeremonies();
892
1075
  }
893
- useEffect4(() => {
1076
+ useEffect5(() => {
894
1077
  if (screen.name === "queue" && session) {
895
1078
  const { trackId } = screen;
896
1079
  setQueueCleanup(() => {
@@ -986,6 +1169,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
986
1169
  const { tracks } = screen;
987
1170
  if (key.tab) {
988
1171
  setTab((t) => (t + 1) % 2);
1172
+ setContribCursor(0);
989
1173
  return;
990
1174
  }
991
1175
  if (tab === 0) {
@@ -1002,6 +1186,30 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1002
1186
  if (t && t.status === "open" && !contributed[t.id]) joinTrack(t);
1003
1187
  return;
1004
1188
  }
1189
+ } else if (tab === 1) {
1190
+ const myContribs = Object.values(contributed).filter(
1191
+ (c) => c.ceremonyId === activeCeremonyId
1192
+ );
1193
+ if (key.upArrow) {
1194
+ setContribCursor((i) => Math.max(0, i - 1));
1195
+ return;
1196
+ }
1197
+ if (key.downArrow) {
1198
+ setContribCursor((i) => Math.min(myContribs.length - 1, i + 1));
1199
+ return;
1200
+ }
1201
+ if (q === "c") {
1202
+ const target = myContribs[contribCursor];
1203
+ if (target && target.contributionHash) {
1204
+ copyToClipboardOSC52(target.contributionHash);
1205
+ setCopyToast(`\u2713 Hash copied (round #${target.sequenceNumber})`);
1206
+ setTimeout(() => setCopyToast(null), 2e3);
1207
+ } else if (target) {
1208
+ setCopyToast("Nothing to copy \u2014 hash is still pending");
1209
+ setTimeout(() => setCopyToast(null), 2e3);
1210
+ }
1211
+ return;
1212
+ }
1005
1213
  }
1006
1214
  if (q === "r") {
1007
1215
  goHome();
@@ -1130,29 +1338,34 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1130
1338
  /* @__PURE__ */ jsx7(TabBar, {}),
1131
1339
  tab === 0 ? (
1132
1340
  // ── Dashboard tab ────────────────────────────────────────────────
1341
+ // CIRCUIT column is sized to fit the longest mainnet name
1342
+ // (`claim-deposit-into-confidential-amount-n4` = 41 chars). Names
1343
+ // longer than that are middle-elided so the disambiguating suffix
1344
+ // (-n1/-n2/-n4) stays visible — previously a hard slice(0,24)
1345
+ // showed every claim variant as `claim-deposit-into-confi..`.
1133
1346
  /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1134
1347
  /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1135
- " CIRCUIT".padEnd(30),
1136
- "TOTAL".padEnd(10),
1348
+ " CIRCUIT".padEnd(46),
1349
+ "TOTAL".padEnd(8),
1137
1350
  "QUEUE".padEnd(8),
1138
- "STATUS".padEnd(16),
1351
+ "STATUS".padEnd(14),
1139
1352
  "MY CONTRIBUTIONS"
1140
1353
  ] }),
1141
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(70) }),
1354
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(90) }),
1142
1355
  tracks.map((t, i) => {
1143
1356
  const isSelected = i === selectedIdx;
1144
1357
  const canContribute = t.status === "open";
1145
- const nameClipped = t.circuit_name.length > 26 ? t.circuit_name.slice(0, 24) + ".." : t.circuit_name;
1358
+ const nameDisplay = elideMiddle(t.circuit_name, 42);
1146
1359
  const statusColor = t.status === "open" ? "green" : t.status === "finalized" ? "cyan" : "yellow";
1147
1360
  const myContrib = contributed[t.id];
1148
1361
  return /* @__PURE__ */ jsxs7(Box7, { children: [
1149
1362
  /* @__PURE__ */ jsxs7(Text7, { color: isSelected ? "cyan" : canContribute ? void 0 : "gray", children: [
1150
1363
  isSelected ? "\u25B6 " : " ",
1151
- nameClipped.padEnd(28),
1152
- String(t.contribution_count).padEnd(10),
1364
+ nameDisplay.padEnd(44),
1365
+ String(t.contribution_count).padEnd(8),
1153
1366
  String(t.queue_depth).padEnd(8)
1154
1367
  ] }),
1155
- /* @__PURE__ */ jsx7(Text7, { color: statusColor, children: trackStatusLabel(t.status).padEnd(16) }),
1368
+ /* @__PURE__ */ jsx7(Text7, { color: statusColor, children: trackStatusLabel(t.status).padEnd(14) }),
1156
1369
  myContrib ? isSelected ? /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
1157
1370
  "\u2713 contributed (round #",
1158
1371
  myContrib.sequenceNumber,
@@ -1177,32 +1390,35 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1177
1390
  /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Switch to Dashboard tab and press Enter on a circuit to contribute." })
1178
1391
  ] }) : /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1179
1392
  /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1180
- " CIRCUIT".padEnd(28),
1181
- "ROUND".padEnd(8),
1182
- "HASH".padEnd(20),
1183
- "TIME"
1393
+ " CIRCUIT".padEnd(44),
1394
+ "ROUND".padEnd(7),
1395
+ "VERIFIED AT".padEnd(22)
1184
1396
  ] }),
1185
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(68) }),
1186
- myContributions.map((c, i) => /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1187
- /* @__PURE__ */ jsxs7(Box7, { children: [
1188
- /* @__PURE__ */ jsx7(Text7, { color: "green", children: " \u2713 " }),
1189
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: c.circuitName.padEnd(24) }),
1190
- /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "#" + c.sequenceNumber + " " }),
1191
- /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: c.contributionHash ? c.contributionHash.slice(0, 16) + "..." : "(pending)" })
1192
- ] }),
1193
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1194
- " ",
1195
- c.verifiedAt ? new Date(c.verifiedAt).toLocaleString() : ""
1196
- ] })
1197
- ] }, i)),
1198
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(68) }),
1199
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1397
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(90) }),
1398
+ myContributions.map((c, i) => {
1399
+ const isSel = i === contribCursor;
1400
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1401
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1402
+ /* @__PURE__ */ jsx7(Text7, { color: isSel ? "cyan" : "green", children: isSel ? "\u25B6 " : " " }),
1403
+ /* @__PURE__ */ jsx7(Text7, { bold: isSel, children: elideMiddle(c.circuitName, 40).padEnd(42) }),
1404
+ /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: ("#" + c.sequenceNumber).padEnd(7) }),
1405
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: c.verifiedAt ? new Date(c.verifiedAt).toLocaleString() : "\u2014" })
1406
+ ] }),
1407
+ /* @__PURE__ */ jsx7(Box7, { paddingLeft: 4, children: /* @__PURE__ */ jsxs7(Text7, { color: isSel ? "cyan" : "gray", dimColor: !isSel, children: [
1408
+ "hash:",
1409
+ " ",
1410
+ c.contributionHash ? c.contributionHash : "(pending \u2014 verify still in flight)"
1411
+ ] }) })
1412
+ ] }, i);
1413
+ }),
1414
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(90) }),
1415
+ copyToast ? /* @__PURE__ */ jsx7(Text7, { color: "green", children: copyToast }) : /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1200
1416
  "Total: ",
1201
1417
  myContributions.length,
1202
1418
  " contribution",
1203
1419
  myContributions.length !== 1 ? "s" : "",
1204
1420
  " \xB7 ",
1205
- "Tab to switch \xB7 Q to quit"
1421
+ "\u2191/\u2193 select \xB7 C copy hash \xB7 Tab switch \xB7 Q quit"
1206
1422
  ] })
1207
1423
  ] }) })
1208
1424
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umbra-privacy/ceremony",
3
- "version": "0.2.5",
3
+ "version": "0.2.8",
4
4
  "description": "Terminal UI for the Umbra Phase 2 trusted setup ceremony",
5
5
  "type": "module",
6
6
  "bin": {