@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-server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadConfig } from "./config.js";
|
|
1
2
|
import { createServer } from "node:http";
|
|
2
3
|
import { join, dirname as pathDirname } from "node:path";
|
|
3
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -6,14 +7,21 @@ import { createRequire } from "node:module";
|
|
|
6
7
|
const _require = createRequire(import.meta.url);
|
|
7
8
|
const _cliVersion = _require("../../package.json").version;
|
|
8
9
|
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
9
|
-
import { listProposals, loadProposal, saveProposal, getProposalsDir, computeProposalIdHash, computeVoteDigestHash, voteSigningMessage,
|
|
10
|
+
import { listProposals, loadProposal, saveProposal, getProposalsDir, computeProposalIdHash, computeVoteDigestHash, voteSigningMessage, isReviewWindowPassed, isVoteApproved, countYes, REVIEW_WINDOW_MS, } from "./proposals.js";
|
|
10
11
|
import { getLiveCell } from "./rpc.js";
|
|
11
12
|
import { resolveRegistryOutpoint } from "./registry.js";
|
|
12
13
|
import { parseRegistryPayload } from "@ckb-firewall/sdk";
|
|
13
|
-
import { hexToBytes, bytesToHex, strip0x,
|
|
14
|
+
import { hexToBytes, bytesToHex, strip0x, extractGovernanceHeaderRaw, parseGovernanceHeader, governanceTreasuryLockHash, scriptToMoleculeBytes, } from "./blkl.js";
|
|
14
15
|
import { computeMerkleProof, verifyMerkleProof } from "./validator-set.js";
|
|
15
16
|
import { TESTNET_GOVERNANCE_PUBKEYS, TESTNET_CONTRACT_OUTPOINTS, SECP256K1_DEP_GROUP, } from "./defaults.js";
|
|
16
|
-
import { ckbBlake2b,
|
|
17
|
+
import { ckbBlake2b, buildGov1WitnessV4, buildValidatorVoteWitness, buildWitnessArgs, encodeRelativeTimestampSince, } from "./witness.js";
|
|
18
|
+
import { assertProposalCellMatches, assertProposalAnchorTypeMatches, encodeProposalCellData, loadRegistryStateForProposal, parseRegistryTypeIdValue, proposalCellDataHash, proposalV4Fields, } from "./governance-v4.js";
|
|
19
|
+
import { hexCapacity, occupiedCapacityShannons, parseCapacity } from "./capacity.js";
|
|
20
|
+
import { loadTreasuryStatus } from "./treasury-status.js";
|
|
21
|
+
const DEFAULT_FEE_SHANNONS = 100000n;
|
|
22
|
+
// Treasury-lock change outputs have 64-byte args — minimum occupied capacity is
|
|
23
|
+
// 8 (capacity field) + 117 (lock script molecule) = 125 CKB = 12,500,000,000 shannons.
|
|
24
|
+
const MIN_CHANGE_SHANNONS = 125n * 100000000n;
|
|
17
25
|
// ── /api/data handler ─────────────────────────────────────────────────────────
|
|
18
26
|
async function buildApiData(opts) {
|
|
19
27
|
const proposals = listProposals();
|
|
@@ -22,10 +30,15 @@ async function buildApiData(opts) {
|
|
|
22
30
|
const { txHash, index } = await resolveRegistryOutpoint(opts.rpcUrl, opts.registryTx, opts.registryIndex);
|
|
23
31
|
const cell = await getLiveCell(opts.rpcUrl, txHash, index);
|
|
24
32
|
const payload = parseRegistryPayload(cell.data);
|
|
33
|
+
const governanceHeaderRaw = extractGovernanceHeaderRaw(cell.data);
|
|
34
|
+
const governanceHeader = governanceHeaderRaw ? parseGovernanceHeader(governanceHeaderRaw) : null;
|
|
25
35
|
registry = {
|
|
26
36
|
txHash,
|
|
27
37
|
index,
|
|
28
38
|
version: payload.version,
|
|
39
|
+
threshold: governanceHeader?.threshold ?? null,
|
|
40
|
+
validatorCount: governanceHeader?.validatorCount ?? null,
|
|
41
|
+
treasury: await loadTreasuryStatus(opts.rpcUrl, cell, governanceHeader),
|
|
29
42
|
error: null,
|
|
30
43
|
entries: payload.entries.map((e) => ({
|
|
31
44
|
identifier: e.identifier,
|
|
@@ -38,6 +51,9 @@ async function buildApiData(opts) {
|
|
|
38
51
|
txHash: opts.registryTx,
|
|
39
52
|
index: opts.registryIndex,
|
|
40
53
|
version: 0,
|
|
54
|
+
threshold: null,
|
|
55
|
+
validatorCount: null,
|
|
56
|
+
treasury: null,
|
|
41
57
|
error: err instanceof Error ? err.message : String(err),
|
|
42
58
|
entries: [],
|
|
43
59
|
};
|
|
@@ -78,6 +94,28 @@ function apiErr(res, msg, status = 400) {
|
|
|
78
94
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
79
95
|
res.end(JSON.stringify({ ok: false, error: msg }));
|
|
80
96
|
}
|
|
97
|
+
function parseOutputIndex(value, name) {
|
|
98
|
+
const raw = String(value ?? "").trim();
|
|
99
|
+
if (!/^\d+$/.test(raw))
|
|
100
|
+
throw new Error(`${name} must be a non-negative integer.`);
|
|
101
|
+
return Number.parseInt(raw, 10);
|
|
102
|
+
}
|
|
103
|
+
function parseOptionalTxHash(value, name) {
|
|
104
|
+
const raw = String(value ?? "").trim();
|
|
105
|
+
if (!raw)
|
|
106
|
+
return undefined;
|
|
107
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(raw))
|
|
108
|
+
throw new Error(`${name} must be a 0x-prefixed 32-byte transaction hash.`);
|
|
109
|
+
return raw;
|
|
110
|
+
}
|
|
111
|
+
function extractProposalOutpoint(proposal, body) {
|
|
112
|
+
const txHash = parseOptionalTxHash(body.proposalTx, "proposalTx") || proposal.proposalCellTxHash?.trim();
|
|
113
|
+
const indexRaw = body.proposalIndex ?? (proposal.proposalCellIndex === undefined ? undefined : String(proposal.proposalCellIndex));
|
|
114
|
+
if (!txHash || indexRaw === undefined) {
|
|
115
|
+
throw new Error("Proposal cell outpoint is missing. Anchor the proposal cell first, then record its tx hash and output index.");
|
|
116
|
+
}
|
|
117
|
+
return { txHash, index: parseOutputIndex(indexRaw, "proposalIndex") };
|
|
118
|
+
}
|
|
81
119
|
// ── /api/propose ──────────────────────────────────────────────────────────────
|
|
82
120
|
function isValidHex(v) {
|
|
83
121
|
const c = strip0x(v);
|
|
@@ -167,20 +205,18 @@ async function handleVote(body, opts) {
|
|
|
167
205
|
throw new Error("Proposal already executed");
|
|
168
206
|
if (proposal.status === "rejected")
|
|
169
207
|
throw new Error("Proposal was rejected");
|
|
170
|
-
if (proposal.signatures.length > 0)
|
|
171
|
-
throw new Error("Cannot vote — signing has already begun");
|
|
172
208
|
const pubkeyBytes = secp256k1.getPublicKey(pkBytes, true);
|
|
173
209
|
const pubkey = bytesToHex(new Uint8Array(pubkeyBytes));
|
|
174
210
|
const validatorSet = TESTNET_GOVERNANCE_PUBKEYS.map((k) => bytesToHex(k));
|
|
175
211
|
const merkleResult = computeMerkleProof(validatorSet, pubkey);
|
|
176
212
|
if (!merkleResult) {
|
|
177
213
|
pkBytes.fill(0);
|
|
178
|
-
throw new Error(`Key is not an authorized
|
|
214
|
+
throw new Error(`Key is not an authorized governance voter (pubkey: ${pubkey.slice(0, 20)}…)`);
|
|
179
215
|
}
|
|
180
216
|
const { proof: merkleProof, leafIndex: merkleLeafIndex } = merkleResult;
|
|
181
217
|
if (proposal.votes.some((v) => v.pubkey.toLowerCase() === pubkey.toLowerCase())) {
|
|
182
218
|
pkBytes.fill(0);
|
|
183
|
-
throw new Error("This
|
|
219
|
+
throw new Error("This governance voter already voted on this proposal");
|
|
184
220
|
}
|
|
185
221
|
const timestamp = new Date().toISOString();
|
|
186
222
|
const msgHash = voteSigningMessage(proposal.proposalIdHash, choice, timestamp, pubkey);
|
|
@@ -198,70 +234,6 @@ async function handleVote(body, opts) {
|
|
|
198
234
|
saveProposal(proposal);
|
|
199
235
|
return { ok: true, yesCount, approved, status: proposal.status, pubkey };
|
|
200
236
|
}
|
|
201
|
-
// ── /api/sign ─────────────────────────────────────────────────────────────────
|
|
202
|
-
async function handleSign(body, opts) {
|
|
203
|
-
if (typeof body !== "object" || body === null)
|
|
204
|
-
throw new Error("Invalid body");
|
|
205
|
-
const b = body;
|
|
206
|
-
const proposalId = String(b.proposalId ?? "").trim();
|
|
207
|
-
if (!proposalId)
|
|
208
|
-
throw new Error("proposalId required");
|
|
209
|
-
const signerIndex = Number(b.signerIndex);
|
|
210
|
-
if (!Number.isInteger(signerIndex) || signerIndex < 0 || signerIndex >= 5)
|
|
211
|
-
throw new Error("signerIndex must be 0–4");
|
|
212
|
-
const pkHex = String(b.privateKey ?? "").trim();
|
|
213
|
-
let pkBytes;
|
|
214
|
-
try {
|
|
215
|
-
pkBytes = hexToBytes(pkHex);
|
|
216
|
-
if (pkBytes.length !== 32)
|
|
217
|
-
throw new Error("must be 32 bytes");
|
|
218
|
-
secp256k1.getPublicKey(pkBytes);
|
|
219
|
-
}
|
|
220
|
-
catch (e) {
|
|
221
|
-
throw new Error("Invalid private key: " + (e instanceof Error ? e.message : String(e)));
|
|
222
|
-
}
|
|
223
|
-
const proposal = loadProposal(proposalId);
|
|
224
|
-
if (proposal.status === "executed")
|
|
225
|
-
throw new Error("Already executed");
|
|
226
|
-
if (proposal.status === "rejected")
|
|
227
|
-
throw new Error("Proposal was rejected");
|
|
228
|
-
if (!isReviewWindowPassed(proposal)) {
|
|
229
|
-
const ms = new Date(proposal.reviewWindowEndsAt).getTime() - Date.now();
|
|
230
|
-
const h2 = Math.floor(ms / 3_600_000), m = Math.floor((ms % 3_600_000) / 60_000);
|
|
231
|
-
throw new Error(`Review window not passed yet — ${h2}h ${m}m remaining`);
|
|
232
|
-
}
|
|
233
|
-
if (!isVoteApproved(proposal))
|
|
234
|
-
throw new Error("Vote threshold not met");
|
|
235
|
-
if (proposal.signatures.some((s) => s.signerIndex === signerIndex))
|
|
236
|
-
throw new Error(`Signer ${signerIndex} already signed`);
|
|
237
|
-
const { txHash, index } = await resolveRegistryOutpoint(opts.rpcUrl, opts.registryTx, opts.registryIndex);
|
|
238
|
-
const cell = await getLiveCell(opts.rpcUrl, txHash, index);
|
|
239
|
-
const currentPayload = parseRegistryPayload(cell.data);
|
|
240
|
-
const govHeaderRaw = extractGovernanceHeaderRaw(cell.data);
|
|
241
|
-
const oldBlkl = hexToBytes(cell.data);
|
|
242
|
-
const newEntries = proposal.action === "add"
|
|
243
|
-
? insertSorted(currentPayload.entries, { identifier: proposal.lockArgs, expiresAt: BigInt(proposal.expiresAt) })
|
|
244
|
-
: removeEntry(currentPayload.entries, proposal.lockArgs);
|
|
245
|
-
const newBlkl = encodeRegistryPayload({ version: currentPayload.version, entries: newEntries }, govHeaderRaw ?? undefined);
|
|
246
|
-
const oldRoot = ckbBlake2b(oldBlkl);
|
|
247
|
-
const newRoot = ckbBlake2b(newBlkl);
|
|
248
|
-
const pubKey = bytesToHex(new Uint8Array(secp256k1.getPublicKey(pkBytes, true)));
|
|
249
|
-
const reviewWindowEndMs = BigInt(new Date(proposal.reviewWindowEndsAt).getTime());
|
|
250
|
-
const msgHash = signingMessage(proposal, oldRoot, newRoot, reviewWindowEndMs);
|
|
251
|
-
const recoveredSig = secp256k1.sign(msgHash, pkBytes, { lowS: true, format: "recovered" });
|
|
252
|
-
const sigBytes = new Uint8Array(65);
|
|
253
|
-
sigBytes.set(recoveredSig.slice(1), 0);
|
|
254
|
-
sigBytes[64] = recoveredSig[0] ?? 0;
|
|
255
|
-
pkBytes.fill(0);
|
|
256
|
-
const govHeader = govHeaderRaw ? parseGovernanceHeader(govHeaderRaw) : null;
|
|
257
|
-
const effectiveThreshold = govHeader?.threshold ?? SIG_THRESHOLD;
|
|
258
|
-
const sigHex = bytesToHex(sigBytes);
|
|
259
|
-
proposal.signatures.push({ signerIndex, signature: sigHex, timestamp: new Date().toISOString() });
|
|
260
|
-
if (proposal.signatures.length >= effectiveThreshold)
|
|
261
|
-
proposal.status = "approved";
|
|
262
|
-
saveProposal(proposal);
|
|
263
|
-
return { ok: true, sigCount: proposal.signatures.length, effectiveThreshold, ready: isReadyToExecute(proposal), pubKey, signature: sigHex };
|
|
264
|
-
}
|
|
265
237
|
// ── /api/execute ──────────────────────────────────────────────────────────────
|
|
266
238
|
async function handleExecute(body, opts) {
|
|
267
239
|
if (typeof body !== "object" || body === null)
|
|
@@ -272,112 +244,245 @@ async function handleExecute(body, opts) {
|
|
|
272
244
|
throw new Error("proposalId required");
|
|
273
245
|
const proposal = loadProposal(proposalId);
|
|
274
246
|
if (proposal.status === "executed")
|
|
275
|
-
throw new Error(
|
|
247
|
+
throw new Error("Proposal already executed");
|
|
276
248
|
if (!isReviewWindowPassed(proposal))
|
|
277
|
-
throw new Error("
|
|
249
|
+
throw new Error("Local review window has not passed");
|
|
278
250
|
if (!isVoteApproved(proposal))
|
|
279
251
|
throw new Error("Vote threshold not met");
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
252
|
+
const outpoint = extractProposalOutpoint(proposal, b);
|
|
253
|
+
const state = await loadRegistryStateForProposal(opts.rpcUrl, opts.registryTx, opts.registryIndex, proposal);
|
|
254
|
+
const proposalCell = await getLiveCell(opts.rpcUrl, outpoint.txHash, outpoint.index);
|
|
255
|
+
const proposalDataHash = assertProposalCellMatches(proposal, proposalCell.data, state.registryTypeIdValue);
|
|
256
|
+
const fields = proposalV4Fields(proposal, state.registryTypeIdValue);
|
|
257
|
+
if (bytesToHex(fields.proposalDataHash) !== bytesToHex(proposalDataHash)) {
|
|
258
|
+
throw new Error("Internal proposal hash mismatch");
|
|
286
259
|
}
|
|
287
|
-
|
|
260
|
+
proposal.proposalDataHash = bytesToHex(proposalDataHash);
|
|
261
|
+
proposal.reviewDelayMs = fields.reviewDelayMs.toString();
|
|
262
|
+
proposal.proposalCellTxHash = outpoint.txHash;
|
|
263
|
+
proposal.proposalCellIndex = outpoint.index;
|
|
288
264
|
for (const v of proposal.votes) {
|
|
289
|
-
const
|
|
290
|
-
if (
|
|
291
|
-
throw new Error(`Vote from ${v.pubkey.slice(0, 14)}
|
|
265
|
+
const sigBytes = hexToBytes(v.signature);
|
|
266
|
+
if (sigBytes.length !== 65)
|
|
267
|
+
throw new Error(`Vote from ${v.pubkey.slice(0, 14)}... has invalid signature length`);
|
|
292
268
|
const msgHash = voteSigningMessage(proposal.proposalIdHash, v.vote, v.timestamp, v.pubkey);
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
throw new Error(`Vote from ${v.pubkey.slice(0, 14)}… has unrecoverable signature`);
|
|
302
|
-
}
|
|
303
|
-
if (recoveredPk !== v.pubkey)
|
|
304
|
-
throw new Error(`Vote from ${v.pubkey.slice(0, 14)}… signature mismatch`);
|
|
269
|
+
const sig65 = new Uint8Array(65);
|
|
270
|
+
sig65[0] = sigBytes[64];
|
|
271
|
+
sig65.set(sigBytes.subarray(0, 64), 1);
|
|
272
|
+
const recoveredPubkey = bytesToHex(new Uint8Array(secp256k1.recoverPublicKey(sig65, msgHash)));
|
|
273
|
+
if (recoveredPubkey !== v.pubkey)
|
|
274
|
+
throw new Error(`Vote signature does not match pubkey ${v.pubkey.slice(0, 14)}...`);
|
|
305
275
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const currentPayload = parseRegistryPayload(cell.data);
|
|
309
|
-
const govHeaderRaw = extractGovernanceHeaderRaw(cell.data);
|
|
310
|
-
const govHeader = govHeaderRaw ? parseGovernanceHeader(govHeaderRaw) : null;
|
|
311
|
-
const oldBlkl = hexToBytes(cell.data);
|
|
312
|
-
const oldRoot = ckbBlake2b(oldBlkl);
|
|
313
|
-
// Verify Merkle proofs against on-chain validator set
|
|
314
|
-
if (govHeader && govHeader.validatorCount > 0) {
|
|
315
|
-
const rootHex = bytesToHex(govHeader.validatorMerkleRoot);
|
|
276
|
+
if (state.governanceHeader?.validatorCount) {
|
|
277
|
+
const rootHex = bytesToHex(state.governanceHeader.validatorMerkleRoot);
|
|
316
278
|
for (const v of proposal.votes) {
|
|
317
|
-
if (!
|
|
318
|
-
throw new Error(`Vote from ${v.pubkey.slice(0, 14)}
|
|
319
|
-
|
|
320
|
-
throw new Error(`Vote from ${v.pubkey.slice(0, 14)}… not in on-chain validator set`);
|
|
279
|
+
if (!verifyMerkleProof(rootHex, v.pubkey, v.merkleProof, v.merkleLeafIndex)) {
|
|
280
|
+
throw new Error(`Vote from ${v.pubkey.slice(0, 14)}... is not in the on-chain governance voter set`);
|
|
281
|
+
}
|
|
321
282
|
}
|
|
322
283
|
}
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
let rk;
|
|
341
|
-
try {
|
|
342
|
-
rk = bytesToHex(new Uint8Array(secp256k1.recoverPublicKey(sig65s, msgHash)));
|
|
343
|
-
}
|
|
344
|
-
catch {
|
|
345
|
-
throw new Error(`Sig from signer ${s.signerIndex} unrecoverable`);
|
|
346
|
-
}
|
|
347
|
-
const expected = bytesToHex(govHeader.pubkeys[s.signerIndex]);
|
|
348
|
-
if (rk !== expected)
|
|
349
|
-
throw new Error(`Sig from signer ${s.signerIndex} does not match on-chain pubkey`);
|
|
284
|
+
const proposalCapacity = parseCapacity(proposalCell.capacity);
|
|
285
|
+
const proposalChangeCapacity = proposalCapacity - DEFAULT_FEE_SHANNONS;
|
|
286
|
+
const treasuryLockHash = governanceTreasuryLockHash(state.governanceHeader);
|
|
287
|
+
const minChange = treasuryLockHash
|
|
288
|
+
? MIN_CHANGE_SHANNONS
|
|
289
|
+
: occupiedCapacityShannons({ lock: proposalCell.lock, type: null, data: "0x" });
|
|
290
|
+
if (proposalChangeCapacity < minChange) {
|
|
291
|
+
throw new Error(`Proposal cell capacity ${proposalCapacity} shannons is too small to return change after fee`);
|
|
292
|
+
}
|
|
293
|
+
let registryOutputCapacity = parseCapacity(state.cell.capacity);
|
|
294
|
+
let extraTreasuryOutputCapacity = 0n;
|
|
295
|
+
let treasuryLockScript;
|
|
296
|
+
if (treasuryLockHash) {
|
|
297
|
+
treasuryLockScript = state.governanceHeader?.treasuryLockScript;
|
|
298
|
+
if (!treasuryLockScript) {
|
|
299
|
+
throw new Error("Registry treasury lock script is missing from the governance header. " +
|
|
300
|
+
"A full treasury lock script (v3 header) is required to safely route change capacity.");
|
|
350
301
|
}
|
|
302
|
+
assertProposalAnchorTypeMatches({
|
|
303
|
+
proposalCellType: proposalCell.type,
|
|
304
|
+
registryTypeIdValue: state.registryTypeIdValue,
|
|
305
|
+
governanceHeader: state.governanceHeader,
|
|
306
|
+
reclaimDelayMs: fields.reviewDelayMs,
|
|
307
|
+
});
|
|
308
|
+
const proposalLockHash = ckbBlake2b(scriptToMoleculeBytes(proposalCell.lock));
|
|
309
|
+
if (bytesToHex(proposalLockHash) !== bytesToHex(treasuryLockHash)) {
|
|
310
|
+
throw new Error("This registry uses treasury-funded anchors, but the proposal cell is not locked to the registry treasury");
|
|
311
|
+
}
|
|
312
|
+
const registryInputCapacity = parseCapacity(state.cell.capacity);
|
|
313
|
+
const minRegistryCapacity = occupiedCapacityShannons({
|
|
314
|
+
lock: state.cell.lock,
|
|
315
|
+
type: state.cell.type,
|
|
316
|
+
data: state.newBlkl,
|
|
317
|
+
});
|
|
318
|
+
registryOutputCapacity = minRegistryCapacity;
|
|
319
|
+
if (registryOutputCapacity > registryInputCapacity) {
|
|
320
|
+
throw new Error(`Registry update needs ${registryOutputCapacity - registryInputCapacity} shannons of treasury capacity for growth. ` +
|
|
321
|
+
"Use the CLI execute command with --treasury-cell inputs.");
|
|
322
|
+
}
|
|
323
|
+
extraTreasuryOutputCapacity = registryInputCapacity - registryOutputCapacity;
|
|
351
324
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
325
|
+
const yesVotes = proposal.votes
|
|
326
|
+
.filter((v) => v.vote === "yes")
|
|
327
|
+
.sort((a, b) => a.pubkey.localeCompare(b.pubkey));
|
|
328
|
+
const gov1 = buildGov1WitnessV4({
|
|
329
|
+
proposalIdHash: hexToBytes(proposal.proposalIdHash),
|
|
330
|
+
voteDigestHash: hexToBytes(proposal.voteDigestHash),
|
|
331
|
+
oldRoot: state.oldRoot,
|
|
332
|
+
newRoot: state.newRoot,
|
|
333
|
+
proposalDataHash,
|
|
334
|
+
reviewDelayMs: fields.reviewDelayMs,
|
|
335
|
+
});
|
|
336
|
+
const voteWitness = buildValidatorVoteWitness(yesVotes.map((v) => ({
|
|
337
|
+
pubkey: hexToBytes(v.pubkey),
|
|
338
|
+
vote: v.vote,
|
|
339
|
+
timestamp: v.timestamp,
|
|
340
|
+
signature: hexToBytes(v.signature),
|
|
341
|
+
merkleLeafIndex: v.merkleLeafIndex,
|
|
342
|
+
merkleProof: v.merkleProof.map(hexToBytes),
|
|
343
|
+
})));
|
|
344
|
+
const witnessBytes = buildWitnessArgs({ lock: voteWitness, inputType: gov1 });
|
|
363
345
|
const txJson = {
|
|
364
346
|
transaction: {
|
|
365
347
|
version: "0x0",
|
|
366
348
|
cell_deps: [
|
|
367
349
|
{ out_point: { tx_hash: SECP256K1_DEP_GROUP.txHash, index: "0x0" }, dep_type: "dep_group" },
|
|
368
|
-
{
|
|
369
|
-
|
|
350
|
+
{
|
|
351
|
+
out_point: {
|
|
352
|
+
tx_hash: TESTNET_CONTRACT_OUTPOINTS.blacklistRegistry.txHash,
|
|
353
|
+
index: `0x${TESTNET_CONTRACT_OUTPOINTS.blacklistRegistry.index.toString(16)}`,
|
|
354
|
+
},
|
|
355
|
+
dep_type: "code",
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
out_point: {
|
|
359
|
+
tx_hash: TESTNET_CONTRACT_OUTPOINTS.governanceLock.txHash,
|
|
360
|
+
index: `0x${TESTNET_CONTRACT_OUTPOINTS.governanceLock.index.toString(16)}`,
|
|
361
|
+
},
|
|
362
|
+
dep_type: "code",
|
|
363
|
+
},
|
|
364
|
+
...(treasuryLockHash ? [{
|
|
365
|
+
out_point: {
|
|
366
|
+
tx_hash: TESTNET_CONTRACT_OUTPOINTS.proposalAnchor.txHash,
|
|
367
|
+
index: `0x${TESTNET_CONTRACT_OUTPOINTS.proposalAnchor.index.toString(16)}`,
|
|
368
|
+
},
|
|
369
|
+
dep_type: "code",
|
|
370
|
+
}] : []),
|
|
370
371
|
],
|
|
371
372
|
header_deps: [],
|
|
372
|
-
inputs: [
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
373
|
+
inputs: [
|
|
374
|
+
{
|
|
375
|
+
since: "0x0",
|
|
376
|
+
previous_output: { tx_hash: state.cell.txHash, index: `0x${state.cell.index.toString(16)}` },
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
since: encodeRelativeTimestampSince(fields.reviewDelayMs),
|
|
380
|
+
previous_output: { tx_hash: outpoint.txHash, index: `0x${outpoint.index.toString(16)}` },
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
outputs: [
|
|
384
|
+
{ capacity: hexCapacity(registryOutputCapacity), lock: state.cell.lock, type: state.cell.type },
|
|
385
|
+
// Merge proposal change + extra treasury capacity into one output to avoid
|
|
386
|
+
// creating a cell below the minimum 125 CKB for treasury-lock's 64-byte args.
|
|
387
|
+
{ capacity: hexCapacity(proposalChangeCapacity + extraTreasuryOutputCapacity), lock: treasuryLockScript ?? proposalCell.lock, type: null },
|
|
388
|
+
],
|
|
389
|
+
outputs_data: [
|
|
390
|
+
bytesToHex(state.newBlkl),
|
|
391
|
+
"0x",
|
|
392
|
+
],
|
|
393
|
+
witnesses: [bytesToHex(witnessBytes), bytesToHex(buildWitnessArgs({}))],
|
|
376
394
|
},
|
|
377
395
|
multisig_configs: {},
|
|
378
396
|
signatures: {},
|
|
379
397
|
};
|
|
380
|
-
|
|
398
|
+
saveProposal(proposal);
|
|
399
|
+
return {
|
|
400
|
+
ok: true,
|
|
401
|
+
proposal,
|
|
402
|
+
txJson,
|
|
403
|
+
filename: `gov_execute_tx_${proposal.id}.json`,
|
|
404
|
+
proposalCell: outpoint,
|
|
405
|
+
proposalDataHash: bytesToHex(proposalDataHash),
|
|
406
|
+
since: encodeRelativeTimestampSince(fields.reviewDelayMs),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
// ── /api/anchor ───────────────────────────────────────────────────────────────
|
|
410
|
+
async function handleAnchor(body, opts) {
|
|
411
|
+
if (typeof body !== "object" || body === null)
|
|
412
|
+
throw new Error("Invalid body");
|
|
413
|
+
const b = body;
|
|
414
|
+
const proposalId = String(b.proposalId ?? "").trim();
|
|
415
|
+
if (!proposalId)
|
|
416
|
+
throw new Error("proposalId required");
|
|
417
|
+
const proposal = loadProposal(proposalId);
|
|
418
|
+
const { txHash, index } = await resolveRegistryOutpoint(opts.rpcUrl, opts.registryTx, opts.registryIndex);
|
|
419
|
+
const registryCell = await getLiveCell(opts.rpcUrl, txHash, index);
|
|
420
|
+
if (!registryCell.type)
|
|
421
|
+
throw new Error("Registry cell has no type script");
|
|
422
|
+
const registryTypeIdValue = parseRegistryTypeIdValue(registryCell.type.args);
|
|
423
|
+
const governanceHeaderRaw = extractGovernanceHeaderRaw(registryCell.data);
|
|
424
|
+
const governanceHeader = governanceHeaderRaw ? parseGovernanceHeader(governanceHeaderRaw) : null;
|
|
425
|
+
const proposalData = encodeProposalCellData(proposal, registryTypeIdValue);
|
|
426
|
+
const proposalDataHashHex = bytesToHex(proposalCellDataHash(proposal, registryTypeIdValue));
|
|
427
|
+
proposal.proposalDataHash = proposalDataHashHex;
|
|
428
|
+
proposal.reviewDelayMs = proposal.reviewDelayMs ?? String(REVIEW_WINDOW_MS);
|
|
429
|
+
const proposalTx = parseOptionalTxHash(b.proposalTx, "proposalTx");
|
|
430
|
+
let anchorVerified = false;
|
|
431
|
+
if (proposalTx) {
|
|
432
|
+
const proposalIndex = parseOutputIndex(b.proposalIndex, "proposalIndex");
|
|
433
|
+
const proposalCell = await getLiveCell(opts.rpcUrl, proposalTx, proposalIndex);
|
|
434
|
+
const liveHash = assertProposalCellMatches(proposal, proposalCell.data, registryTypeIdValue);
|
|
435
|
+
if (bytesToHex(liveHash) !== proposalDataHashHex) {
|
|
436
|
+
throw new Error("Live proposal-cell hash does not match the expected PBLK data");
|
|
437
|
+
}
|
|
438
|
+
assertProposalAnchorTypeMatches({
|
|
439
|
+
proposalCellType: proposalCell.type,
|
|
440
|
+
registryTypeIdValue,
|
|
441
|
+
governanceHeader,
|
|
442
|
+
reclaimDelayMs: BigInt(proposal.reviewDelayMs),
|
|
443
|
+
});
|
|
444
|
+
const treasuryLockHash = governanceTreasuryLockHash(governanceHeader);
|
|
445
|
+
if (treasuryLockHash) {
|
|
446
|
+
const proposalLockHash = ckbBlake2b(scriptToMoleculeBytes(proposalCell.lock));
|
|
447
|
+
if (bytesToHex(proposalLockHash) !== bytesToHex(treasuryLockHash)) {
|
|
448
|
+
throw new Error("This registry uses treasury-funded anchors, but the proposal cell is not locked to the registry treasury");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
proposal.proposalCellTxHash = proposalTx;
|
|
452
|
+
proposal.proposalCellIndex = proposalIndex;
|
|
453
|
+
anchorVerified = true;
|
|
454
|
+
}
|
|
455
|
+
else if (b.proposalIndex !== undefined && String(b.proposalIndex).trim()) {
|
|
456
|
+
throw new Error("proposalTx is required when proposalIndex is provided");
|
|
457
|
+
}
|
|
458
|
+
saveProposal(proposal);
|
|
459
|
+
const treasuryLockHash = governanceTreasuryLockHash(governanceHeader);
|
|
460
|
+
const toAddress = String(b.toAddress ?? "").trim();
|
|
461
|
+
const command = treasuryLockHash ? [
|
|
462
|
+
"ckb-firewall",
|
|
463
|
+
"anchor",
|
|
464
|
+
"--proposal", proposal.id,
|
|
465
|
+
"--tx-out", `gov_anchor_tx_${proposal.id}.json`,
|
|
466
|
+
].join(" ") : toAddress ? [
|
|
467
|
+
"ckb-cli",
|
|
468
|
+
"--url", opts.rpcUrl,
|
|
469
|
+
"wallet", "transfer",
|
|
470
|
+
"--to-address", toAddress,
|
|
471
|
+
"--capacity", "62",
|
|
472
|
+
"--to-data", bytesToHex(proposalData),
|
|
473
|
+
"--fee-rate", "1000",
|
|
474
|
+
"--output-format", "json",
|
|
475
|
+
].map((p) => (/\s/.test(p) ? JSON.stringify(p) : p)).join(" ") : null;
|
|
476
|
+
return {
|
|
477
|
+
ok: true,
|
|
478
|
+
proposal,
|
|
479
|
+
proposalData: bytesToHex(proposalData),
|
|
480
|
+
proposalDataHash: proposalDataHashHex,
|
|
481
|
+
reviewDelayMs: proposal.reviewDelayMs,
|
|
482
|
+
anchorVerified,
|
|
483
|
+
treasuryBacked: Boolean(treasuryLockHash),
|
|
484
|
+
command,
|
|
485
|
+
};
|
|
381
486
|
}
|
|
382
487
|
// ── /api/import ───────────────────────────────────────────────────────────────
|
|
383
488
|
function handleImport(body) {
|
|
@@ -388,8 +493,8 @@ function handleImport(body) {
|
|
|
388
493
|
for (const k of requiredStrings)
|
|
389
494
|
if (typeof p[k] !== "string")
|
|
390
495
|
throw new Error(`Missing field: ${k}`);
|
|
391
|
-
if (!Array.isArray(p.votes)
|
|
392
|
-
throw new Error("votes
|
|
496
|
+
if (!Array.isArray(p.votes))
|
|
497
|
+
throw new Error("votes must be an array");
|
|
393
498
|
const computed = computeProposalIdHash({
|
|
394
499
|
action: p.action, lockArgs: p.lockArgs,
|
|
395
500
|
expiresAt: p.expiresAt, evidence: p.evidence,
|
|
@@ -416,31 +521,37 @@ function handleImport(body) {
|
|
|
416
521
|
existing = incoming;
|
|
417
522
|
}
|
|
418
523
|
if (existing.proposalIdHash === incoming.proposalIdHash) {
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
mergedVoteDigest = existing.signatures.length > 0 ? existing.voteDigestHash : incoming.voteDigestHash;
|
|
425
|
-
}
|
|
426
|
-
else {
|
|
427
|
-
const votesByPk = new Map(existing.votes.map((v) => [v.pubkey.toLowerCase(), v]));
|
|
428
|
-
for (const v of incoming.votes) {
|
|
429
|
-
const prev = votesByPk.get(v.pubkey.toLowerCase());
|
|
430
|
-
if (!prev || v.timestamp > prev.timestamp)
|
|
431
|
-
votesByPk.set(v.pubkey.toLowerCase(), v);
|
|
524
|
+
const votesByPk = new Map(existing.votes.map((v) => [v.pubkey.toLowerCase(), v]));
|
|
525
|
+
for (const v of incoming.votes) {
|
|
526
|
+
const prev = votesByPk.get(v.pubkey.toLowerCase());
|
|
527
|
+
if (!prev || v.timestamp > prev.timestamp) {
|
|
528
|
+
votesByPk.set(v.pubkey.toLowerCase(), v);
|
|
432
529
|
}
|
|
433
|
-
mergedVotes = [...votesByPk.values()];
|
|
434
|
-
mergedVoteDigest = computeVoteDigestHash(mergedVotes);
|
|
435
530
|
}
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
sigBySigner.set(s.signerIndex, s);
|
|
439
|
-
const mergedSigs = [...sigBySigner.values()];
|
|
531
|
+
const mergedVotes = [...votesByPk.values()];
|
|
532
|
+
const mergedVoteDigest = computeVoteDigestHash(mergedVotes);
|
|
440
533
|
const rankStatus = (st) => ["pending-review", "voting", "approved", "rejected", "executed"].indexOf(st);
|
|
441
|
-
const merged = {
|
|
534
|
+
const merged = {
|
|
535
|
+
...incoming,
|
|
536
|
+
votes: mergedVotes,
|
|
537
|
+
voteDigestHash: mergedVoteDigest,
|
|
538
|
+
signatures: [],
|
|
539
|
+
status: rankStatus(existing.status) >= rankStatus(incoming.status) ? existing.status : incoming.status,
|
|
540
|
+
};
|
|
541
|
+
const proposalDataHash = existing.proposalDataHash ?? incoming.proposalDataHash;
|
|
542
|
+
const reviewDelayMs = existing.reviewDelayMs ?? incoming.reviewDelayMs;
|
|
543
|
+
const proposalCellTxHash = existing.proposalCellTxHash ?? incoming.proposalCellTxHash;
|
|
544
|
+
const proposalCellIndex = existing.proposalCellIndex ?? incoming.proposalCellIndex;
|
|
545
|
+
if (proposalDataHash !== undefined)
|
|
546
|
+
merged.proposalDataHash = proposalDataHash;
|
|
547
|
+
if (reviewDelayMs !== undefined)
|
|
548
|
+
merged.reviewDelayMs = reviewDelayMs;
|
|
549
|
+
if (proposalCellTxHash !== undefined)
|
|
550
|
+
merged.proposalCellTxHash = proposalCellTxHash;
|
|
551
|
+
if (proposalCellIndex !== undefined)
|
|
552
|
+
merged.proposalCellIndex = proposalCellIndex;
|
|
442
553
|
saveProposal(merged);
|
|
443
|
-
return { ok: true, merged: true, id: incoming.id, votes: mergedVotes.length
|
|
554
|
+
return { ok: true, merged: true, id: incoming.id, votes: mergedVotes.length };
|
|
444
555
|
}
|
|
445
556
|
throw new Error(`A different proposal with ID ${incoming.id} already exists locally`);
|
|
446
557
|
}
|
|
@@ -476,13 +587,14 @@ function _buildGuiHtml(apiDataJson) {
|
|
|
476
587
|
window.TFW_PROPOSALS = ${safeJson(d.proposals ?? [])};
|
|
477
588
|
window.TFW_REGISTRY_ENTRIES = ${safeJson(d.registry?.entries ?? [])};
|
|
478
589
|
window.TFW_META = ${safeJson({
|
|
479
|
-
threshold: 3,
|
|
480
|
-
governanceSetSize: 5,
|
|
590
|
+
threshold: d.registry?.threshold ?? 3,
|
|
591
|
+
governanceSetSize: d.registry?.validatorCount ?? 5,
|
|
481
592
|
reviewWindowHours: 72,
|
|
482
593
|
registryTxHash: d.registry?.txHash ?? null,
|
|
483
594
|
registryError: d.registry?.error ?? null,
|
|
595
|
+
treasury: d.registry?.treasury ?? null,
|
|
484
596
|
yourPubkey: null,
|
|
485
|
-
|
|
597
|
+
proposerName: loadConfig().proposerName ?? null,
|
|
486
598
|
...d.meta,
|
|
487
599
|
})};
|
|
488
600
|
</script>`;
|
|
@@ -812,9 +924,8 @@ function countdown(iso){
|
|
|
812
924
|
return '<span style="color:var(--yellow)">'+h2+'h '+m+'m remaining</span>';
|
|
813
925
|
}
|
|
814
926
|
function countYes(votes){ return (votes||[]).filter(function(v){return v.vote==='yes';}).length; }
|
|
815
|
-
function sigCount(p){ return (p.signatures||[]).length; }
|
|
816
927
|
function reviewPassed(p){ return Date.now()>=new Date(p.reviewWindowEndsAt).getTime(); }
|
|
817
|
-
function isReady(p){ return reviewPassed(p)&&countYes(p.votes)>=3
|
|
928
|
+
function isReady(p){ return reviewPassed(p)&&countYes(p.votes)>=3; }
|
|
818
929
|
function prog(v,max){
|
|
819
930
|
var pct=Math.min(100,Math.round(v/max*100));
|
|
820
931
|
var cls=v>=max?'prog-g':'prog-y';
|
|
@@ -920,12 +1031,6 @@ function renderOverview(){
|
|
|
920
1031
|
action.forEach(function(p){ out+=propCard(p,true); });
|
|
921
1032
|
out+='</div>';
|
|
922
1033
|
}
|
|
923
|
-
// awaiting signatures
|
|
924
|
-
if(approved.length){
|
|
925
|
-
out+='<div class="section"><div class="sec-hdr">✎ Awaiting Signatures</div>';
|
|
926
|
-
approved.forEach(function(p){ out+=propCard(p,true); });
|
|
927
|
-
out+='</div>';
|
|
928
|
-
}
|
|
929
1034
|
// recent
|
|
930
1035
|
var recent=ps.slice().sort(function(a,b){return b.submittedAt.localeCompare(a.submittedAt);}).slice(0,6);
|
|
931
1036
|
if(recent.length){
|
|
@@ -1022,14 +1127,11 @@ function propCard(p,compact){
|
|
|
1022
1127
|
out+='</div>';
|
|
1023
1128
|
out+='<div class="card-foot">';
|
|
1024
1129
|
out+=prog(countYes(p.votes),3);
|
|
1025
|
-
out+=prog(sigCount(p),3);
|
|
1026
1130
|
out+='<span style="flex:1"></span>';
|
|
1027
1131
|
out+=regHint;
|
|
1028
1132
|
// quick action buttons
|
|
1029
|
-
if(
|
|
1133
|
+
if(p.status==='pending-review'||p.status==='voting'){
|
|
1030
1134
|
out+='<button class="btn btn-p" style="padding:3px 10px;font-size:11px" onclick="event.stopPropagation();openVoteForm(\''+h(p.id)+'\')">Vote →</button>';
|
|
1031
|
-
} else if(reviewPassed(p)&&countYes(p.votes)>=3&&p.status!=='executed'&&p.status!=='rejected'){
|
|
1032
|
-
out+='<button class="btn btn-g" style="padding:3px 10px;font-size:11px" onclick="event.stopPropagation();openSignForm(\''+h(p.id)+'\')">Sign →</button>';
|
|
1033
1135
|
} else if(isReady(p)){
|
|
1034
1136
|
out+='<button class="btn btn-p" style="padding:3px 10px;font-size:11px" onclick="event.stopPropagation();openExecuteForm(\''+h(p.id)+'\')">Execute →</button>';
|
|
1035
1137
|
}
|
|
@@ -1109,25 +1211,11 @@ function openProposal(id){
|
|
|
1109
1211
|
});}
|
|
1110
1212
|
out+='</div>';
|
|
1111
1213
|
|
|
1112
|
-
// signatures
|
|
1113
|
-
var sigs=p.signatures||[];
|
|
1114
|
-
out+='<div class="msec"><div class="msec-ttl">Signatures '+prog(sigs.length,3)+'</div>';
|
|
1115
|
-
if(!sigs.length){out+='<div style="color:var(--subtle);font-size:12px">No signatures yet.</div>';}
|
|
1116
|
-
else{sigs.forEach(function(s){
|
|
1117
|
-
out+='<div class="vrow"><span class="vch" style="color:var(--dim);width:64px">Signer #'+h(String(s.signerIndex))+'</span>'
|
|
1118
|
-
+'<code class="vpk">'+h(s.signature.slice(0,22))+'…</code>'
|
|
1119
|
-
+'<span class="vts">'+fmtDate(s.timestamp)+'</span></div>';
|
|
1120
|
-
});}
|
|
1121
|
-
out+='</div>';
|
|
1122
|
-
|
|
1123
1214
|
// Actions
|
|
1124
1215
|
out+='<div class="msec"><div class="msec-ttl">Actions</div><div class="act-btns">';
|
|
1125
|
-
if(
|
|
1216
|
+
if(p.status==='pending-review'||p.status==='voting'){
|
|
1126
1217
|
out+='<button class="btn btn-p" onclick="closeModal();openVoteForm(\''+h(p.id)+'\')">Cast Vote</button>';
|
|
1127
1218
|
}
|
|
1128
|
-
if(reviewPassed(p)&&countYes(p.votes)>=3&&p.status!=='executed'&&p.status!=='rejected'){
|
|
1129
|
-
out+='<button class="btn btn-g" onclick="closeModal();openSignForm(\''+h(p.id)+'\')">Sign</button>';
|
|
1130
|
-
}
|
|
1131
1219
|
if(isReady(p)){
|
|
1132
1220
|
out+='<button class="btn btn-p" onclick="closeModal();openExecuteForm(\''+h(p.id)+'\')">Build Execute TX</button>';
|
|
1133
1221
|
}
|
|
@@ -1139,9 +1227,6 @@ function openProposal(id){
|
|
|
1139
1227
|
if(p.status==='pending-review'||p.status==='voting'){
|
|
1140
1228
|
out+=cmdRow('Vote on this proposal','ckb-firewall vote --proposal '+p.id,'cmd-v');
|
|
1141
1229
|
}
|
|
1142
|
-
if(p.status==='approved'||(reviewPassed(p)&&countYes(p.votes)>=3)){
|
|
1143
|
-
out+=cmdRow('Sign this proposal (3-of-5 required)','ckb-firewall sign --proposal '+p.id,'cmd-s');
|
|
1144
|
-
}
|
|
1145
1230
|
if(isReady(p)){
|
|
1146
1231
|
out+=cmdRow('Execute on-chain','ckb-firewall execute --proposal '+p.id,'cmd-e');
|
|
1147
1232
|
}
|
|
@@ -1186,7 +1271,7 @@ function openAddr(identifier){
|
|
|
1186
1271
|
out+='<div class="card" data-pid="'+h(p.id)+'" style="margin-bottom:8px">';
|
|
1187
1272
|
out+='<div class="card-top">'+actionBadge(p)+'<span class="card-id">#'+h(p.id)+'</span><span style="flex:1"></span>'+statusBadge(p)+'</div>';
|
|
1188
1273
|
out+='<div class="card-meta">'+h(p.classification)+' / '+h(p.severity)+' · '+fmtDate(p.submittedAt)+'</div>';
|
|
1189
|
-
out+='<div class="card-foot">'+prog(countYes(p.votes),3)+
|
|
1274
|
+
out+='<div class="card-foot">'+prog(countYes(p.votes),3)+'</div>';
|
|
1190
1275
|
out+='</div>';
|
|
1191
1276
|
});
|
|
1192
1277
|
out+='</div>';
|
|
@@ -1307,41 +1392,10 @@ function submitVote(id){
|
|
|
1307
1392
|
.catch(function(e){ setDisabled('v-submit',false); showErr('v-err',e.message); });
|
|
1308
1393
|
}
|
|
1309
1394
|
|
|
1310
|
-
// ── sign form ─────────────────────────────────────────────────────────────────
|
|
1311
|
-
function openSignForm(id){
|
|
1312
|
-
var p=D.proposals.find(function(x){return x.id===id;});
|
|
1313
|
-
if(!p)return;
|
|
1314
|
-
var html='';
|
|
1315
|
-
html+='<div class="sec-note sec-note-warn">🔒 Private key zeroed immediately after signing. Never stored or transmitted.</div>';
|
|
1316
|
-
html+='<div class="form-row"><label class="form-lbl" for="s-idx">Signer Index <small>(0–4, your position in governance set)</small></label>';
|
|
1317
|
-
html+='<input id="s-idx" class="fi" type="number" min="0" max="4" placeholder="0–4"></div>';
|
|
1318
|
-
html+='<div class="form-row"><label class="form-lbl" for="s-pk">Private Key <small>(32 bytes, hex)</small></label>';
|
|
1319
|
-
html+='<input id="s-pk" class="fi fi-mono" type="password" placeholder="64 hex chars" autocomplete="off" autocorrect="off" spellcheck="false"></div>';
|
|
1320
|
-
html+='<div id="s-err" class="form-err"></div>';
|
|
1321
|
-
html+='<div id="s-ok" class="form-ok"></div>';
|
|
1322
|
-
html+='<div class="btn-row"><button class="btn btn-g" id="s-submit" onclick="submitSign(\''+h(id)+'\')">Sign</button><button class="btn btn-d" onclick="closeAct()">Cancel</button></div>';
|
|
1323
|
-
openAct('Sign — Proposal #'+h(id),html);
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
function submitSign(id){
|
|
1327
|
-
hideMsg('s-err'); hideMsg('s-ok'); setDisabled('s-submit',true);
|
|
1328
|
-
var body={proposalId:id,signerIndex:Number(fval('s-idx')),privateKey:fval('s-pk').trim()};
|
|
1329
|
-
fetch('/api/sign',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
|
|
1330
|
-
.then(function(r){return r.json();})
|
|
1331
|
-
.then(function(d){
|
|
1332
|
-
setDisabled('s-submit',false);
|
|
1333
|
-
document.getElementById('s-pk').value='';
|
|
1334
|
-
if(!d.ok){showErr('s-err',d.error||'Unknown error');return;}
|
|
1335
|
-
showOk('s-ok','Signed! Sigs: '+d.sigCount+'/'+d.effectiveThreshold+(d.ready?' — Ready to execute!':''));
|
|
1336
|
-
load().then(function(){ setTimeout(closeAct,1500); });
|
|
1337
|
-
})
|
|
1338
|
-
.catch(function(e){ setDisabled('s-submit',false); showErr('s-err',e.message); });
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
1395
|
// ── execute form ──────────────────────────────────────────────────────────────
|
|
1342
1396
|
function openExecuteForm(id){
|
|
1343
1397
|
var html='';
|
|
1344
|
-
html+='<div class="sec-note sec-note-tip">⚠ This verifies
|
|
1398
|
+
html+='<div class="sec-note sec-note-tip">⚠ This verifies validator votes and builds the transaction JSON for download. Submit it via ckb-cli or a CKB wallet.</div>';
|
|
1345
1399
|
html+='<div id="e-err" class="form-err"></div>';
|
|
1346
1400
|
html+='<div id="e-ok" class="form-ok"></div>';
|
|
1347
1401
|
html+='<div class="btn-row"><button class="btn btn-p" id="e-submit" onclick="submitExecute(\''+h(id)+'\')">Build & Download TX</button><button class="btn btn-d" onclick="closeAct()">Cancel</button></div>';
|
|
@@ -1398,7 +1452,7 @@ function submitImport(){
|
|
|
1398
1452
|
.then(function(d){
|
|
1399
1453
|
setDisabled('i-submit',false);
|
|
1400
1454
|
if(!d.ok){showErr('i-err',d.error||'Unknown error');return;}
|
|
1401
|
-
showOk('i-ok',d.merged?'Merged proposal #'+d.id+' ('+d.votes+' votes
|
|
1455
|
+
showOk('i-ok',d.merged?'Merged proposal #'+d.id+' ('+d.votes+' votes)':'Imported proposal #'+d.id);
|
|
1402
1456
|
load().then(function(){ setTimeout(closeAct,1500); });
|
|
1403
1457
|
})
|
|
1404
1458
|
.catch(function(e){ setDisabled('i-submit',false); showErr('i-err',e.message); });
|
|
@@ -1500,9 +1554,9 @@ async function handleRequest(req, res, opts) {
|
|
|
1500
1554
|
}
|
|
1501
1555
|
return;
|
|
1502
1556
|
}
|
|
1503
|
-
if (url === "/api/
|
|
1557
|
+
if (url === "/api/anchor" && method === "POST") {
|
|
1504
1558
|
try {
|
|
1505
|
-
apiOk(res, await
|
|
1559
|
+
apiOk(res, await handleAnchor(await readJsonBody(req), opts));
|
|
1506
1560
|
}
|
|
1507
1561
|
catch (e) {
|
|
1508
1562
|
apiErr(res, e instanceof Error ? e.message : String(e));
|