@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.
- package/README.md +11 -6
- package/dist/commands/anchor.d.ts +23 -0
- package/dist/commands/anchor.d.ts.map +1 -0
- package/dist/commands/anchor.js +412 -0
- package/dist/commands/anchor.js.map +1 -0
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +59 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/execute.d.ts +9 -1
- package/dist/commands/execute.d.ts.map +1 -1
- package/dist/commands/execute.js +394 -229
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/import.d.ts.map +1 -1
- package/dist/commands/import.js +15 -48
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +32 -0
- package/dist/commands/inspect.js.map +1 -1
- package/dist/commands/proposals.d.ts.map +1 -1
- package/dist/commands/proposals.js +2 -4
- package/dist/commands/proposals.js.map +1 -1
- package/dist/commands/propose.d.ts +5 -0
- package/dist/commands/propose.d.ts.map +1 -1
- package/dist/commands/propose.js +91 -7
- package/dist/commands/propose.js.map +1 -1
- package/dist/commands/reclaim.d.ts +18 -0
- package/dist/commands/reclaim.d.ts.map +1 -0
- package/dist/commands/reclaim.js +214 -0
- package/dist/commands/reclaim.js.map +1 -0
- package/dist/commands/sign.d.ts.map +1 -1
- package/dist/commands/sign.js +40 -94
- package/dist/commands/sign.js.map +1 -1
- package/dist/commands/vote.d.ts +2 -0
- package/dist/commands/vote.d.ts.map +1 -1
- package/dist/commands/vote.js +31 -25
- package/dist/commands/vote.js.map +1 -1
- package/dist/index.js +91 -16
- package/dist/index.js.map +1 -1
- package/dist/lib/blkl.d.ts +17 -2
- package/dist/lib/blkl.d.ts.map +1 -1
- package/dist/lib/blkl.js +133 -17
- package/dist/lib/blkl.js.map +1 -1
- package/dist/lib/capacity.d.ts +12 -0
- package/dist/lib/capacity.d.ts.map +1 -0
- package/dist/lib/capacity.js +18 -0
- package/dist/lib/capacity.js.map +1 -0
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +35 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/defaults.d.ts +19 -0
- package/dist/lib/defaults.d.ts.map +1 -1
- package/dist/lib/defaults.js +34 -7
- package/dist/lib/defaults.js.map +1 -1
- package/dist/lib/governance-v4.d.ts +39 -0
- package/dist/lib/governance-v4.d.ts.map +1 -0
- package/dist/lib/governance-v4.js +194 -0
- package/dist/lib/governance-v4.js.map +1 -0
- package/dist/lib/gui-bundle.html +485 -316
- package/dist/lib/gui-server.d.ts.map +1 -1
- package/dist/lib/gui-server.js +300 -246
- package/dist/lib/gui-server.js.map +1 -1
- package/dist/lib/hints.d.ts +1 -1
- package/dist/lib/hints.d.ts.map +1 -1
- package/dist/lib/hints.js +3 -9
- package/dist/lib/hints.js.map +1 -1
- package/dist/lib/proposals.d.ts +17 -9
- package/dist/lib/proposals.d.ts.map +1 -1
- package/dist/lib/proposals.js +4 -26
- package/dist/lib/proposals.js.map +1 -1
- package/dist/lib/rpc.d.ts +5 -0
- package/dist/lib/rpc.d.ts.map +1 -1
- package/dist/lib/rpc.js +21 -0
- package/dist/lib/rpc.js.map +1 -1
- package/dist/lib/treasury-status.d.ts +28 -0
- package/dist/lib/treasury-status.d.ts.map +1 -0
- package/dist/lib/treasury-status.js +70 -0
- package/dist/lib/treasury-status.js.map +1 -0
- package/dist/lib/treasury.d.ts +15 -0
- package/dist/lib/treasury.d.ts.map +1 -0
- package/dist/lib/treasury.js +62 -0
- package/dist/lib/treasury.js.map +1 -0
- package/dist/lib/tx-deps.d.ts +9 -0
- package/dist/lib/tx-deps.d.ts.map +1 -0
- package/dist/lib/tx-deps.js +15 -0
- package/dist/lib/tx-deps.js.map +1 -0
- package/dist/lib/witness.d.ts +13 -11
- package/dist/lib/witness.d.ts.map +1 -1
- package/dist/lib/witness.js +85 -48
- package/dist/lib/witness.js.map +1 -1
- package/package.json +1 -1
package/dist/lib/gui-bundle.html
CHANGED
|
@@ -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: "
|
|
1928
|
-
"ready": { label: "ready
|
|
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,
|
|
2252
|
+
TFW_VoteDots, TFW_Address, TFW_ClassificationTag,
|
|
2175
2253
|
TFW_trunc, TFW_fmtDate, TFW_fmtDateShort, TFW_relTime, TFW_reviewCountdown,
|
|
2176
|
-
TFW_countYes,
|
|
2254
|
+
TFW_countYes, TFW_reviewPassed, TFW_isReady, TFW_displayStatus,
|
|
2177
2255
|
} = window;
|
|
2178
2256
|
|
|
2179
|
-
function ProposalCard({ proposal, onOpen, onVote,
|
|
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
|
|
2267
|
+
const hasVoted = meta?.yourPubkey
|
|
2268
|
+
? (p.votes || []).some(v => v.pubkey === meta.yourPubkey)
|
|
2269
|
+
: false;
|
|
2190
2270
|
|
|
2191
|
-
//
|
|
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") &&
|
|
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,
|
|
2374
|
+
TFW_Badge, TFW_VoteDots, TFW_StatusBadge, TFW_ActionPill,
|
|
2301
2375
|
TFW_ClassificationTag, TFW_SeverityChip, TFW_Address,
|
|
2302
|
-
TFW_isReady, TFW_displayStatus, TFW_countYes,
|
|
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
|
|
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">
|
|
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}
|
|
2347
|
-
{action.length === 1 ? "s" : ""} your
|
|
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’t voted on <strong>{youHaventVoted.length}</strong>.</>
|
|
2350
2464
|
)}
|
|
2351
2465
|
</>
|
|
2352
2466
|
) : (
|
|
2353
|
-
<>All clear. No proposals
|
|
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
|
-
{/*
|
|
2487
|
+
{/* VOTES NEEDED */}
|
|
2384
2488
|
{action.length > 0 && (
|
|
2385
2489
|
<div className="tfw-section">
|
|
2386
2490
|
<TFW_SectionHead
|
|
2387
2491
|
eyebrow="◆ PRIORITY"
|
|
2388
|
-
title="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
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"
|
|
2662
|
-
|
|
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">
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
//
|
|
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 (
|
|
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 & 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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
2979
|
+
<div className="tfw-dossier__label">REVIEW</div>
|
|
2928
2980
|
<div className="tfw-dossier__val tfw-mono">
|
|
2929
|
-
{
|
|
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(
|
|
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="
|
|
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
|
|
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="
|
|
3279
|
-
className="tfw-input
|
|
3280
|
-
value={expires}
|
|
3281
|
-
min=
|
|
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
|
-
|
|
3346
|
-
|
|
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
|
-
// ───
|
|
3399
|
-
function
|
|
3400
|
-
const [
|
|
3401
|
-
const [
|
|
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
|
|
3458
|
+
const treasuryBacked = Boolean(meta?.treasury);
|
|
3405
3459
|
|
|
3406
|
-
const
|
|
3460
|
+
const generateCommand = () => {
|
|
3407
3461
|
setError("");
|
|
3408
|
-
|
|
3409
|
-
|
|
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
|
-
|
|
3414
|
-
|
|
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 (
|
|
3418
|
-
setError("
|
|
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/
|
|
3423
|
-
body: JSON.stringify({
|
|
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
|
-
|
|
3430
|
-
|
|
3431
|
-
setTimeout(onClose, 1100);
|
|
3505
|
+
setRecordResult(d);
|
|
3506
|
+
onSubmit(d.proposal);
|
|
3432
3507
|
})
|
|
3433
|
-
.catch(e => { setSubmitting(false);
|
|
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="
|
|
3449
|
-
|
|
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
|
-
|
|
3453
|
-
<
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
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
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
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}>
|
|
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(
|
|
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>
|
|
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
|
|
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>
|
|
3549
|
-
<span className="tfw-mono">{
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 "
|
|
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
|
|
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
|
|
3751
|
-
...
|
|
3752
|
-
...
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
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
|
-
|
|
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", "
|
|
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
|
-
|
|
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
|
|
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">
|
|
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 === "
|
|
4202
|
+
open={modal?.kind === "anchor" && !!modalProposal}
|
|
4037
4203
|
onClose={closeModal}
|
|
4038
4204
|
size="md"
|
|
4039
|
-
eyebrow={modalProposal ? `
|
|
4040
|
-
title="
|
|
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
|
-
<
|
|
4210
|
+
<TFW_AnchorForm
|
|
4044
4211
|
proposal={modalProposal}
|
|
4045
4212
|
meta={state.meta}
|
|
4046
|
-
onSubmit={(
|
|
4047
|
-
dispatch({ type: "
|
|
4048
|
-
addToast("success",
|
|
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
|
-
|
|
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}
|