@ckb-firewall/cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +11 -6
  2. package/dist/commands/anchor.d.ts +23 -0
  3. package/dist/commands/anchor.d.ts.map +1 -0
  4. package/dist/commands/anchor.js +412 -0
  5. package/dist/commands/anchor.js.map +1 -0
  6. package/dist/commands/config.d.ts +4 -0
  7. package/dist/commands/config.d.ts.map +1 -0
  8. package/dist/commands/config.js +59 -0
  9. package/dist/commands/config.js.map +1 -0
  10. package/dist/commands/execute.d.ts +9 -1
  11. package/dist/commands/execute.d.ts.map +1 -1
  12. package/dist/commands/execute.js +394 -229
  13. package/dist/commands/execute.js.map +1 -1
  14. package/dist/commands/import.d.ts.map +1 -1
  15. package/dist/commands/import.js +15 -48
  16. package/dist/commands/import.js.map +1 -1
  17. package/dist/commands/inspect.d.ts.map +1 -1
  18. package/dist/commands/inspect.js +32 -0
  19. package/dist/commands/inspect.js.map +1 -1
  20. package/dist/commands/proposals.d.ts.map +1 -1
  21. package/dist/commands/proposals.js +2 -4
  22. package/dist/commands/proposals.js.map +1 -1
  23. package/dist/commands/propose.d.ts +5 -0
  24. package/dist/commands/propose.d.ts.map +1 -1
  25. package/dist/commands/propose.js +91 -7
  26. package/dist/commands/propose.js.map +1 -1
  27. package/dist/commands/reclaim.d.ts +18 -0
  28. package/dist/commands/reclaim.d.ts.map +1 -0
  29. package/dist/commands/reclaim.js +214 -0
  30. package/dist/commands/reclaim.js.map +1 -0
  31. package/dist/commands/sign.d.ts.map +1 -1
  32. package/dist/commands/sign.js +40 -94
  33. package/dist/commands/sign.js.map +1 -1
  34. package/dist/commands/vote.d.ts +2 -0
  35. package/dist/commands/vote.d.ts.map +1 -1
  36. package/dist/commands/vote.js +31 -25
  37. package/dist/commands/vote.js.map +1 -1
  38. package/dist/index.js +91 -16
  39. package/dist/index.js.map +1 -1
  40. package/dist/lib/blkl.d.ts +17 -2
  41. package/dist/lib/blkl.d.ts.map +1 -1
  42. package/dist/lib/blkl.js +133 -17
  43. package/dist/lib/blkl.js.map +1 -1
  44. package/dist/lib/capacity.d.ts +12 -0
  45. package/dist/lib/capacity.d.ts.map +1 -0
  46. package/dist/lib/capacity.js +18 -0
  47. package/dist/lib/capacity.js.map +1 -0
  48. package/dist/lib/config.d.ts +7 -0
  49. package/dist/lib/config.d.ts.map +1 -0
  50. package/dist/lib/config.js +35 -0
  51. package/dist/lib/config.js.map +1 -0
  52. package/dist/lib/defaults.d.ts +19 -0
  53. package/dist/lib/defaults.d.ts.map +1 -1
  54. package/dist/lib/defaults.js +34 -7
  55. package/dist/lib/defaults.js.map +1 -1
  56. package/dist/lib/governance-v4.d.ts +39 -0
  57. package/dist/lib/governance-v4.d.ts.map +1 -0
  58. package/dist/lib/governance-v4.js +194 -0
  59. package/dist/lib/governance-v4.js.map +1 -0
  60. package/dist/lib/gui-bundle.html +485 -316
  61. package/dist/lib/gui-server.d.ts.map +1 -1
  62. package/dist/lib/gui-server.js +300 -246
  63. package/dist/lib/gui-server.js.map +1 -1
  64. package/dist/lib/hints.d.ts +1 -1
  65. package/dist/lib/hints.d.ts.map +1 -1
  66. package/dist/lib/hints.js +3 -9
  67. package/dist/lib/hints.js.map +1 -1
  68. package/dist/lib/proposals.d.ts +17 -9
  69. package/dist/lib/proposals.d.ts.map +1 -1
  70. package/dist/lib/proposals.js +4 -26
  71. package/dist/lib/proposals.js.map +1 -1
  72. package/dist/lib/rpc.d.ts +5 -0
  73. package/dist/lib/rpc.d.ts.map +1 -1
  74. package/dist/lib/rpc.js +21 -0
  75. package/dist/lib/rpc.js.map +1 -1
  76. package/dist/lib/treasury-status.d.ts +28 -0
  77. package/dist/lib/treasury-status.d.ts.map +1 -0
  78. package/dist/lib/treasury-status.js +70 -0
  79. package/dist/lib/treasury-status.js.map +1 -0
  80. package/dist/lib/treasury.d.ts +15 -0
  81. package/dist/lib/treasury.d.ts.map +1 -0
  82. package/dist/lib/treasury.js +62 -0
  83. package/dist/lib/treasury.js.map +1 -0
  84. package/dist/lib/tx-deps.d.ts +9 -0
  85. package/dist/lib/tx-deps.d.ts.map +1 -0
  86. package/dist/lib/tx-deps.js +15 -0
  87. package/dist/lib/tx-deps.js.map +1 -0
  88. package/dist/lib/witness.d.ts +13 -11
  89. package/dist/lib/witness.d.ts.map +1 -1
  90. package/dist/lib/witness.js +85 -48
  91. package/dist/lib/witness.js.map +1 -1
  92. package/package.json +1 -1
@@ -320,6 +320,79 @@ a:focus, button:focus { outline: 2px solid var(--c-ink); outline-offset: 1px; }
320
320
 
321
321
  .tfw-page { display: flex; flex-direction: column; gap: 36px; }
322
322
 
323
+ .tfw-treasury {
324
+ display: grid;
325
+ grid-template-columns: minmax(0, 1fr) 220px;
326
+ gap: 18px 24px;
327
+ align-items: center;
328
+ padding: 16px 0;
329
+ border-top: 2px solid var(--c-rule);
330
+ border-bottom: 1px solid var(--c-hairline);
331
+ }
332
+ .tfw-treasury--compact {
333
+ margin-bottom: -8px;
334
+ }
335
+ .tfw-treasury--warn {
336
+ border-top-color: var(--c-red);
337
+ background: linear-gradient(90deg, rgba(183, 58, 37, 0.08), transparent);
338
+ }
339
+ .tfw-treasury__eyebrow {
340
+ font-family: var(--f-mono);
341
+ font-size: 10px;
342
+ font-weight: 600;
343
+ letter-spacing: 0.14em;
344
+ color: var(--c-red);
345
+ text-transform: uppercase;
346
+ margin-bottom: 7px;
347
+ }
348
+ .tfw-treasury__line {
349
+ display: flex;
350
+ flex-wrap: wrap;
351
+ gap: 8px 16px;
352
+ align-items: baseline;
353
+ color: var(--c-ink-2);
354
+ }
355
+ .tfw-treasury__line strong {
356
+ font-family: var(--f-serif);
357
+ font-size: 28px;
358
+ font-weight: 500;
359
+ line-height: 1;
360
+ color: var(--c-ink);
361
+ }
362
+ .tfw-treasury__line span {
363
+ font-family: var(--f-mono);
364
+ font-size: 11px;
365
+ color: var(--c-ink-mute);
366
+ }
367
+ .tfw-treasury__meter {
368
+ height: 10px;
369
+ border: 1px solid var(--c-ink);
370
+ background: var(--c-paper);
371
+ position: relative;
372
+ }
373
+ .tfw-treasury__meter span {
374
+ display: block;
375
+ height: 100%;
376
+ background: var(--c-green);
377
+ }
378
+ .tfw-treasury--warn .tfw-treasury__meter span {
379
+ background: var(--c-red);
380
+ }
381
+ .tfw-treasury__donate {
382
+ grid-column: 1 / -1;
383
+ display: flex;
384
+ flex-wrap: wrap;
385
+ gap: 8px 14px;
386
+ align-items: center;
387
+ color: var(--c-red);
388
+ font-weight: 600;
389
+ }
390
+ .tfw-treasury__donate code {
391
+ color: var(--c-ink);
392
+ overflow-wrap: anywhere;
393
+ font-weight: 500;
394
+ }
395
+
323
396
  .tfw-pagehead {
324
397
  display: flex;
325
398
  align-items: flex-end;
@@ -1320,6 +1393,15 @@ select.tfw-input {
1320
1393
  padding-top: 16px;
1321
1394
  border-top: 1px solid var(--c-hairline);
1322
1395
  }
1396
+ .tfw-advanced-actions {
1397
+ display: flex;
1398
+ flex-wrap: wrap;
1399
+ gap: 10px;
1400
+ margin-top: 12px;
1401
+ padding: 12px;
1402
+ border: 1px dashed var(--c-hairline);
1403
+ background: var(--c-paper-soft);
1404
+ }
1323
1405
 
1324
1406
  /* ── Forms ───────────────────────────────────────────────────────────────── */
1325
1407
  .tfw-form { display: flex; flex-direction: column; gap: 18px; }
@@ -1408,6 +1490,22 @@ select.tfw-input {
1408
1490
  border-top: 1px solid var(--c-hairline);
1409
1491
  }
1410
1492
 
1493
+ .tfw-step {
1494
+ border: 1px solid var(--c-hairline);
1495
+ border-radius: 4px;
1496
+ padding: 14px 16px;
1497
+ display: flex;
1498
+ flex-direction: column;
1499
+ gap: 10px;
1500
+ }
1501
+ .tfw-step__label {
1502
+ font-size: 11px;
1503
+ font-weight: 700;
1504
+ letter-spacing: 0.06em;
1505
+ text-transform: uppercase;
1506
+ color: var(--c-ink-mute);
1507
+ }
1508
+
1411
1509
  .tfw-form-success {
1412
1510
  text-align: center;
1413
1511
  padding: 32px 12px;
@@ -1521,6 +1619,7 @@ body[data-density="compact"] .tfw-page { gap: 26px; }
1521
1619
  .tfw-hero__title { font-size: 42px; }
1522
1620
  .tfw-pagehead__title { font-size: 36px; }
1523
1621
  .tfw-pagehead { flex-direction: column; align-items: flex-start; }
1622
+ .tfw-treasury { grid-template-columns: 1fr; }
1524
1623
  .tfw-stat__num { font-size: 38px; }
1525
1624
  .tfw-form-row { grid-template-columns: 1fr; }
1526
1625
  .tfw-radio-grp { grid-template-columns: 1fr; }
@@ -1902,7 +2001,6 @@ function reviewCountdown(iso) {
1902
2001
  function countYes(p) { return (p.votes || []).filter(v => v.vote === "yes").length; }
1903
2002
  function countNo(p) { return (p.votes || []).filter(v => v.vote === "no").length; }
1904
2003
  function countAbstain(p) { return (p.votes || []).filter(v => v.vote === "abstain").length; }
1905
- function sigCount(p) { return (p.signatures || []).length; }
1906
2004
  function reviewPassed(p) {
1907
2005
  const t = new Date(p.reviewWindowEndsAt).getTime();
1908
2006
  return !isNaN(t) && Date.now() >= t;
@@ -1910,7 +2008,6 @@ function reviewPassed(p) {
1910
2008
  function isReady(p) {
1911
2009
  return reviewPassed(p)
1912
2010
  && countYes(p) >= 3
1913
- && sigCount(p) >= 3
1914
2011
  && p.status !== "executed"
1915
2012
  && p.status !== "rejected";
1916
2013
  }
@@ -1924,8 +2021,8 @@ function displayStatus(p) {
1924
2021
  const STATUS_META = {
1925
2022
  "pending-review": { label: "pending review", tone: "amber" },
1926
2023
  "voting": { label: "voting open", tone: "blue" },
1927
- "approved": { label: "awaiting sigs", tone: "amber" },
1928
- "ready": { label: "ready to execute", tone: "green" },
2024
+ "approved": { label: "approved", tone: "amber" },
2025
+ "ready": { label: "ready", tone: "green" },
1929
2026
  "executed": { label: "executed", tone: "mute" },
1930
2027
  "rejected": { label: "rejected", tone: "red" },
1931
2028
  };
@@ -1983,23 +2080,6 @@ function VoteDots({ proposal, total = 3 }) {
1983
2080
  );
1984
2081
  }
1985
2082
 
1986
- function SigDots({ proposal, total = 3 }) {
1987
- const c = sigCount(proposal);
1988
- const dots = [];
1989
- for (let i = 0; i < total; i++) {
1990
- let cls = "tfw-dot tfw-dot--empty";
1991
- if (i < c) cls = "tfw-dot tfw-dot--sig";
1992
- dots.push(<span key={i} className={cls} />);
1993
- }
1994
- return (
1995
- <span className="tfw-meter" title={`${c} of ${total} signatures`}>
1996
- <span className="tfw-meter__label">SIGS</span>
1997
- <span className="tfw-meter__dots">{dots}</span>
1998
- <span className="tfw-meter__count">{c}/{total}</span>
1999
- </span>
2000
- );
2001
- }
2002
-
2003
2083
  // ── address rendering ────────────────────────────────────────────────────────
2004
2084
  function Address({ value, truncate = 22, onCopy, mono = true }) {
2005
2085
  const handleCopy = (e) => {
@@ -2139,7 +2219,6 @@ Object.assign(window, {
2139
2219
  TFW_countYes: countYes,
2140
2220
  TFW_countNo: countNo,
2141
2221
  TFW_countAbstain: countAbstain,
2142
- TFW_sigCount: sigCount,
2143
2222
  TFW_reviewPassed: reviewPassed,
2144
2223
  TFW_isReady: isReady,
2145
2224
  TFW_displayStatus: displayStatus,
@@ -2151,7 +2230,6 @@ Object.assign(window, {
2151
2230
  TFW_ActionPill: ActionPill,
2152
2231
  TFW_SeverityChip: SeverityChip,
2153
2232
  TFW_VoteDots: VoteDots,
2154
- TFW_SigDots: SigDots,
2155
2233
  TFW_Address: Address,
2156
2234
  TFW_ClassificationTag: ClassificationTag,
2157
2235
  TFW_ConnectionDot: ConnectionDot,
@@ -2171,12 +2249,12 @@ Object.assign(window, {
2171
2249
 
2172
2250
  const {
2173
2251
  TFW_Badge, TFW_StatusBadge, TFW_ActionPill, TFW_SeverityChip,
2174
- TFW_VoteDots, TFW_SigDots, TFW_Address, TFW_ClassificationTag,
2252
+ TFW_VoteDots, TFW_Address, TFW_ClassificationTag,
2175
2253
  TFW_trunc, TFW_fmtDate, TFW_fmtDateShort, TFW_relTime, TFW_reviewCountdown,
2176
- TFW_countYes, TFW_sigCount, TFW_reviewPassed, TFW_isReady, TFW_displayStatus,
2254
+ TFW_countYes, TFW_reviewPassed, TFW_isReady, TFW_displayStatus,
2177
2255
  } = window;
2178
2256
 
2179
- function ProposalCard({ proposal, onOpen, onVote, onSign, onExecute, registry, compact = false }) {
2257
+ function ProposalCard({ proposal, onOpen, onVote, onAnchor, onExecute, registry, meta, compact = false }) {
2180
2258
  const p = proposal;
2181
2259
  const ds = TFW_displayStatus(p);
2182
2260
  const reg = registry.find(e => e.identifier.toLowerCase() === p.lockArgs.toLowerCase());
@@ -2186,16 +2264,14 @@ function ProposalCard({ proposal, onOpen, onVote, onSign, onExecute, registry, c
2186
2264
 
2187
2265
  const review = TFW_reviewCountdown(p.reviewWindowEndsAt);
2188
2266
  const yes = TFW_countYes(p);
2189
- const sigs = TFW_sigCount(p);
2267
+ const hasVoted = meta?.yourPubkey
2268
+ ? (p.votes || []).some(v => v.pubkey === meta.yourPubkey)
2269
+ : false;
2190
2270
 
2191
- // Determine primary action
2271
+ // Validator UI: the card only promotes voting. Advanced transaction steps stay in details.
2192
2272
  let primaryAction = null;
2193
- if ((p.status === "pending-review" || p.status === "voting") && sigs === 0) {
2273
+ if ((p.status === "pending-review" || p.status === "voting") && !hasVoted) {
2194
2274
  primaryAction = { label: "Vote", onClick: () => onVote(p.id) };
2195
- } else if (TFW_isReady(p)) {
2196
- primaryAction = { label: "Execute →", onClick: () => onExecute(p.id), strong: true };
2197
- } else if (TFW_reviewPassed(p) && yes >= 3 && p.status !== "executed" && p.status !== "rejected") {
2198
- primaryAction = { label: "Sign", onClick: () => onSign(p.id), strong: true };
2199
2275
  }
2200
2276
 
2201
2277
  // Card-state class for left accent stripe
@@ -2250,8 +2326,6 @@ function ProposalCard({ proposal, onOpen, onVote, onSign, onExecute, registry, c
2250
2326
 
2251
2327
  <div className="tfw-card__foot">
2252
2328
  <TFW_VoteDots proposal={p} />
2253
- <span className="tfw-card__foot-sep" />
2254
- <TFW_SigDots proposal={p} />
2255
2329
  <span className="tfw-card__spacer" />
2256
2330
 
2257
2331
  {/* registry hint */}
@@ -2297,12 +2371,54 @@ Object.assign(window, { TFW_ProposalCard: ProposalCard });
2297
2371
 
2298
2372
  const {
2299
2373
  TFW_ProposalCard, TFW_Stat, TFW_SectionHead, TFW_EmptyState,
2300
- TFW_Badge, TFW_VoteDots, TFW_SigDots, TFW_StatusBadge, TFW_ActionPill,
2374
+ TFW_Badge, TFW_VoteDots, TFW_StatusBadge, TFW_ActionPill,
2301
2375
  TFW_ClassificationTag, TFW_SeverityChip, TFW_Address,
2302
- TFW_isReady, TFW_displayStatus, TFW_countYes, TFW_sigCount,
2376
+ TFW_isReady, TFW_displayStatus, TFW_countYes,
2303
2377
  TFW_reviewPassed, TFW_relTime, TFW_fmtDateShort, TFW_trunc,
2304
2378
  } = window;
2305
2379
 
2380
+ const TFW_SHANNONS_PER_CKB = 100_000_000;
2381
+
2382
+ function TFW_fmtCkbFromShannons(value) {
2383
+ if (value === undefined || value === null || value === "") return "n/a";
2384
+ const n = Number(value);
2385
+ if (!Number.isFinite(n)) return "n/a";
2386
+ return `${(n / TFW_SHANNONS_PER_CKB).toLocaleString(undefined, {
2387
+ maximumFractionDigits: 4,
2388
+ })} CKB`;
2389
+ }
2390
+
2391
+ function TFW_TreasuryBanner({ treasury, compact = false }) {
2392
+ if (!treasury) return null;
2393
+ const used = typeof treasury.poolUsedPercent === "number" ? treasury.poolUsedPercent : null;
2394
+ const warning = Boolean(treasury.donateRecommended);
2395
+ const donationTarget = treasury.donationAddress || treasury.lockHash;
2396
+ const balance = TFW_fmtCkbFromShannons(treasury.balanceShannons);
2397
+ const capacity = TFW_fmtCkbFromShannons(treasury.poolCapacityShannons);
2398
+ return (
2399
+ <div className={`tfw-treasury${warning ? " tfw-treasury--warn" : ""}${compact ? " tfw-treasury--compact" : ""}`}>
2400
+ <div className="tfw-treasury__main">
2401
+ <div className="tfw-treasury__eyebrow">BLACKLIST POOL</div>
2402
+ <div className="tfw-treasury__line">
2403
+ <strong>{used === null ? "Usage unavailable" : `${used.toFixed(2)}% used`}</strong>
2404
+ <span>Reserve {balance}</span>
2405
+ <span>Pool {capacity}</span>
2406
+ {typeof treasury.liveCellCount === "number" && <span>{treasury.liveCellCount} treasury cell{treasury.liveCellCount === 1 ? "" : "s"}</span>}
2407
+ </div>
2408
+ </div>
2409
+ <div className="tfw-treasury__meter" aria-hidden="true">
2410
+ <span style={{ width: `${Math.min(100, Math.max(0, used || 0))}%` }} />
2411
+ </div>
2412
+ {warning && (
2413
+ <div className="tfw-treasury__donate">
2414
+ <span>Donate CKB to keep registry growth funded</span>
2415
+ <code className="tfw-mono">{donationTarget}</code>
2416
+ </div>
2417
+ )}
2418
+ </div>
2419
+ );
2420
+ }
2421
+
2306
2422
  // ── Overview ──────────────────────────────────────────────────────────────────
2307
2423
  function OverviewPage({ state, actions }) {
2308
2424
  const { proposals, registry, meta } = state;
@@ -2312,11 +2428,7 @@ function OverviewPage({ state, actions }) {
2312
2428
  const expiredReg = registry.filter(e => e.expiresAt && Number(e.expiresAt) <= now).length;
2313
2429
 
2314
2430
  const open = proposals.filter(p => p.status === "pending-review" || p.status === "voting");
2315
- const ready = proposals.filter(TFW_isReady);
2316
- const approved = proposals.filter(p => p.status === "approved" && !TFW_isReady(p));
2317
- const executed = proposals.filter(p => p.status === "executed");
2318
-
2319
- const action = ready.concat(open);
2431
+ const action = open.filter(p => !(p.votes || []).some(v => v.pubkey === meta.yourPubkey));
2320
2432
  const recent = [...proposals]
2321
2433
  .sort((a, b) => b.submittedAt.localeCompare(a.submittedAt))
2322
2434
  .slice(0, 6);
@@ -2328,10 +2440,12 @@ function OverviewPage({ state, actions }) {
2328
2440
  return (
2329
2441
  <div className="tfw-page tfw-page--overview">
2330
2442
 
2443
+ <TFW_TreasuryBanner treasury={meta.treasury} />
2444
+
2331
2445
  {/* HERO STRIP — editorial-sized stats */}
2332
2446
  <div className="tfw-hero">
2333
2447
  <div className="tfw-hero__lead">
2334
- <div className="tfw-hero__eyebrow">GOVERNANCE STATUS · {new Date().toISOString().slice(0,10)}</div>
2448
+ <div className="tfw-hero__eyebrow">VALIDATOR STATUS · {new Date().toISOString().slice(0,10)}</div>
2335
2449
  <h1 className="tfw-hero__title">
2336
2450
  <span className="tfw-hero__title-line1">{activeReg === 0 ? "Registry" : "Blocking"}</span>
2337
2451
  <span className="tfw-hero__title-line2">
@@ -2343,14 +2457,14 @@ function OverviewPage({ state, actions }) {
2343
2457
  <div className="tfw-hero__sub">
2344
2458
  {action.length > 0 ? (
2345
2459
  <>
2346
- <strong>{action.length} item{action.length !== 1 ? "s" : ""}</strong> need
2347
- {action.length === 1 ? "s" : ""} your attention.
2460
+ <strong>{action.length} proposal{action.length !== 1 ? "s" : ""}</strong> need
2461
+ {action.length === 1 ? "s" : ""} your vote.
2348
2462
  {youHaventVoted.length > 0 && (
2349
2463
  <> You haven&rsquo;t voted on <strong>{youHaventVoted.length}</strong>.</>
2350
2464
  )}
2351
2465
  </>
2352
2466
  ) : (
2353
- <>All clear. No proposals require action right now.</>
2467
+ <>All clear. No proposals need your vote right now.</>
2354
2468
  )}
2355
2469
  </div>
2356
2470
  </div>
@@ -2367,25 +2481,15 @@ function OverviewPage({ state, actions }) {
2367
2481
  tone="amber"
2368
2482
  sub={youHaventVoted.length ? `${youHaventVoted.length} need you` : "all caught up"}
2369
2483
  />
2370
- <TFW_Stat
2371
- value={ready.length}
2372
- label="READY TO EXECUTE"
2373
- tone="green"
2374
- />
2375
- <TFW_Stat
2376
- value={approved.length}
2377
- label="AWAITING SIGS"
2378
- tone="amber"
2379
- />
2380
2484
  </div>
2381
2485
  </div>
2382
2486
 
2383
- {/* ACTION REQUIRED */}
2487
+ {/* VOTES NEEDED */}
2384
2488
  {action.length > 0 && (
2385
2489
  <div className="tfw-section">
2386
2490
  <TFW_SectionHead
2387
2491
  eyebrow="◆ PRIORITY"
2388
- title="Action required"
2492
+ title="Votes needed"
2389
2493
  count={action.length}
2390
2494
  action={
2391
2495
  <button type="button" className="tfw-link" onClick={() => actions.setTab("proposals")}>
@@ -2399,33 +2503,10 @@ function OverviewPage({ state, actions }) {
2399
2503
  key={p.id}
2400
2504
  proposal={p}
2401
2505
  registry={registry}
2506
+ meta={meta}
2402
2507
  onOpen={actions.openProposal}
2403
2508
  onVote={actions.openVote}
2404
- onSign={actions.openSign}
2405
- onExecute={actions.openExecute}
2406
- />
2407
- ))}
2408
- </div>
2409
- </div>
2410
- )}
2411
-
2412
- {/* AWAITING SIGS */}
2413
- {approved.length > 0 && (
2414
- <div className="tfw-section">
2415
- <TFW_SectionHead
2416
- eyebrow="◇ MULTISIG"
2417
- title="Awaiting signatures"
2418
- count={approved.length}
2419
- />
2420
- <div className="tfw-cards">
2421
- {approved.map(p => (
2422
- <TFW_ProposalCard
2423
- key={p.id}
2424
- proposal={p}
2425
- registry={registry}
2426
- onOpen={actions.openProposal}
2427
- onVote={actions.openVote}
2428
- onSign={actions.openSign}
2509
+ onAnchor={actions.openAnchor}
2429
2510
  onExecute={actions.openExecute}
2430
2511
  />
2431
2512
  ))}
@@ -2449,9 +2530,10 @@ function OverviewPage({ state, actions }) {
2449
2530
  key={p.id}
2450
2531
  proposal={p}
2451
2532
  registry={registry}
2533
+ meta={meta}
2452
2534
  onOpen={actions.openProposal}
2453
2535
  onVote={actions.openVote}
2454
- onSign={actions.openSign}
2536
+ onAnchor={actions.openAnchor}
2455
2537
  onExecute={actions.openExecute}
2456
2538
  compact
2457
2539
  />
@@ -2505,6 +2587,8 @@ function RegistryPage({ state, actions }) {
2505
2587
 
2506
2588
  return (
2507
2589
  <div className="tfw-page tfw-page--registry">
2590
+ <TFW_TreasuryBanner treasury={meta.treasury} compact />
2591
+
2508
2592
  <div className="tfw-pagehead">
2509
2593
  <div className="tfw-pagehead__col">
2510
2594
  <div className="tfw-pagehead__eyebrow">ON-CHAIN STATE</div>
@@ -2619,17 +2703,15 @@ function RegistryPage({ state, actions }) {
2619
2703
  // ── Proposals ────────────────────────────────────────────────────────────────
2620
2704
  const PROP_FILTERS = [
2621
2705
  { key: "all", label: "All" },
2622
- { key: "action", label: "Needs action" },
2706
+ { key: "action", label: "Needs my vote" },
2623
2707
  { key: "pending-review", label: "Pending review" },
2624
2708
  { key: "voting", label: "Voting" },
2625
- { key: "approved", label: "Awaiting sigs" },
2626
- { key: "ready", label: "Ready" },
2627
2709
  { key: "executed", label: "Executed" },
2628
2710
  { key: "rejected", label: "Rejected" },
2629
2711
  ];
2630
2712
 
2631
2713
  function ProposalsPage({ state, actions }) {
2632
- const { proposals, registry } = state;
2714
+ const { proposals, registry, meta } = state;
2633
2715
  const [filter, setFilter] = React.useState("all");
2634
2716
  const [query, setQuery] = React.useState("");
2635
2717
 
@@ -2638,9 +2720,9 @@ function ProposalsPage({ state, actions }) {
2638
2720
  const ds = TFW_displayStatus(p);
2639
2721
  if (filter === "all") return true;
2640
2722
  if (filter === "action") {
2641
- return p.status === "pending-review" || p.status === "voting" || TFW_isReady(p);
2723
+ return (p.status === "pending-review" || p.status === "voting")
2724
+ && !(p.votes || []).some(v => v.pubkey === meta.yourPubkey);
2642
2725
  }
2643
- if (filter === "ready") return TFW_isReady(p);
2644
2726
  return ds === filter;
2645
2727
  })
2646
2728
  .filter(p => {
@@ -2658,22 +2740,22 @@ function ProposalsPage({ state, actions }) {
2658
2740
  proposals.forEach(p => {
2659
2741
  const ds = TFW_displayStatus(p);
2660
2742
  counts[ds] = (counts[ds] || 0) + 1;
2661
- if (p.status === "pending-review" || p.status === "voting" || TFW_isReady(p)) {
2662
- counts.action = (counts.action || 0) + 1;
2743
+ if (p.status === "pending-review" || p.status === "voting") {
2744
+ if (!(p.votes || []).some(v => v.pubkey === meta.yourPubkey)) {
2745
+ counts.action = (counts.action || 0) + 1;
2746
+ }
2663
2747
  }
2664
2748
  });
2665
2749
  counts.all = proposals.length;
2666
- counts.ready = proposals.filter(TFW_isReady).length;
2667
2750
 
2668
2751
  return (
2669
2752
  <div className="tfw-page tfw-page--proposals">
2670
2753
  <div className="tfw-pagehead">
2671
2754
  <div className="tfw-pagehead__col">
2672
- <div className="tfw-pagehead__eyebrow">GOVERNANCE WORKFLOW</div>
2755
+ <div className="tfw-pagehead__eyebrow">VALIDATOR WORKFLOW</div>
2673
2756
  <h1 className="tfw-pagehead__title">Proposals</h1>
2674
2757
  <div className="tfw-pagehead__sub">
2675
- Every proposal moves through review voting multisig execution.
2676
- 3 of 5 keyholders must approve each on-chain change.
2758
+ Review evidence, cast a vote, and export proposal JSON when another participant needs your vote.
2677
2759
  </div>
2678
2760
  </div>
2679
2761
  <button type="button" className="tfw-btn tfw-btn--primary" onClick={actions.openCreate}>
@@ -2715,9 +2797,10 @@ function ProposalsPage({ state, actions }) {
2715
2797
  key={p.id}
2716
2798
  proposal={p}
2717
2799
  registry={registry}
2800
+ meta={meta}
2718
2801
  onOpen={actions.openProposal}
2719
2802
  onVote={actions.openVote}
2720
- onSign={actions.openSign}
2803
+ onAnchor={actions.openAnchor}
2721
2804
  onExecute={actions.openExecute}
2722
2805
  />
2723
2806
  ))}
@@ -2735,17 +2818,25 @@ Object.assign(window, {
2735
2818
 
2736
2819
  </script>
2737
2820
  <script type="text/babel" data-type="module">
2738
- // Modals: detail (proposal, address) and forms (create, vote, sign, execute, import)
2821
+ // Modals: detail (proposal, address) and forms (create, vote, anchor, execute, import)
2739
2822
 
2740
2823
  const {
2741
2824
  TFW_Badge, TFW_StatusBadge, TFW_ActionPill, TFW_SeverityChip,
2742
- TFW_VoteDots, TFW_SigDots, TFW_Address, TFW_ClassificationTag,
2825
+ TFW_VoteDots, TFW_Address, TFW_ClassificationTag,
2743
2826
  TFW_CodeBlock, TFW_Stat,
2744
2827
  TFW_trunc, TFW_fmtDate, TFW_fmtDateShort, TFW_relTime, TFW_reviewCountdown,
2745
- TFW_countYes, TFW_countNo, TFW_countAbstain, TFW_sigCount,
2828
+ TFW_countYes, TFW_countNo, TFW_countAbstain,
2746
2829
  TFW_reviewPassed, TFW_isReady, TFW_displayStatus,
2747
2830
  } = window;
2748
2831
 
2832
+ function safeUnixToISO(unixSeconds) {
2833
+ try {
2834
+ const num = Number(unixSeconds);
2835
+ if (!num || isNaN(num) || num <= 0) return null;
2836
+ return new Date(num * 1000).toISOString();
2837
+ } catch { return null; }
2838
+ }
2839
+
2749
2840
  // ─── Modal shell ─────────────────────────────────────────────────────────────
2750
2841
  function Modal({ open, onClose, eyebrow, title, subtitle, children, size = "md", footer }) {
2751
2842
  React.useEffect(() => {
@@ -2790,8 +2881,9 @@ function Modal({ open, onClose, eyebrow, title, subtitle, children, size = "md",
2790
2881
  }
2791
2882
 
2792
2883
  // ─── Proposal detail content ─────────────────────────────────────────────────
2793
- function ProposalDetailContent({ proposal, registry, actions, onClose }) {
2884
+ function ProposalDetailContent({ proposal, registry, meta, actions, onClose }) {
2794
2885
  const p = proposal;
2886
+ const [showAdvancedActions, setShowAdvancedActions] = React.useState(false);
2795
2887
  const reg = registry.find(e => e.identifier.toLowerCase() === p.lockArgs.toLowerCase());
2796
2888
  const now = Date.now() / 1000;
2797
2889
  const inReg = !!reg;
@@ -2800,27 +2892,21 @@ function ProposalDetailContent({ proposal, registry, actions, onClose }) {
2800
2892
  const yes = TFW_countYes(p);
2801
2893
  const no = TFW_countNo(p);
2802
2894
  const abs = TFW_countAbstain(p);
2803
- const sigs = TFW_sigCount(p);
2804
2895
  const review = TFW_reviewCountdown(p.reviewWindowEndsAt);
2805
2896
  const ds = TFW_displayStatus(p);
2897
+ const yourPubkey = meta?.yourPubkey;
2898
+ const hasVoted = yourPubkey
2899
+ ? (p.votes || []).some(v => v.pubkey === yourPubkey)
2900
+ : false;
2901
+ const canVote = (p.status === "pending-review" || p.status === "voting") && !hasVoted;
2902
+ const hasAdvancedActions = (
2903
+ (!p.proposalCellTxHash && p.status !== "executed" && p.status !== "rejected") ||
2904
+ TFW_isReady(p)
2905
+ );
2806
2906
 
2807
- // pipeline steps
2808
- const steps = [
2809
- { key: "submit", label: "Submitted", done: true },
2810
- { key: "review", label: "Review", done: TFW_reviewPassed(p), active: !TFW_reviewPassed(p) },
2811
- { key: "vote", label: "Vote", done: yes >= 3, active: TFW_reviewPassed(p) && yes < 3 },
2812
- { key: "sign", label: "Multisig", done: sigs >= 3, active: TFW_reviewPassed(p) && yes >= 3 && sigs < 3 },
2813
- { key: "execute", label: "Executed", done: p.status === "executed", active: TFW_isReady(p) },
2814
- ];
2815
- if (p.status === "rejected") {
2816
- steps[2] = { key: "vote", label: "Vote", done: false, rejected: true };
2817
- steps[3] = { key: "sign", label: "Multisig", done: false };
2818
- steps[4] = { key: "execute", label: "Rejected", done: false, rejected: true };
2819
- }
2820
-
2821
- // primary actions
2907
+ // Primary validator actions
2822
2908
  const buttons = [];
2823
- if ((p.status === "pending-review" || p.status === "voting") && sigs === 0) {
2909
+ if (canVote) {
2824
2910
  buttons.push(
2825
2911
  <button key="vote" type="button" className="tfw-btn tfw-btn--primary"
2826
2912
  onClick={() => { onClose(); actions.openVote(p.id); }}>
@@ -2828,50 +2914,16 @@ function ProposalDetailContent({ proposal, registry, actions, onClose }) {
2828
2914
  </button>
2829
2915
  );
2830
2916
  }
2831
- if (TFW_reviewPassed(p) && yes >= 3 && p.status !== "executed" && p.status !== "rejected" && !TFW_isReady(p)) {
2832
- buttons.push(
2833
- <button key="sign" type="button" className="tfw-btn tfw-btn--accent"
2834
- onClick={() => { onClose(); actions.openSign(p.id); }}>
2835
- Sign
2836
- </button>
2837
- );
2838
- }
2839
- if (TFW_isReady(p)) {
2840
- buttons.push(
2841
- <button key="exec" type="button" className="tfw-btn tfw-btn--accent"
2842
- onClick={() => { onClose(); actions.openExecute(p.id); }}>
2843
- Build &amp; download TX
2844
- </button>
2845
- );
2846
- }
2847
2917
  buttons.push(
2848
2918
  <button key="export" type="button" className="tfw-btn tfw-btn--ghost"
2849
2919
  onClick={() => actions.exportProposal(p)}>
2850
- Export JSON
2920
+ Export JSON
2851
2921
  </button>
2852
2922
  );
2853
2923
 
2854
2924
  return (
2855
2925
  <div className="tfw-detail">
2856
2926
 
2857
- {/* Pipeline */}
2858
- <div className="tfw-pipeline">
2859
- {steps.map((s, i) => (
2860
- <React.Fragment key={s.key}>
2861
- <div className={`tfw-pipe-step${s.done ? " tfw-pipe-step--done" : ""}${s.active ? " tfw-pipe-step--active" : ""}${s.rejected ? " tfw-pipe-step--rejected" : ""}`}>
2862
- <div className="tfw-pipe-step__circle">
2863
- {s.done ? "✓" : s.rejected ? "✕" : i + 1}
2864
- </div>
2865
- <div className="tfw-pipe-step__label">{s.label}</div>
2866
- </div>
2867
- {i < steps.length - 1 && (
2868
- <div className={`tfw-pipe-conn${s.done ? " tfw-pipe-conn--done" : ""}`} />
2869
- )}
2870
- </React.Fragment>
2871
- ))}
2872
- </div>
2873
-
2874
- {/* Top dossier */}
2875
2927
  <div className="tfw-dossier">
2876
2928
  <div className="tfw-dossier__row">
2877
2929
  <div className="tfw-dossier__label">ACTION</div>
@@ -2895,12 +2947,12 @@ function ProposalDetailContent({ proposal, registry, actions, onClose }) {
2895
2947
  {!inReg && <span className="tfw-dossier__none">Not on chain</span>}
2896
2948
  {inReg && isExpReg && (
2897
2949
  <TFW_Badge tone="red" size="sm">
2898
- expired — {TFW_fmtDateShort(new Date(Number(reg.expiresAt) * 1000).toISOString())}
2950
+ expired — {TFW_fmtDateShort(safeUnixToISO(reg.expiresAt))}
2899
2951
  </TFW_Badge>
2900
2952
  )}
2901
2953
  {inReg && !isExpReg && reg.expiresAt && (
2902
2954
  <TFW_Badge tone="green" size="sm">
2903
- active — expires {TFW_fmtDateShort(new Date(Number(reg.expiresAt) * 1000).toISOString())}
2955
+ active — expires {TFW_fmtDateShort(safeUnixToISO(reg.expiresAt))}
2904
2956
  </TFW_Badge>
2905
2957
  )}
2906
2958
  {inReg && !isExpReg && !reg.expiresAt && (
@@ -2924,18 +2976,16 @@ function ProposalDetailContent({ proposal, registry, actions, onClose }) {
2924
2976
  <div className="tfw-dossier__val tfw-mono">{TFW_fmtDate(p.submittedAt)} ({TFW_relTime(p.submittedAt)})</div>
2925
2977
  </div>
2926
2978
  <div className="tfw-dossier__row">
2927
- <div className="tfw-dossier__label">REVIEW ENDS</div>
2979
+ <div className="tfw-dossier__label">REVIEW</div>
2928
2980
  <div className="tfw-dossier__val tfw-mono">
2929
- {TFW_fmtDate(p.reviewWindowEndsAt)}
2930
- {!review.done && <span style={{ color: "var(--c-amber)", marginLeft: 8 }}>({review.text} left)</span>}
2931
- {review.done && <span style={{ color: "var(--c-green)", marginLeft: 8 }}>(complete)</span>}
2981
+ {review.done ? "complete" : `${review.text} left`}
2932
2982
  </div>
2933
2983
  </div>
2934
2984
  {p.expiresAt && p.expiresAt !== "0" && (
2935
2985
  <div className="tfw-dossier__row">
2936
2986
  <div className="tfw-dossier__label">ENTRY EXPIRES</div>
2937
2987
  <div className="tfw-dossier__val tfw-mono">
2938
- {TFW_fmtDate(new Date(Number(p.expiresAt) * 1000).toISOString())}
2988
+ {TFW_fmtDate(safeUnixToISO(p.expiresAt))}
2939
2989
  </div>
2940
2990
  </div>
2941
2991
  )}
@@ -2994,44 +3044,36 @@ function ProposalDetailContent({ proposal, registry, actions, onClose }) {
2994
3044
  )}
2995
3045
  </div>
2996
3046
 
2997
- {/* Signatures */}
2998
- <div className="tfw-detail-sec">
2999
- <div className="tfw-detail-sec__head">
3000
- SIGNATURES <span className="tfw-detail-sec__meta">{sigs} / 3 required</span>
3001
- </div>
3002
- {sigs === 0 ? (
3003
- <div className="tfw-detail-sec__empty">No signatures yet.</div>
3004
- ) : (
3005
- <div className="tfw-list">
3006
- {p.signatures.map((s, i) => (
3007
- <div key={i} className="tfw-list-row">
3008
- <div className="tfw-list-row__sigidx tfw-mono">SIGNER #{s.signerIndex}</div>
3009
- <code className="tfw-mono tfw-list-row__id">{TFW_trunc(s.signature, 34)}</code>
3010
- <span className="tfw-list-row__time">{TFW_fmtDate(s.timestamp)}</span>
3011
- </div>
3012
- ))}
3013
- </div>
3014
- )}
3015
- </div>
3016
-
3017
- {/* CLI commands */}
3018
- <div className="tfw-detail-sec">
3019
- <div className="tfw-detail-sec__head">CLI COMMANDS</div>
3020
- {(p.status === "pending-review" || p.status === "voting") && (
3021
- <TFW_CodeBlock label="Vote on this proposal">{`ckb-firewall vote --proposal ${p.id}`}</TFW_CodeBlock>
3022
- )}
3023
- {(p.status === "approved" || (TFW_reviewPassed(p) && yes >= 3)) && p.status !== "executed" && (
3024
- <TFW_CodeBlock label="Sign (3-of-5 required)">{`ckb-firewall sign --proposal ${p.id}`}</TFW_CodeBlock>
3025
- )}
3026
- {TFW_isReady(p) && (
3027
- <TFW_CodeBlock label="Execute on-chain">{`ckb-firewall execute --proposal ${p.id}`}</TFW_CodeBlock>
3028
- )}
3029
- <TFW_CodeBlock label="Export JSON">{`ckb-firewall export --proposal ${p.id} --out proposal-${p.id}.json`}</TFW_CodeBlock>
3030
- <TFW_CodeBlock label="Check this address in registry">{`ckb-firewall check --lock-args ${p.lockArgs}`}</TFW_CodeBlock>
3031
- </div>
3032
-
3033
3047
  {/* Action bar */}
3034
3048
  <div className="tfw-detail-actions">{buttons}</div>
3049
+
3050
+ {hasAdvancedActions && (
3051
+ <div className="tfw-detail-sec">
3052
+ <button
3053
+ type="button"
3054
+ className="tfw-link"
3055
+ onClick={() => setShowAdvancedActions(v => !v)}
3056
+ >
3057
+ {showAdvancedActions ? "Hide advanced actions" : "Show advanced actions"}
3058
+ </button>
3059
+ {showAdvancedActions && (
3060
+ <div className="tfw-advanced-actions">
3061
+ {!p.proposalCellTxHash && p.status !== "executed" && p.status !== "rejected" && (
3062
+ <button type="button" className="tfw-btn tfw-btn--ghost"
3063
+ onClick={() => { onClose(); actions.openAnchor(p.id); }}>
3064
+ Anchor proposal
3065
+ </button>
3066
+ )}
3067
+ {TFW_isReady(p) && (
3068
+ <button type="button" className="tfw-btn tfw-btn--ghost"
3069
+ onClick={() => { onClose(); actions.openExecute(p.id); }}>
3070
+ Build execute transaction
3071
+ </button>
3072
+ )}
3073
+ </div>
3074
+ )}
3075
+ </div>
3076
+ )}
3035
3077
  </div>
3036
3078
  );
3037
3079
  }
@@ -3136,14 +3178,14 @@ function SecurityNote({ kind = "info", children }) {
3136
3178
  }
3137
3179
 
3138
3180
  // ─── Create proposal form ───────────────────────────────────────────────────
3139
- function CreateForm({ onSubmit, onClose }) {
3181
+ function CreateForm({ meta, onSubmit, onClose }) {
3140
3182
  const [action, setAction] = React.useState("add");
3141
3183
  const [lockArgs, setLockArgs] = React.useState("");
3142
3184
  const [classification, setClassification] = React.useState("theft");
3143
3185
  const [severity, setSeverity] = React.useState("critical");
3144
3186
  const [evidence, setEvidence] = React.useState("");
3145
3187
  const [rationale, setRationale] = React.useState("");
3146
- const [proposer, setProposer] = React.useState("");
3188
+ const [proposer, setProposer] = React.useState(meta?.proposerName || "");
3147
3189
  const [expires, setExpires] = React.useState("0");
3148
3190
  const [errors, setErrors] = React.useState({});
3149
3191
  const [submitting, setSubmitting] = React.useState(false);
@@ -3210,7 +3252,7 @@ function CreateForm({ onSubmit, onClose }) {
3210
3252
  </div>
3211
3253
  </FormField>
3212
3254
 
3213
- <FormField label="Lock args" hint="0x-prefixed hex" error={errors.lockArgs}>
3255
+ <FormField label="Lock args" hint="hex identifier of the CKB lock script to blacklist" error={errors.lockArgs}>
3214
3256
  <input
3215
3257
  type="text"
3216
3258
  className="tfw-input tfw-mono"
@@ -3273,13 +3315,20 @@ function CreateForm({ onSubmit, onClose }) {
3273
3315
  />
3274
3316
  </FormField>
3275
3317
  {action === "add" && (
3276
- <FormField label="Expires at" hint="unix timestamp, 0 = never">
3318
+ <FormField
3319
+ label="Expires at"
3320
+ hint={expires && expires !== "0"
3321
+ ? `Unix timestamp: ${expires}`
3322
+ : "no expiry — entry is permanent"}
3323
+ >
3277
3324
  <input
3278
- type="number"
3279
- className="tfw-input tfw-mono"
3280
- value={expires}
3281
- min="0"
3282
- onChange={e => setExpires(e.target.value)}
3325
+ type="datetime-local"
3326
+ className="tfw-input"
3327
+ value={safeUnixToISO(expires)?.slice(0, 16) ?? ""}
3328
+ min={new Date().toISOString().slice(0, 16)}
3329
+ onChange={e => setExpires(e.target.value
3330
+ ? String(Math.floor(new Date(e.target.value).getTime() / 1000))
3331
+ : "0")}
3283
3332
  />
3284
3333
  </FormField>
3285
3334
  )}
@@ -3342,8 +3391,8 @@ function VoteForm({ proposal, meta, onSubmit, onClose }) {
3342
3391
  return (
3343
3392
  <div className="tfw-form">
3344
3393
  <SecurityNote kind="warn">
3345
- Your private key is posted over loopback only and zeroed immediately after the cryptographic operation.
3346
- It is never stored, logged, or forwarded beyond localhost.
3394
+ Only keys in the registry governance voter set can vote. This is not a public user vote.
3395
+ The private key is posted over loopback only, zeroed immediately after signing, and never stored.
3347
3396
  </SecurityNote>
3348
3397
 
3349
3398
  <FormField label="Your vote">
@@ -3395,109 +3444,208 @@ function VoteForm({ proposal, meta, onSubmit, onClose }) {
3395
3444
  );
3396
3445
  }
3397
3446
 
3398
- // ─── Sign form ───────────────────────────────────────────────────────────────
3399
- function SignForm({ proposal, meta, onSubmit, onClose }) {
3400
- const [idx, setIdx] = React.useState(String(meta.yourSignerIndex || 0));
3401
- const [pk, setPk] = React.useState("");
3447
+ // ─── Anchor form ─────────────────────────────────────────────────────────────
3448
+ function AnchorForm({ proposal, meta, onSubmit, onClose }) {
3449
+ const [toAddress, setToAddress] = React.useState("");
3450
+ const [proposalTx, setProposalTx] = React.useState(proposal.proposalCellTxHash || "");
3451
+ const [proposalIndex, setProposalIndex] = React.useState(String(proposal.proposalCellIndex ?? 0));
3452
+ // If already anchored, skip to step 2
3453
+ const [step, setStep] = React.useState(proposal.proposalCellTxHash ? 2 : 1);
3454
+ const [command, setCommand] = React.useState(null);
3455
+ const [recordResult, setRecordResult] = React.useState(null);
3402
3456
  const [error, setError] = React.useState("");
3403
3457
  const [submitting, setSubmitting] = React.useState(false);
3404
- const [success, setSuccess] = React.useState(false);
3458
+ const treasuryBacked = Boolean(meta?.treasury);
3405
3459
 
3406
- const submit = () => {
3460
+ const generateCommand = () => {
3407
3461
  setError("");
3408
- const i = Number(idx);
3409
- if (!Number.isInteger(i) || i < 0 || i > 4) {
3410
- setError("Signer index must be 0–4");
3462
+ if (!treasuryBacked && !toAddress.trim()) {
3463
+ setError("Payment recipient address is required for non-treasury registries.");
3411
3464
  return;
3412
3465
  }
3413
- if ((proposal.signatures || []).some(s => s.signerIndex === i)) {
3414
- setError("Signer #" + i + " has already signed.");
3466
+ setSubmitting(true);
3467
+ fetch('/api/anchor', { method: 'POST', headers: { 'Content-Type': 'application/json' },
3468
+ body: JSON.stringify({
3469
+ proposalId: proposal.id,
3470
+ toAddress: treasuryBacked ? undefined : toAddress.trim(),
3471
+ }) })
3472
+ .then(r => r.json())
3473
+ .then(d => {
3474
+ setSubmitting(false);
3475
+ if (!d.ok) { setError(d.error || 'Server error'); return; }
3476
+ setCommand(d.command);
3477
+ if (d.proposal) onSubmit(d.proposal);
3478
+ setStep(2);
3479
+ })
3480
+ .catch(e => { setSubmitting(false); setError(e.message); });
3481
+ };
3482
+
3483
+ const recordAnchor = () => {
3484
+ setError("");
3485
+ if (!/^0x[0-9a-fA-F]{64}$/.test(proposalTx.trim())) {
3486
+ setError("Proposal tx must be 0x + 64 hex chars");
3415
3487
  return;
3416
3488
  }
3417
- if (!/^0x[0-9a-fA-F]{64}$/.test(pk.trim())) {
3418
- setError("Private key must be 0x + 64 hex chars");
3489
+ if (!/^\d+$/.test(proposalIndex.trim())) {
3490
+ setError("Proposal index must be a non-negative integer");
3419
3491
  return;
3420
3492
  }
3421
3493
  setSubmitting(true);
3422
- fetch('/api/sign', { method: 'POST', headers: { 'Content-Type': 'application/json' },
3423
- body: JSON.stringify({ proposalId: proposal.id, signerIndex: i, privateKey: pk.trim() }) })
3494
+ fetch('/api/anchor', { method: 'POST', headers: { 'Content-Type': 'application/json' },
3495
+ body: JSON.stringify({
3496
+ proposalId: proposal.id,
3497
+ proposalTx: proposalTx.trim(),
3498
+ proposalIndex: proposalIndex.trim(),
3499
+ toAddress: treasuryBacked ? undefined : toAddress.trim(),
3500
+ }) })
3424
3501
  .then(r => r.json())
3425
3502
  .then(d => {
3426
3503
  setSubmitting(false);
3427
- setPk("");
3428
3504
  if (!d.ok) { setError(d.error || 'Server error'); return; }
3429
- onSubmit({ proposalId: proposal.id, signerIndex: i, signature: d.signature || '', timestamp: new Date().toISOString() });
3430
- setSuccess(true);
3431
- setTimeout(onClose, 1100);
3505
+ setRecordResult(d);
3506
+ onSubmit(d.proposal);
3432
3507
  })
3433
- .catch(e => { setSubmitting(false); setPk(""); setError(e.message); });
3508
+ .catch(e => { setSubmitting(false); setError(e.message); });
3434
3509
  };
3435
3510
 
3436
- if (success) {
3437
- return (
3438
- <div className="tfw-form-success">
3439
- <div className="tfw-form-success__glyph">✓</div>
3440
- <div className="tfw-form-success__title">Signature added</div>
3441
- <div className="tfw-form-success__sub">Signature recorded.</div>
3442
- </div>
3443
- );
3444
- }
3445
-
3446
3511
  return (
3447
3512
  <div className="tfw-form">
3448
- <SecurityNote kind="warn">
3449
- Your private key is posted over loopback only and zeroed immediately after the cryptographic operation. It is never stored, logged, or forwarded beyond localhost.
3513
+ <SecurityNote kind="info">
3514
+ Anchoring writes a proposal cell on-chain that enforces the 72-hour review window via CKB consensus.
3515
+ {!treasuryBacked && " This registry has no treasury — you'll need a recipient address for the proposal cell deposit."}
3450
3516
  </SecurityNote>
3451
3517
 
3452
- <FormField label="Signer index" hint="0–4, your slot in the 5-key governance set">
3453
- <input
3454
- type="number"
3455
- className="tfw-input tfw-mono"
3456
- min="0"
3457
- max="4"
3458
- value={idx}
3459
- onChange={e => setIdx(e.target.value)}
3460
- />
3461
- </FormField>
3518
+ {!treasuryBacked && step === 1 && (
3519
+ <FormField label="Address for proposal-cell deposit" hint="who pays the on-chain capacity">
3520
+ <input
3521
+ className="tfw-input tfw-mono"
3522
+ value={toAddress}
3523
+ onChange={e => setToAddress(e.target.value)}
3524
+ placeholder="ckt1..."
3525
+ autoComplete="off"
3526
+ spellCheck={false}
3527
+ />
3528
+ </FormField>
3529
+ )}
3462
3530
 
3463
- <FormField label="Private key" hint="32 bytes, hex" error={error}>
3464
- <input
3465
- type="password"
3466
- className="tfw-input tfw-mono"
3467
- value={pk}
3468
- onChange={e => setPk(e.target.value)}
3469
- placeholder="0x… (64 hex chars)"
3470
- autoComplete="off"
3471
- spellCheck={false}
3472
- />
3473
- </FormField>
3531
+ {/* ── Step 1: generate the CLI command ── */}
3532
+ <div className="tfw-step">
3533
+ <div className="tfw-step__label">Step 1 — Generate the anchor command</div>
3534
+ {command ? (
3535
+ <>
3536
+ <TFW_CodeBlock label={treasuryBacked ? "Run in your terminal" : "Run in your terminal (ckb-cli)"}>{command}</TFW_CodeBlock>
3537
+ <div className="tfw-field__hint">
3538
+ Copy and run the command above. Wait until the transaction is accepted on-chain, then complete Step 2.
3539
+ </div>
3540
+ </>
3541
+ ) : step === 1 ? (
3542
+ <>
3543
+ <div className="tfw-field__hint">
3544
+ This generates the CLI command to create the proposal cell. You will run it in a terminal.
3545
+ </div>
3546
+ <button
3547
+ type="button"
3548
+ className="tfw-btn tfw-btn--accent"
3549
+ style={{ alignSelf: "flex-start" }}
3550
+ onClick={generateCommand}
3551
+ disabled={submitting}
3552
+ >
3553
+ {submitting ? "Preparing…" : "Generate anchor command"}
3554
+ </button>
3555
+ </>
3556
+ ) : (
3557
+ <div className="tfw-field__hint">
3558
+ Already done.{" "}
3559
+ <button type="button" className="tfw-link" onClick={() => setStep(1)}>Re-generate</button>
3560
+ </div>
3561
+ )}
3562
+ </div>
3563
+
3564
+ {/* ── Step 2: record the accepted tx hash ── */}
3565
+ <div className="tfw-step">
3566
+ <div className="tfw-step__label">Step 2 — Record the accepted transaction</div>
3567
+ {recordResult?.anchorVerified ? (
3568
+ <div className="tfw-exec-preview">
3569
+ <div className="tfw-exec-preview__row">
3570
+ <span>Verified cell</span>
3571
+ <code className="tfw-mono" style={{ fontSize: "11px" }}>
3572
+ {TFW_trunc(recordResult.proposal.proposalCellTxHash, 22)}:{recordResult.proposal.proposalCellIndex ?? 0}
3573
+ </code>
3574
+ </div>
3575
+ <div className="tfw-exec-preview__row">
3576
+ <span>Proposal hash</span>
3577
+ <code className="tfw-mono" style={{ fontSize: "11px" }}>{TFW_trunc(recordResult.proposalDataHash, 30)}</code>
3578
+ </div>
3579
+ </div>
3580
+ ) : (
3581
+ <>
3582
+ <div className="tfw-field__hint">
3583
+ After the anchor transaction is accepted, paste the tx hash below to verify and record it.
3584
+ </div>
3585
+ <div style={{ display: "grid", gridTemplateColumns: "minmax(0,1fr) 120px", gap: 12 }}>
3586
+ <FormField label="Accepted anchor tx hash">
3587
+ <input
3588
+ className="tfw-input tfw-mono"
3589
+ value={proposalTx}
3590
+ onChange={e => setProposalTx(e.target.value)}
3591
+ placeholder="0x..."
3592
+ autoComplete="off"
3593
+ spellCheck={false}
3594
+ />
3595
+ </FormField>
3596
+ <FormField label="Output index">
3597
+ <input
3598
+ className="tfw-input tfw-mono"
3599
+ value={proposalIndex}
3600
+ onChange={e => setProposalIndex(e.target.value)}
3601
+ placeholder="0"
3602
+ inputMode="numeric"
3603
+ />
3604
+ </FormField>
3605
+ </div>
3606
+ <button
3607
+ type="button"
3608
+ className="tfw-btn tfw-btn--accent"
3609
+ style={{ alignSelf: "flex-start" }}
3610
+ onClick={recordAnchor}
3611
+ disabled={submitting || step === 1 && !command}
3612
+ >
3613
+ {submitting ? "Verifying…" : "Verify & record anchor"}
3614
+ </button>
3615
+ </>
3616
+ )}
3617
+ </div>
3618
+
3619
+ {error && <div className="tfw-field__err" style={{ marginTop: 12 }}>{error}</div>}
3474
3620
 
3475
3621
  <div className="tfw-form-actions">
3476
- <button type="button" className="tfw-btn tfw-btn--ghost" onClick={onClose}>Cancel</button>
3477
- <button
3478
- type="button"
3479
- className="tfw-btn tfw-btn--accent"
3480
- onClick={submit}
3481
- disabled={submitting}
3482
- >
3483
- {submitting ? "Signing…" : "Sign proposal"}
3484
- </button>
3622
+ <button type="button" className="tfw-btn tfw-btn--ghost" onClick={onClose}>Close</button>
3485
3623
  </div>
3486
3624
  </div>
3487
3625
  );
3488
3626
  }
3489
3627
 
3490
3628
  // ─── Execute form ────────────────────────────────────────────────────────────
3491
- function ExecuteForm({ proposal, onSubmit, onClose }) {
3629
+ function ExecuteForm({ proposal, meta, onSubmit, onClose }) {
3630
+ const [proposalTx, setProposalTx] = React.useState(proposal.proposalCellTxHash || "");
3631
+ const [proposalIndex, setProposalIndex] = React.useState(String(proposal.proposalCellIndex ?? 0));
3492
3632
  const [submitting, setSubmitting] = React.useState(false);
3493
3633
  const [success, setSuccess] = React.useState(false);
3494
3634
  const [error, setError] = React.useState("");
3495
3635
 
3496
3636
  const submit = () => {
3497
3637
  setError("");
3638
+ if (!/^0x[0-9a-fA-F]{64}$/.test(proposalTx.trim())) {
3639
+ setError("Proposal tx must be 0x + 64 hex chars. Anchor the proposal cell first.");
3640
+ return;
3641
+ }
3642
+ if (!/^\d+$/.test(proposalIndex.trim())) {
3643
+ setError("Proposal index must be a non-negative integer.");
3644
+ return;
3645
+ }
3498
3646
  setSubmitting(true);
3499
3647
  fetch('/api/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' },
3500
- body: JSON.stringify({ proposalId: proposal.id }) })
3648
+ body: JSON.stringify({ proposalId: proposal.id, proposalTx: proposalTx.trim(), proposalIndex: proposalIndex.trim() }) })
3501
3649
  .then(r => r.json())
3502
3650
  .then(d => {
3503
3651
  setSubmitting(false);
@@ -3508,7 +3656,7 @@ function ExecuteForm({ proposal, onSubmit, onClose }) {
3508
3656
  a.href = url; a.download = d.filename || ('gov_execute_tx_' + proposal.id + '.json');
3509
3657
  document.body.appendChild(a); a.click();
3510
3658
  document.body.removeChild(a); URL.revokeObjectURL(url);
3511
- onSubmit(proposal.id);
3659
+ onSubmit(d);
3512
3660
  setSuccess(true);
3513
3661
  setTimeout(onClose, 1500);
3514
3662
  })
@@ -3520,10 +3668,10 @@ function ExecuteForm({ proposal, onSubmit, onClose }) {
3520
3668
  <div className="tfw-form-success">
3521
3669
  <div className="tfw-form-success__glyph">↓</div>
3522
3670
  <div className="tfw-form-success__title">Transaction downloaded</div>
3523
- <div className="tfw-form-success__sub">
3524
- Broadcast <code className="tfw-mono">gov_execute_tx_{proposal.id}.json</code> via ckb-cli
3525
- or your wallet to submit it to the network.
3671
+ <div className="tfw-form-success__sub" style={{ marginBottom: 16 }}>
3672
+ Broadcast <code className="tfw-mono">gov_execute_tx_{proposal.id}.json</code> to submit it to the CKB network:
3526
3673
  </div>
3674
+ <TFW_CodeBlock label="Submit via ckb-cli">{`ckb-cli tx send --tx-file gov_execute_tx_${proposal.id}.json`}</TFW_CodeBlock>
3527
3675
  </div>
3528
3676
  );
3529
3677
  }
@@ -3531,10 +3679,31 @@ function ExecuteForm({ proposal, onSubmit, onClose }) {
3531
3679
  return (
3532
3680
  <div className="tfw-form">
3533
3681
  <SecurityNote kind="tip">
3534
- This verifies all 3 signatures, builds the on-chain transaction JSON, and downloads it.
3682
+ This verifies the anchored proposal cell and validator votes, builds the on-chain transaction JSON, and downloads it.
3535
3683
  Submit it via <code className="tfw-mono">ckb-cli</code> or a CKB wallet.
3536
3684
  </SecurityNote>
3537
3685
 
3686
+ <div style={{ display: "grid", gridTemplateColumns: "minmax(0,1fr) 120px", gap: 12 }}>
3687
+ <FormField label="Proposal tx hash">
3688
+ <input
3689
+ className="tfw-input tfw-mono"
3690
+ value={proposalTx}
3691
+ onChange={e => setProposalTx(e.target.value)}
3692
+ placeholder="0x..."
3693
+ autoComplete="off"
3694
+ spellCheck={false}
3695
+ />
3696
+ </FormField>
3697
+ <FormField label="Output index">
3698
+ <input
3699
+ className="tfw-input tfw-mono"
3700
+ value={proposalIndex}
3701
+ onChange={e => setProposalIndex(e.target.value)}
3702
+ inputMode="numeric"
3703
+ />
3704
+ </FormField>
3705
+ </div>
3706
+
3538
3707
  <div className="tfw-exec-preview">
3539
3708
  <div className="tfw-exec-preview__row">
3540
3709
  <span>Action</span>
@@ -3545,8 +3714,8 @@ function ExecuteForm({ proposal, onSubmit, onClose }) {
3545
3714
  <code className="tfw-mono" style={{ fontSize: "11px" }}>{TFW_trunc(proposal.lockArgs, 28)}</code>
3546
3715
  </div>
3547
3716
  <div className="tfw-exec-preview__row">
3548
- <span>Signatures verified</span>
3549
- <span className="tfw-mono">{TFW_sigCount(proposal)} / 3</span>
3717
+ <span>Yes votes</span>
3718
+ <span className="tfw-mono">{TFW_countYes(proposal)} / {meta?.threshold || 3}</span>
3550
3719
  </div>
3551
3720
  <div className="tfw-exec-preview__row">
3552
3721
  <span>Output file</span>
@@ -3610,7 +3779,7 @@ function ImportForm({ onSubmit, onClose }) {
3610
3779
  <div className="tfw-form-success">
3611
3780
  <div className="tfw-form-success__glyph">↥</div>
3612
3781
  <div className="tfw-form-success__title">Proposal imported</div>
3613
- <div className="tfw-form-success__sub">Merged any new votes or signatures.</div>
3782
+ <div className="tfw-form-success__sub">Merged any new votes.</div>
3614
3783
  </div>
3615
3784
  );
3616
3785
  }
@@ -3619,7 +3788,7 @@ function ImportForm({ onSubmit, onClose }) {
3619
3788
  <div className="tfw-form">
3620
3789
  <SecurityNote kind="info">
3621
3790
  Paste or upload a proposal JSON exported by another governance participant.
3622
- Votes and signatures will be merged.
3791
+ Votes will be merged.
3623
3792
  </SecurityNote>
3624
3793
 
3625
3794
  <FormField label="Upload JSON file">
@@ -3663,7 +3832,7 @@ Object.assign(window, {
3663
3832
  TFW_AddressDetailContent: AddressDetailContent,
3664
3833
  TFW_CreateForm: CreateForm,
3665
3834
  TFW_VoteForm: VoteForm,
3666
- TFW_SignForm: SignForm,
3835
+ TFW_AnchorForm: AnchorForm,
3667
3836
  TFW_ExecuteForm: ExecuteForm,
3668
3837
  TFW_ImportForm: ImportForm,
3669
3838
  });
@@ -3676,7 +3845,7 @@ const {
3676
3845
  TFW_PROPOSALS, TFW_REGISTRY_ENTRIES, TFW_META,
3677
3846
  TFW_OverviewPage, TFW_RegistryPage, TFW_ProposalsPage,
3678
3847
  TFW_Modal, TFW_ProposalDetailContent, TFW_AddressDetailContent,
3679
- TFW_CreateForm, TFW_VoteForm, TFW_SignForm, TFW_ExecuteForm, TFW_ImportForm,
3848
+ TFW_CreateForm, TFW_VoteForm, TFW_AnchorForm, TFW_ExecuteForm, TFW_ImportForm,
3680
3849
  TFW_ConnectionDot, TFW_Badge, TFW_StatusBadge, TFW_ActionPill, TFW_Toast,
3681
3850
  TFW_trunc, TFW_isReady, TFW_displayStatus,
3682
3851
  } = window;
@@ -3698,18 +3867,8 @@ function reducer(state, action) {
3698
3867
  });
3699
3868
  return { ...state, proposals };
3700
3869
  }
3701
- case "ADD_SIG": {
3702
- const proposals = state.proposals.map(p => {
3703
- if (p.id !== action.proposalId) return p;
3704
- const signatures = [...(p.signatures || []), {
3705
- signerIndex: action.signerIndex,
3706
- signature: action.signature,
3707
- timestamp: action.timestamp
3708
- }];
3709
- let status = p.status;
3710
- if (status === "voting") status = "approved";
3711
- return { ...p, signatures, status };
3712
- });
3870
+ case "UPDATE_PROPOSAL": {
3871
+ const proposals = state.proposals.map(p => p.id === action.proposal.id ? action.proposal : p);
3713
3872
  return { ...state, proposals };
3714
3873
  }
3715
3874
  case "EXECUTE": {
@@ -3740,20 +3899,23 @@ function reducer(state, action) {
3740
3899
  if (!existing) {
3741
3900
  return { ...state, proposals: [action.proposal, ...state.proposals] };
3742
3901
  }
3743
- // merge votes/sigs
3902
+ // merge votes
3744
3903
  const votes = [
3745
3904
  ...(existing.votes || []),
3746
3905
  ...(action.proposal.votes || []).filter(v =>
3747
3906
  !(existing.votes || []).some(ev => ev.pubkey === v.pubkey)
3748
3907
  ),
3749
3908
  ];
3750
- const signatures = [
3751
- ...(existing.signatures || []),
3752
- ...(action.proposal.signatures || []).filter(s =>
3753
- !(existing.signatures || []).some(es => es.signerIndex === s.signerIndex)
3754
- ),
3755
- ];
3756
- const proposals = state.proposals.map(p => p.id === existing.id ? { ...p, votes, signatures } : p);
3909
+ const proposals = state.proposals.map(p => p.id === existing.id ? {
3910
+ ...p,
3911
+ ...action.proposal,
3912
+ votes,
3913
+ signatures: [],
3914
+ proposalDataHash: p.proposalDataHash || action.proposal.proposalDataHash,
3915
+ reviewDelayMs: p.reviewDelayMs || action.proposal.reviewDelayMs,
3916
+ proposalCellTxHash: p.proposalCellTxHash || action.proposal.proposalCellTxHash,
3917
+ proposalCellIndex: p.proposalCellIndex ?? action.proposal.proposalCellIndex,
3918
+ } : p);
3757
3919
  return { ...state, proposals };
3758
3920
  }
3759
3921
  case "SET_DATA":
@@ -3762,7 +3924,6 @@ function reducer(state, action) {
3762
3924
  proposals: action.proposals,
3763
3925
  registry: action.registry,
3764
3926
  meta: { ...action.meta,
3765
- yourSignerIndex: state.meta.yourSignerIndex || 0,
3766
3927
  yourPubkey: state.meta.yourPubkey },
3767
3928
  };
3768
3929
  default:
@@ -3801,6 +3962,9 @@ function App() {
3801
3962
  meta: Object.assign({}, d.meta || {}, {
3802
3963
  registryTxHash: d.registry && d.registry.txHash,
3803
3964
  registryError: d.registry && d.registry.error,
3965
+ treasury: (d.registry && d.registry.treasury) || null,
3966
+ threshold: (d.registry && d.registry.threshold) || null,
3967
+ governanceSetSize: (d.registry && d.registry.validatorCount) || null,
3804
3968
  }),
3805
3969
  });
3806
3970
  })
@@ -3821,7 +3985,7 @@ function App() {
3821
3985
  openAddr: (id) => setModal({ kind: "addr", payload: id }),
3822
3986
  openCreate: () => setModal({ kind: "create" }),
3823
3987
  openVote: (id) => setModal({ kind: "vote", payload: id }),
3824
- openSign: (id) => setModal({ kind: "sign", payload: id }),
3988
+ openAnchor: (id) => setModal({ kind: "anchor", payload: id }),
3825
3989
  openExecute: (id) => setModal({ kind: "execute", payload: id }),
3826
3990
  openImport: () => setModal({ kind: "import" }),
3827
3991
  exportProposal: (p) => {
@@ -3842,7 +4006,7 @@ function App() {
3842
4006
 
3843
4007
  // resolve proposal for active modal
3844
4008
  const modalProposal = useMemo(() => {
3845
- if (!modal || !["proposal", "vote", "sign", "execute"].includes(modal.kind)) return null;
4009
+ if (!modal || !["proposal", "vote", "anchor", "execute"].includes(modal.kind)) return null;
3846
4010
  return state.proposals.find(p => p.id === modal.payload);
3847
4011
  }, [modal, state.proposals]);
3848
4012
 
@@ -3862,14 +4026,14 @@ function App() {
3862
4026
  <header className="tfw-header">
3863
4027
  <div className="tfw-header__left">
3864
4028
  <div className="tfw-brand">
3865
- <div className="tfw-brand__mark">
4029
+ <div className="tfw-brand__mark">
3866
4030
  <span className="tfw-brand__mark-bar" />
3867
4031
  <span className="tfw-brand__mark-bar" />
3868
4032
  <span className="tfw-brand__mark-bar" />
3869
4033
  </div>
3870
4034
  <div className="tfw-brand__text">
3871
4035
  <div className="tfw-brand__name">Transaction Firewall</div>
3872
- <div className="tfw-brand__sub">{state.meta?.cliVersion || "v0.4.0"} · 3-of-5 multisig</div>
4036
+ <div className="tfw-brand__sub">{state.meta?.cliVersion} · validator console</div>
3873
4037
  </div>
3874
4038
  </div>
3875
4039
  </div>
@@ -3918,7 +4082,7 @@ function App() {
3918
4082
  <div className="tfw-you__avatar">{state.meta?.yourPubkey ? state.meta.yourPubkey.slice(2,4).toUpperCase() : "?"}</div>
3919
4083
  <div className="tfw-you__text">
3920
4084
  <div className="tfw-you__line1">{state.meta?.yourPubkey ? TFW_trunc(state.meta.yourPubkey, 16) : "anonymous"}</div>
3921
- <div className="tfw-you__line2 tfw-mono">SIGNER #{state.meta.yourSignerIndex}</div>
4085
+ <div className="tfw-you__line2 tfw-mono">validator key</div>
3922
4086
  </div>
3923
4087
  </div>
3924
4088
  </div>
@@ -3972,6 +4136,7 @@ function App() {
3972
4136
  <TFW_ProposalDetailContent
3973
4137
  proposal={modalProposal}
3974
4138
  registry={state.registry}
4139
+ meta={state.meta}
3975
4140
  actions={actions}
3976
4141
  onClose={closeModal}
3977
4142
  />
@@ -4004,6 +4169,7 @@ function App() {
4004
4169
  subtitle="Will enter a 72-hour review window before voting opens."
4005
4170
  >
4006
4171
  <TFW_CreateForm
4172
+ meta={state.meta}
4007
4173
  onSubmit={(p) => {
4008
4174
  dispatch({ type: "ADD_PROPOSAL", proposal: p });
4009
4175
  addToast("success", `Proposal #${p.id} created`);
@@ -4033,19 +4199,20 @@ function App() {
4033
4199
  </TFW_Modal>
4034
4200
 
4035
4201
  <TFW_Modal
4036
- open={modal?.kind === "sign" && !!modalProposal}
4202
+ open={modal?.kind === "anchor" && !!modalProposal}
4037
4203
  onClose={closeModal}
4038
4204
  size="md"
4039
- eyebrow={modalProposal ? `SIGN · PROPOSAL № ${modalProposal.id}` : ""}
4040
- title="Add multisig signature"
4205
+ eyebrow={modalProposal ? `ANCHOR · PROPOSAL № ${modalProposal.id}` : ""}
4206
+ title="Anchor proposal"
4207
+ subtitle="Creates or records the on-chain proposal anchor used to enforce the review window."
4041
4208
  >
4042
4209
  {modalProposal && (
4043
- <TFW_SignForm
4210
+ <TFW_AnchorForm
4044
4211
  proposal={modalProposal}
4045
4212
  meta={state.meta}
4046
- onSubmit={(s) => {
4047
- dispatch({ type: "ADD_SIG", ...s });
4048
- addToast("success", `Signature added by signer #${s.signerIndex}`);
4213
+ onSubmit={(p) => {
4214
+ dispatch({ type: "UPDATE_PROPOSAL", proposal: p });
4215
+ addToast("success", p.proposalCellTxHash ? "Proposal cell outpoint recorded" : "Proposal cell data prepared");
4049
4216
  }}
4050
4217
  onClose={closeModal}
4051
4218
  />
@@ -4062,7 +4229,9 @@ function App() {
4062
4229
  {modalProposal && (
4063
4230
  <TFW_ExecuteForm
4064
4231
  proposal={modalProposal}
4065
- onSubmit={(pid) => {
4232
+ meta={state.meta}
4233
+ onSubmit={(result) => {
4234
+ if (result.proposal) dispatch({ type: "UPDATE_PROPOSAL", proposal: result.proposal });
4066
4235
  addToast("success", "TX downloaded — broadcast it via ckb-cli to complete execution.");
4067
4236
  }}
4068
4237
  onClose={closeModal}