@ckb-firewall/cli 0.3.1 → 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/gui.d.ts +6 -0
- package/dist/commands/gui.d.ts.map +1 -0
- package/dist/commands/gui.js +112 -0
- package/dist/commands/gui.js.map +1 -0
- 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 +101 -16
- package/dist/index.js.map +1 -1
- package/dist/lib/Transaction Firewall.html +201 -0
- 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 +4266 -0
- package/dist/lib/gui-server.d.ts +12 -0
- package/dist/lib/gui-server.d.ts.map +1 -0
- package/dist/lib/gui-server.js +1634 -0
- package/dist/lib/gui-server.js.map +1 -0
- 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/portless.d.ts +41 -0
- package/dist/lib/portless.d.ts.map +1 -0
- package/dist/lib/portless.js +194 -0
- package/dist/lib/portless.js.map +1 -0
- 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 +2 -2
package/dist/commands/execute.js
CHANGED
|
@@ -5,260 +5,334 @@ import logSymbols from "log-symbols";
|
|
|
5
5
|
import ora from "ora";
|
|
6
6
|
import inquirer from "inquirer";
|
|
7
7
|
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { loadProposal, saveProposal, listProposals, isReviewWindowPassed, isVoteApproved, isReadyToExecute, computeVoteDigestHash, voteSigningMessage, signingMessage, SIG_THRESHOLD, } from "../lib/proposals.js";
|
|
12
|
-
import { encodeRegistryPayload, extractGovernanceHeaderRaw, parseGovernanceHeader, insertSorted, removeEntry, bytesToHex, hexToBytes, strip0x, } from "../lib/blkl.js";
|
|
8
|
+
import { isReviewWindowPassed, isVoteApproved, listProposals, loadProposal, saveProposal, voteSigningMessage, computeVoteDigestHash, } from "../lib/proposals.js";
|
|
9
|
+
import { bytesToHex, governanceTreasuryLockHash, hexToBytes, scriptToMoleculeBytes } from "../lib/blkl.js";
|
|
10
|
+
import { getLiveCell, getLiveCellsByLock } from "../lib/rpc.js";
|
|
13
11
|
import { verifyMerkleProof } from "../lib/validator-set.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
12
|
+
import { assertProposalCellMatches, assertProposalAnchorTypeMatches, loadRegistryStateForProposal, proposalV4Fields, } from "../lib/governance-v4.js";
|
|
13
|
+
import { buildGov1WitnessV4, buildValidatorVoteWitness, buildWitnessArgs, ckbBlake2b, encodeRelativeTimestampSince, } from "../lib/witness.js";
|
|
14
|
+
import { SECP256K1_DEP_GROUP, TESTNET_CONTRACT_OUTPOINTS, TESTNET_GOVERNANCE_PUBKEYS, TESTNET_REGISTRY_CELL, TESTNET_RPC_URL, TESTNET_TREASURY_LOCK_DEP, warnIfTrivialTestKeys, } from "../lib/defaults.js";
|
|
16
15
|
import { printHints } from "../lib/hints.js";
|
|
16
|
+
import { hexCapacity, occupiedCapacityShannons, parseCapacity } from "../lib/capacity.js";
|
|
17
|
+
import { parseCellDepList } from "../lib/tx-deps.js";
|
|
18
|
+
const DEFAULT_FEE_SHANNONS = 100000n;
|
|
19
|
+
// Treasury-lock change outputs have 64-byte args — minimum occupied capacity is
|
|
20
|
+
// 8 (capacity field) + 117 (lock script molecule) = 125 CKB = 12,500,000,000 shannons.
|
|
21
|
+
const MIN_CHANGE_SHANNONS = 125n * 100000000n;
|
|
22
|
+
export function executeDefaults() {
|
|
23
|
+
return {
|
|
24
|
+
rpcUrl: TESTNET_RPC_URL,
|
|
25
|
+
registryTx: TESTNET_REGISTRY_CELL.txHash,
|
|
26
|
+
registryIndex: String(TESTNET_REGISTRY_CELL.index),
|
|
27
|
+
proposalAnchorCodeTx: TESTNET_CONTRACT_OUTPOINTS.proposalAnchor.txHash,
|
|
28
|
+
proposalAnchorCodeIndex: String(TESTNET_CONTRACT_OUTPOINTS.proposalAnchor.index),
|
|
29
|
+
txOut: "gov_execute_tx.json",
|
|
30
|
+
sign: false,
|
|
31
|
+
fromAccount: "",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function parseOutputIndex(value, name) {
|
|
35
|
+
if (!/^\d+$/.test(value.trim())) {
|
|
36
|
+
throw new Error(`${name} must be a non-negative integer.`);
|
|
37
|
+
}
|
|
38
|
+
return Number.parseInt(value.trim(), 10);
|
|
39
|
+
}
|
|
40
|
+
function parseOutpointList(values, name) {
|
|
41
|
+
return (values ?? []).map((value) => {
|
|
42
|
+
const raw = value.trim();
|
|
43
|
+
const match = /^(0x[0-9a-fA-F]{64})(?::|#)(\d+)$/.exec(raw);
|
|
44
|
+
if (!match) {
|
|
45
|
+
throw new Error(`${name} must be formatted as <tx-hash>:<index>. Got "${value}".`);
|
|
46
|
+
}
|
|
47
|
+
return { txHash: match[1], index: Number.parseInt(match[2], 10) };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
17
50
|
export async function executeCommand(opts) {
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
51
|
+
// --ready: find every proposal with votes complete + window passed + anchored, execute sequentially.
|
|
52
|
+
if (opts.ready && !opts.proposal?.trim()) {
|
|
53
|
+
const candidates = listProposals().filter((p) => p.status !== "executed" &&
|
|
54
|
+
p.status !== "rejected" &&
|
|
55
|
+
p.proposalCellTxHash &&
|
|
56
|
+
isVoteApproved(p) &&
|
|
57
|
+
isReviewWindowPassed(p));
|
|
58
|
+
if (candidates.length === 0) {
|
|
59
|
+
console.log(logSymbols.info, chalk.dim("No proposals are ready to execute right now."));
|
|
60
|
+
return;
|
|
26
61
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
62
|
+
console.log(logSymbols.info, `${candidates.length} proposal(s) ready — executing in sequence...`);
|
|
63
|
+
let passed = 0;
|
|
64
|
+
let failed = 0;
|
|
65
|
+
for (const p of candidates) {
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(chalk.bold(`→ ${p.id} (${p.action} ${p.lockArgs.slice(0, 24)}…)`));
|
|
68
|
+
try {
|
|
69
|
+
await executeCommand({ ...opts, ready: false, proposal: p.id });
|
|
70
|
+
passed++;
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error(logSymbols.error, chalk.red(`Failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
74
|
+
failed++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(passed > 0 ? logSymbols.success : logSymbols.warning, `${passed} executed, ${failed} failed.`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const proposalId = opts.proposal?.trim();
|
|
82
|
+
if (!proposalId) {
|
|
83
|
+
console.error(logSymbols.error, chalk.red("--proposal <id> or --ready is required."));
|
|
84
|
+
process.exit(1);
|
|
39
85
|
}
|
|
40
86
|
const proposal = loadProposal(proposalId);
|
|
41
|
-
|
|
87
|
+
const proposalCellTx = opts.proposalTx?.trim() || proposal.proposalCellTxHash?.trim();
|
|
88
|
+
const proposalIndexRaw = opts.proposalIndex ?? (proposal.proposalCellIndex === undefined ? undefined : String(proposal.proposalCellIndex));
|
|
89
|
+
if (!proposalCellTx || proposalIndexRaw === undefined) {
|
|
90
|
+
console.error(logSymbols.error, chalk.red("Proposal cell outpoint is required."));
|
|
91
|
+
console.error(chalk.dim(`Run ckb-firewall anchor --proposal ${proposal.id} --proposal-tx <tx-hash> --proposal-index <n>, ` +
|
|
92
|
+
"or pass --proposal-tx/--proposal-index directly."));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
let registryIndex;
|
|
96
|
+
let proposalIndex;
|
|
97
|
+
let proposalAnchorCodeIndex;
|
|
98
|
+
let treasuryLockDeps;
|
|
99
|
+
try {
|
|
100
|
+
registryIndex = parseOutputIndex(opts.registryIndex, "--registry-index");
|
|
101
|
+
proposalIndex = parseOutputIndex(proposalIndexRaw, "--proposal-index");
|
|
102
|
+
proposalAnchorCodeIndex = opts.proposalAnchorCodeIndex === undefined
|
|
103
|
+
? undefined
|
|
104
|
+
: parseOutputIndex(opts.proposalAnchorCodeIndex, "--proposal-anchor-code-index");
|
|
105
|
+
treasuryLockDeps = parseCellDepList(opts.treasuryLockDep, "--treasury-lock-dep");
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.error(logSymbols.error, chalk.red(err instanceof Error ? err.message : String(err)));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
42
111
|
if (proposal.status === "executed") {
|
|
43
|
-
console.log(logSymbols.success, chalk.green(`Already executed
|
|
112
|
+
console.log(logSymbols.success, chalk.green(`Already executed: ${proposal.txHash ?? "unknown tx"}`));
|
|
44
113
|
process.exit(0);
|
|
45
114
|
}
|
|
46
115
|
if (!isReviewWindowPassed(proposal)) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
console.log(logSymbols.error, chalk.red(`Review window not passed — ${h}h remaining.`));
|
|
116
|
+
console.error(logSymbols.error, chalk.red("Local review window has not passed."));
|
|
117
|
+
console.error(chalk.dim(` Review ends: ${proposal.reviewWindowEndsAt}`));
|
|
50
118
|
process.exit(1);
|
|
51
119
|
}
|
|
52
|
-
// The review window is also enforced on-chain: the governance cell input's `since`
|
|
53
|
-
// field is set to an absolute MTP timestamp, and governance-lock v3 rejects the
|
|
54
|
-
// transaction if the chain's median time has not yet reached reviewWindowEndsAt.
|
|
55
120
|
if (!isVoteApproved(proposal)) {
|
|
56
|
-
console.
|
|
57
|
-
console.log(chalk.dim(` Use: ckb-firewall vote --proposal ${proposal.id}`));
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
if (proposal.signatures.length < SIG_THRESHOLD) {
|
|
61
|
-
console.log(logSymbols.error, chalk.red(`Only ${proposal.signatures.length}/${SIG_THRESHOLD} signatures — need more.`));
|
|
62
|
-
console.log(chalk.dim(` Use: ckb-firewall sign --proposal ${proposal.id}`));
|
|
121
|
+
console.error(logSymbols.error, chalk.red("Vote threshold not met."));
|
|
63
122
|
process.exit(1);
|
|
64
123
|
}
|
|
65
|
-
// P0-8: reject if the proposal's expiry has already passed.
|
|
66
124
|
if (proposal.expiresAt !== "0") {
|
|
67
125
|
const expiryMs = BigInt(proposal.expiresAt) * 1000n;
|
|
68
126
|
if (BigInt(Date.now()) >= expiryMs) {
|
|
69
|
-
console.
|
|
70
|
-
console.log(chalk.dim(" A new proposal must be created for this blacklist entry."));
|
|
127
|
+
console.error(logSymbols.error, chalk.red(`Proposal entry has already expired (${new Date(Number(expiryMs)).toISOString()}).`));
|
|
71
128
|
process.exit(1);
|
|
72
129
|
}
|
|
73
130
|
}
|
|
74
|
-
|
|
131
|
+
const spinner = ora("Building GOV1 v4 registry update transaction").start();
|
|
132
|
+
let state;
|
|
133
|
+
let proposalDataHash;
|
|
134
|
+
let reviewDelayMs;
|
|
135
|
+
let proposalCell;
|
|
136
|
+
let proposalChangeCapacity;
|
|
137
|
+
let treasuryCells = [];
|
|
138
|
+
let registryOutputCapacity;
|
|
139
|
+
let extraTreasuryOutputCapacity = 0n;
|
|
140
|
+
let proposalAnchorCellDep = null;
|
|
141
|
+
let treasuryLockScript;
|
|
142
|
+
try {
|
|
143
|
+
state = await loadRegistryStateForProposal(opts.rpcUrl, opts.registryTx, registryIndex, proposal);
|
|
144
|
+
warnIfTrivialTestKeys(TESTNET_GOVERNANCE_PUBKEYS);
|
|
145
|
+
proposalCell = await getLiveCell(opts.rpcUrl, proposalCellTx, proposalIndex);
|
|
146
|
+
proposalDataHash = assertProposalCellMatches(proposal, proposalCell.data, state.registryTypeIdValue);
|
|
147
|
+
const fields = proposalV4Fields(proposal, state.registryTypeIdValue);
|
|
148
|
+
reviewDelayMs = fields.reviewDelayMs;
|
|
149
|
+
if (bytesToHex(fields.proposalDataHash) !== bytesToHex(proposalDataHash)) {
|
|
150
|
+
throw new Error("Internal proposal hash mismatch.");
|
|
151
|
+
}
|
|
152
|
+
proposal.proposalDataHash = bytesToHex(proposalDataHash);
|
|
153
|
+
proposal.reviewDelayMs = reviewDelayMs.toString();
|
|
154
|
+
proposal.proposalCellTxHash = proposalCellTx;
|
|
155
|
+
proposal.proposalCellIndex = proposalIndex;
|
|
156
|
+
const proposalCapacity = parseCapacity(proposalCell.capacity);
|
|
157
|
+
proposalChangeCapacity = proposalCapacity - DEFAULT_FEE_SHANNONS;
|
|
158
|
+
const treasuryLockHash = governanceTreasuryLockHash(state.governanceHeader);
|
|
159
|
+
const minChange = treasuryLockHash
|
|
160
|
+
? MIN_CHANGE_SHANNONS
|
|
161
|
+
: occupiedCapacityShannons({ lock: proposalCell.lock, type: null, data: "0x" });
|
|
162
|
+
if (proposalChangeCapacity < minChange) {
|
|
163
|
+
throw new Error(`Proposal cell capacity ${proposalCapacity} shannons is too small to return change after fee. ` +
|
|
164
|
+
`Need at least ${minChange + DEFAULT_FEE_SHANNONS} shannons.`);
|
|
165
|
+
}
|
|
166
|
+
if (treasuryLockHash) {
|
|
167
|
+
treasuryLockScript = state.governanceHeader?.treasuryLockScript;
|
|
168
|
+
if (!treasuryLockScript) {
|
|
169
|
+
throw new Error("Registry treasury lock script is missing from the governance header. " +
|
|
170
|
+
"A full treasury lock script (v3 header) is required to safely route change capacity.");
|
|
171
|
+
}
|
|
172
|
+
assertProposalAnchorTypeMatches({
|
|
173
|
+
proposalCellType: proposalCell.type,
|
|
174
|
+
registryTypeIdValue: state.registryTypeIdValue,
|
|
175
|
+
governanceHeader: state.governanceHeader,
|
|
176
|
+
reclaimDelayMs: reviewDelayMs,
|
|
177
|
+
});
|
|
178
|
+
if (!opts.proposalAnchorCodeTx || proposalAnchorCodeIndex === undefined) {
|
|
179
|
+
throw new Error("Typed proposal anchors require --proposal-anchor-code-tx and --proposal-anchor-code-index so the transaction can include the anchor type script cell_dep.");
|
|
180
|
+
}
|
|
181
|
+
proposalAnchorCellDep = {
|
|
182
|
+
out_point: { tx_hash: opts.proposalAnchorCodeTx, index: `0x${proposalAnchorCodeIndex.toString(16)}` },
|
|
183
|
+
dep_type: "code",
|
|
184
|
+
};
|
|
185
|
+
// Proposal cells now use governance-lock (not treasury secp256k1), so no lock check here.
|
|
186
|
+
const rawOutpoints = parseOutpointList(opts.treasuryCell, "--treasury-cell");
|
|
187
|
+
const treasuryOutpoints = Array.from(new Map(rawOutpoints.map((op) => [`${op.txHash}:${op.index}`, op])).values());
|
|
188
|
+
treasuryCells = treasuryOutpoints.length > 0
|
|
189
|
+
? await Promise.all(treasuryOutpoints.map((outpoint) => getLiveCell(opts.rpcUrl, outpoint.txHash, outpoint.index)))
|
|
190
|
+
: [];
|
|
191
|
+
let treasuryInputCapacity = 0n;
|
|
192
|
+
for (const cell of treasuryCells) {
|
|
193
|
+
const lockHash = ckbBlake2b(scriptToMoleculeBytes(cell.lock));
|
|
194
|
+
if (bytesToHex(lockHash) !== bytesToHex(treasuryLockHash)) {
|
|
195
|
+
throw new Error(`Treasury cell ${cell.txHash}:${cell.index} is not locked to the registry treasury.`);
|
|
196
|
+
}
|
|
197
|
+
if (cell.type) {
|
|
198
|
+
throw new Error(`Treasury cell ${cell.txHash}:${cell.index} has a type script; only plain treasury cells are supported.`);
|
|
199
|
+
}
|
|
200
|
+
if (cell.data !== "0x") {
|
|
201
|
+
throw new Error(`Treasury cell ${cell.txHash}:${cell.index} has data; only empty treasury cells are supported.`);
|
|
202
|
+
}
|
|
203
|
+
treasuryInputCapacity += parseCapacity(cell.capacity);
|
|
204
|
+
}
|
|
205
|
+
const registryInputCapacity = parseCapacity(state.cell.capacity);
|
|
206
|
+
const minRegistryCapacity = occupiedCapacityShannons({
|
|
207
|
+
lock: state.cell.lock,
|
|
208
|
+
type: state.cell.type,
|
|
209
|
+
data: state.newBlkl,
|
|
210
|
+
});
|
|
211
|
+
registryOutputCapacity = minRegistryCapacity;
|
|
212
|
+
const registryGrowth = registryOutputCapacity > registryInputCapacity ? registryOutputCapacity - registryInputCapacity : 0n;
|
|
213
|
+
const registryShrink = registryInputCapacity > registryOutputCapacity ? registryInputCapacity - registryOutputCapacity : 0n;
|
|
214
|
+
// Registry growth must always be funded entirely by treasury inputs.
|
|
215
|
+
// The proposal-anchor contract requires the full proposal cell capacity (minus fee)
|
|
216
|
+
// to be returned to the treasury, so proposalChangeCapacity must not be reduced.
|
|
217
|
+
if (registryGrowth > treasuryInputCapacity) {
|
|
218
|
+
if (!treasuryOutpoints.length) {
|
|
219
|
+
// Auto-discover autonomous treasury-lock cells (keyless — no signature required).
|
|
220
|
+
const candidates = await getLiveCellsByLock(opts.rpcUrl, treasuryLockScript, 100);
|
|
221
|
+
for (const cell of candidates) {
|
|
222
|
+
if (cell.type || cell.data !== "0x")
|
|
223
|
+
continue;
|
|
224
|
+
treasuryCells.push(cell);
|
|
225
|
+
treasuryInputCapacity += parseCapacity(cell.capacity);
|
|
226
|
+
if (treasuryInputCapacity >= registryGrowth)
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (registryGrowth > treasuryInputCapacity) {
|
|
231
|
+
throw new Error(`Registry update needs ${registryGrowth} shannons of additional capacity for growth. ` +
|
|
232
|
+
`Treasury pool is insufficient — donate CKB: ckb-firewall donate`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
extraTreasuryOutputCapacity = treasuryInputCapacity - registryGrowth + registryShrink;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
registryOutputCapacity = parseCapacity(state.cell.capacity);
|
|
239
|
+
}
|
|
240
|
+
spinner.succeed("GOV1 v4 transaction inputs verified");
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
spinner.fail("Could not build GOV1 v4 transaction");
|
|
244
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
// ── ECDSA signature verification ─────────────────────────────────────────
|
|
75
248
|
for (const v of proposal.votes) {
|
|
76
249
|
const sigBytes = hexToBytes(v.signature);
|
|
77
250
|
if (sigBytes.length !== 65) {
|
|
78
|
-
console.error(logSymbols.error, chalk.red(`Vote from ${v.pubkey.slice(0, 14)}
|
|
251
|
+
console.error(logSymbols.error, chalk.red(`Vote from ${v.pubkey.slice(0, 14)}... has invalid signature length.`));
|
|
79
252
|
process.exit(1);
|
|
80
253
|
}
|
|
81
254
|
const msgHash = voteSigningMessage(proposal.proposalIdHash, v.vote, v.timestamp, v.pubkey);
|
|
82
|
-
// Rebuild [recovery_id, r, s] from stored [r, s, recovery_id].
|
|
83
255
|
const sig65 = new Uint8Array(65);
|
|
84
|
-
sig65[0] = sigBytes[64];
|
|
256
|
+
sig65[0] = sigBytes[64];
|
|
85
257
|
sig65.set(sigBytes.subarray(0, 64), 1);
|
|
86
258
|
let recoveredPubkey;
|
|
87
259
|
try {
|
|
88
|
-
|
|
260
|
+
// prehash:false — msgHash is already blake2b; skip noble/curves' internal sha256 step.
|
|
261
|
+
recoveredPubkey = bytesToHex(new Uint8Array(secp256k1.recoverPublicKey(sig65, msgHash, { prehash: false })));
|
|
89
262
|
}
|
|
90
263
|
catch {
|
|
91
|
-
console.error(logSymbols.error, chalk.red(`Vote from ${v.pubkey.slice(0, 14)}
|
|
264
|
+
console.error(logSymbols.error, chalk.red(`Vote from ${v.pubkey.slice(0, 14)}... has unrecoverable signature.`));
|
|
92
265
|
process.exit(1);
|
|
93
266
|
}
|
|
94
267
|
if (recoveredPubkey !== v.pubkey) {
|
|
95
|
-
console.error(logSymbols.error, chalk.red(`Vote signature does not match pubkey ${v.pubkey.slice(0, 14)}
|
|
268
|
+
console.error(logSymbols.error, chalk.red(`Vote signature does not match pubkey ${v.pubkey.slice(0, 14)}...`));
|
|
96
269
|
process.exit(1);
|
|
97
270
|
}
|
|
98
271
|
}
|
|
99
|
-
// ──
|
|
100
|
-
|
|
101
|
-
|
|
272
|
+
// ── voteDigestHash integrity check ───────────────────────────────────────
|
|
273
|
+
// Recompute from the actual vote records in the proposal file. If the file was
|
|
274
|
+
// tampered with (votes added/removed/modified after signing), this catches it
|
|
275
|
+
// client-side before submitting a transaction that would fail on-chain.
|
|
276
|
+
const recomputedDigest = computeVoteDigestHash(proposal.votes);
|
|
277
|
+
if (recomputedDigest.toLowerCase() !== proposal.voteDigestHash.toLowerCase()) {
|
|
278
|
+
console.error(logSymbols.error, chalk.red("Vote digest mismatch — proposal vote data may have been tampered with."));
|
|
279
|
+
console.error(chalk.dim(` Stored: ${proposal.voteDigestHash}`));
|
|
280
|
+
console.error(chalk.dim(` Recomputed: ${recomputedDigest}`));
|
|
102
281
|
process.exit(1);
|
|
103
282
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
cell
|
|
110
|
-
spinner.succeed("Registry cell loaded");
|
|
111
|
-
}
|
|
112
|
-
catch (err) {
|
|
113
|
-
spinner.fail("Could not fetch registry cell");
|
|
114
|
-
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
283
|
+
// ── on-chain Merkle membership verification ───────────────────────────────
|
|
284
|
+
// Every vote must belong to the current on-chain validator set. These checks
|
|
285
|
+
// mirror what governance-lock runs at consensus — any failure here means the
|
|
286
|
+
// transaction would be rejected on-chain.
|
|
287
|
+
if (!state.governanceHeader) {
|
|
288
|
+
console.error(logSymbols.error, chalk.red("Could not parse governance header from registry cell — cannot verify vote authorization."));
|
|
115
289
|
process.exit(1);
|
|
116
290
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
currentPayload = parseRegistryPayload(cell.data);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
console.error(logSymbols.error, chalk.red("Registry cell does not contain a valid BLKL payload."));
|
|
291
|
+
if (state.governanceHeader.validatorCount === 0) {
|
|
292
|
+
console.error(logSymbols.error, chalk.red("Registry governance header has zero validators — cannot authorize votes."));
|
|
123
293
|
process.exit(1);
|
|
124
294
|
}
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const govHeader = govHeaderRaw ? parseGovernanceHeader(govHeaderRaw) : null;
|
|
130
|
-
// Warn if the live on-chain committee uses known trivial test keys.
|
|
131
|
-
if (govHeader && govHeader.pubkeys.length > 0) {
|
|
132
|
-
warnIfTrivialTestKeys(govHeader.pubkeys);
|
|
133
|
-
}
|
|
134
|
-
// M1: verify signature count against the on-chain threshold, not the compile-time default.
|
|
135
|
-
if (govHeader && govHeader.threshold > 0) {
|
|
136
|
-
const onChainThreshold = govHeader.threshold;
|
|
137
|
-
if (onChainThreshold !== SIG_THRESHOLD) {
|
|
138
|
-
process.stderr.write(`Warning: on-chain signature threshold (${onChainThreshold}) differs from compile-time default (${SIG_THRESHOLD}). ` +
|
|
139
|
-
`Using the on-chain value.\n`);
|
|
140
|
-
}
|
|
141
|
-
if (proposal.signatures.length < onChainThreshold) {
|
|
142
|
-
console.error(logSymbols.error, chalk.red(`Only ${proposal.signatures.length}/${onChainThreshold} signatures — need more (on-chain threshold).`));
|
|
143
|
-
console.error(chalk.dim(` Use: ckb-firewall sign --proposal ${proposal.id}`));
|
|
295
|
+
const rootHex = bytesToHex(state.governanceHeader.validatorMerkleRoot);
|
|
296
|
+
for (const v of proposal.votes) {
|
|
297
|
+
if (!Array.isArray(v.merkleProof) || typeof v.merkleLeafIndex !== "number") {
|
|
298
|
+
console.error(logSymbols.error, chalk.red(`Vote from ${v.pubkey.slice(0, 14)}... is missing a Merkle proof — re-cast this vote with the current CLI.`));
|
|
144
299
|
process.exit(1);
|
|
145
300
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
for (const v of proposal.votes) {
|
|
150
|
-
if (!Array.isArray(v.merkleProof) || typeof v.merkleLeafIndex !== "number") {
|
|
151
|
-
console.error(logSymbols.error, chalk.red(`Vote from ${v.pubkey.slice(0, 14)}… is missing a Merkle proof — re-cast the vote with the current CLI.`));
|
|
152
|
-
process.exit(1);
|
|
153
|
-
}
|
|
154
|
-
const valid = verifyMerkleProof(rootHex, v.pubkey, v.merkleProof, v.merkleLeafIndex);
|
|
155
|
-
if (!valid) {
|
|
156
|
-
console.error(logSymbols.error, chalk.red(`Vote from ${v.pubkey.slice(0, 14)}… is not in the on-chain validator set.`));
|
|
157
|
-
process.exit(1);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
// ── build new BLKL payload ────────────────────────────────────────────────
|
|
162
|
-
let newEntries;
|
|
163
|
-
if (proposal.action === "add") {
|
|
164
|
-
if (currentPayload.entries.some((e) => strip0x(e.identifier).toLowerCase() === strip0x(proposal.lockArgs).toLowerCase())) {
|
|
165
|
-
console.log(logSymbols.warning, chalk.yellow(`${proposal.lockArgs} is already in the registry.`));
|
|
166
|
-
process.exit(0);
|
|
167
|
-
}
|
|
168
|
-
newEntries = insertSorted(currentPayload.entries, {
|
|
169
|
-
identifier: proposal.lockArgs,
|
|
170
|
-
expiresAt: BigInt(proposal.expiresAt),
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
newEntries = removeEntry(currentPayload.entries, proposal.lockArgs);
|
|
175
|
-
if (newEntries.length === currentPayload.entries.length) {
|
|
176
|
-
console.log(logSymbols.warning, chalk.yellow(`${proposal.lockArgs} is not in the registry.`));
|
|
177
|
-
process.exit(0);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
const newPayload = { version: currentPayload.version, entries: newEntries };
|
|
181
|
-
const newBlkl = encodeRegistryPayload(newPayload, govHeaderRaw ?? undefined);
|
|
182
|
-
const newRoot = ckbBlake2b(newBlkl);
|
|
183
|
-
// C-4: verify governance signer signatures against on-chain pubkeys from governance header.
|
|
184
|
-
// Use the v3 signing message (includes reviewWindowEndMs) to match what sign.ts produced.
|
|
185
|
-
const reviewWindowEndMsForVerify = BigInt(new Date(proposal.reviewWindowEndsAt).getTime());
|
|
186
|
-
if (govHeader && govHeader.pubkeys.length > 0) {
|
|
187
|
-
const msgHash = signingMessage(proposal, oldRoot, newRoot, reviewWindowEndMsForVerify);
|
|
188
|
-
for (const s of proposal.signatures) {
|
|
189
|
-
if (!Number.isInteger(s.signerIndex) || s.signerIndex < 0 || s.signerIndex >= govHeader.pubkeys.length) {
|
|
190
|
-
console.error(logSymbols.error, chalk.red(`Signer index ${s.signerIndex} is out of range for the on-chain governance committee (${govHeader.pubkeys.length} signers).`));
|
|
191
|
-
process.exit(1);
|
|
192
|
-
}
|
|
193
|
-
const sigBytes = hexToBytes(s.signature);
|
|
194
|
-
if (sigBytes.length !== 65) {
|
|
195
|
-
console.error(logSymbols.error, chalk.red(`Governance signature from signer ${s.signerIndex} has invalid length.`));
|
|
196
|
-
process.exit(1);
|
|
197
|
-
}
|
|
198
|
-
// Rebuild [recovery_id, r, s] from stored [r, s, recovery_id].
|
|
199
|
-
const sig65 = new Uint8Array(65);
|
|
200
|
-
sig65[0] = sigBytes[64]; // length === 65 verified above
|
|
201
|
-
sig65.set(sigBytes.subarray(0, 64), 1);
|
|
202
|
-
let recoveredPubkey;
|
|
203
|
-
try {
|
|
204
|
-
recoveredPubkey = bytesToHex(new Uint8Array(secp256k1.recoverPublicKey(sig65, msgHash)));
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
console.error(logSymbols.error, chalk.red(`Governance signature from signer ${s.signerIndex} is unrecoverable.`));
|
|
208
|
-
process.exit(1);
|
|
209
|
-
}
|
|
210
|
-
const expectedPubkey = bytesToHex(govHeader.pubkeys[s.signerIndex]);
|
|
211
|
-
if (recoveredPubkey !== expectedPubkey) {
|
|
212
|
-
console.error(logSymbols.error, chalk.red(`Governance signature from signer ${s.signerIndex} does not match the on-chain pubkey — proposal may have been tampered.`));
|
|
213
|
-
process.exit(1);
|
|
214
|
-
}
|
|
301
|
+
if (!verifyMerkleProof(rootHex, v.pubkey, v.merkleProof, v.merkleLeafIndex)) {
|
|
302
|
+
console.error(logSymbols.error, chalk.red(`Vote from ${v.pubkey.slice(0, 14)}... is not in the on-chain validator set.`));
|
|
303
|
+
process.exit(1);
|
|
215
304
|
}
|
|
216
305
|
}
|
|
217
|
-
// ── build GOV1 v3 witness with real signatures ────────────────────────────
|
|
218
|
-
// P0-2: recompute voteDigestHash from votes and verify it matches the stored value.
|
|
219
|
-
const recomputedVoteDigest = computeVoteDigestHash(proposal.votes);
|
|
220
|
-
if (recomputedVoteDigest !== proposal.voteDigestHash) {
|
|
221
|
-
console.error(logSymbols.error, chalk.red("Vote digest hash mismatch — proposal votes may have been tampered."));
|
|
222
|
-
console.error(chalk.dim(` Stored: ${proposal.voteDigestHash}`));
|
|
223
|
-
console.error(chalk.dim(` Recomputed: ${recomputedVoteDigest}`));
|
|
224
|
-
process.exit(1);
|
|
225
|
-
}
|
|
226
|
-
// reviewWindowEndMs is included in the GOV1 v3 witness and the signing preimage,
|
|
227
|
-
// binding signers to the review window end time. governance-lock verifies the input's
|
|
228
|
-
// `since` field is an absolute timestamp >= this value, enforcing the review window on-chain.
|
|
229
|
-
const reviewWindowEndMs = BigInt(new Date(proposal.reviewWindowEndsAt).getTime());
|
|
230
306
|
const proposalIdBytes = hexToBytes(proposal.proposalIdHash);
|
|
231
307
|
const voteDigestBytes = hexToBytes(proposal.voteDigestHash);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
sig: hexToBytes(s.signature),
|
|
237
|
-
}));
|
|
238
|
-
// v3: GOV1 binding (with review window) in input_type, signer entries in lock field.
|
|
239
|
-
const gov1 = buildGov1WitnessV3({
|
|
308
|
+
const yesVotes = proposal.votes
|
|
309
|
+
.filter((v) => v.vote === "yes")
|
|
310
|
+
.sort((a, b) => a.pubkey.localeCompare(b.pubkey));
|
|
311
|
+
const gov1 = buildGov1WitnessV4({
|
|
240
312
|
proposalIdHash: proposalIdBytes,
|
|
241
313
|
voteDigestHash: voteDigestBytes,
|
|
242
|
-
oldRoot,
|
|
243
|
-
newRoot,
|
|
244
|
-
|
|
314
|
+
oldRoot: state.oldRoot,
|
|
315
|
+
newRoot: state.newRoot,
|
|
316
|
+
proposalDataHash,
|
|
317
|
+
reviewDelayMs,
|
|
245
318
|
});
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
console.log();
|
|
256
|
-
// ── build tx JSON ────────────────────────────────────────────────────────
|
|
319
|
+
const voteWitness = buildValidatorVoteWitness(yesVotes.map((v) => ({
|
|
320
|
+
pubkey: hexToBytes(v.pubkey),
|
|
321
|
+
vote: v.vote,
|
|
322
|
+
timestamp: v.timestamp,
|
|
323
|
+
signature: hexToBytes(v.signature),
|
|
324
|
+
merkleLeafIndex: v.merkleLeafIndex,
|
|
325
|
+
merkleProof: v.merkleProof.map(hexToBytes),
|
|
326
|
+
})));
|
|
327
|
+
const witnessBytes = buildWitnessArgs({ lock: voteWitness, inputType: gov1 });
|
|
257
328
|
const txJson = {
|
|
258
329
|
transaction: {
|
|
259
330
|
version: "0x0",
|
|
260
331
|
cell_deps: [
|
|
261
332
|
{ out_point: { tx_hash: SECP256K1_DEP_GROUP.txHash, index: "0x0" }, dep_type: "dep_group" },
|
|
333
|
+
// Include treasury-lock code dep whenever we have treasury inputs (keyless validation).
|
|
334
|
+
...(treasuryCells.length > 0 ? [{ out_point: { tx_hash: TESTNET_TREASURY_LOCK_DEP.txHash, index: `0x${TESTNET_TREASURY_LOCK_DEP.index.toString(16)}` }, dep_type: "code" }] : []),
|
|
335
|
+
...treasuryLockDeps,
|
|
262
336
|
{
|
|
263
337
|
out_point: {
|
|
264
338
|
tx_hash: TESTNET_CONTRACT_OUTPOINTS.blacklistRegistry.txHash,
|
|
@@ -273,45 +347,146 @@ export async function executeCommand(opts) {
|
|
|
273
347
|
},
|
|
274
348
|
dep_type: "code",
|
|
275
349
|
},
|
|
350
|
+
...(proposalAnchorCellDep ? [proposalAnchorCellDep] : []),
|
|
276
351
|
],
|
|
277
|
-
// Governance transactions update the registry cell, not firewall-protected cells.
|
|
278
|
-
// No header_deps are needed — expiry checks only apply when spending firewall-locked cells.
|
|
279
352
|
header_deps: [],
|
|
280
353
|
inputs: [
|
|
281
354
|
{
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
355
|
+
since: "0x0",
|
|
356
|
+
previous_output: { tx_hash: state.cell.txHash, index: `0x${state.cell.index.toString(16)}` },
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
since: encodeRelativeTimestampSince(reviewDelayMs),
|
|
360
|
+
previous_output: { tx_hash: proposalCellTx, index: `0x${proposalIndex.toString(16)}` },
|
|
361
|
+
},
|
|
362
|
+
...treasuryCells.map((cell) => ({
|
|
363
|
+
since: "0x0",
|
|
285
364
|
previous_output: { tx_hash: cell.txHash, index: `0x${cell.index.toString(16)}` },
|
|
365
|
+
})),
|
|
366
|
+
],
|
|
367
|
+
outputs: [
|
|
368
|
+
{ capacity: hexCapacity(registryOutputCapacity), lock: state.cell.lock, type: state.cell.type },
|
|
369
|
+
// Return proposal cell capacity + any extra treasury growth capacity as a single
|
|
370
|
+
// output to the autonomous treasury-lock pool. Merging avoids creating a second
|
|
371
|
+
// output below the minimum cell capacity (125 CKB for treasury-lock's 64-byte args).
|
|
372
|
+
{
|
|
373
|
+
capacity: hexCapacity(proposalChangeCapacity + extraTreasuryOutputCapacity),
|
|
374
|
+
lock: treasuryLockScript ?? proposalCell.lock,
|
|
375
|
+
type: null,
|
|
286
376
|
},
|
|
287
377
|
],
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
378
|
+
outputs_data: [
|
|
379
|
+
bytesToHex(state.newBlkl),
|
|
380
|
+
"0x",
|
|
381
|
+
],
|
|
382
|
+
witnesses: [
|
|
383
|
+
bytesToHex(witnessBytes),
|
|
384
|
+
bytesToHex(buildWitnessArgs({})),
|
|
385
|
+
...treasuryCells.map(() => bytesToHex(buildWitnessArgs({}))),
|
|
386
|
+
],
|
|
291
387
|
},
|
|
292
388
|
multisig_configs: {},
|
|
293
389
|
signatures: {},
|
|
294
390
|
};
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
console.log(logSymbols.success, `Transaction written to ${chalk.bold(txOut)}`);
|
|
391
|
+
writeFileSync(opts.txOut, JSON.stringify(txJson, null, 2) + "\n");
|
|
392
|
+
saveProposal(proposal);
|
|
298
393
|
console.log();
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
394
|
+
console.log(logSymbols.success, chalk.green(`Transaction written to ${opts.txOut}`));
|
|
395
|
+
console.log(` Proposal: ${proposal.id}`);
|
|
396
|
+
console.log(` Proposal cell: ${proposalCellTx}:${proposalIndex}`);
|
|
397
|
+
console.log(` Proposal hash: ${bytesToHex(proposalDataHash)}`);
|
|
398
|
+
console.log(` Since delay: ${encodeRelativeTimestampSince(reviewDelayMs)}`);
|
|
399
|
+
console.log(` Registry: ${state.currentPayload.entries.length} -> ${state.newEntryCount} entries`);
|
|
400
|
+
console.log();
|
|
401
|
+
const usePrivkey = !!opts.privkeyPath?.trim();
|
|
402
|
+
// Autonomous treasury-lock cells need no signature — the contract validates by detecting
|
|
403
|
+
// the proposal-anchor input. Execute is fully keyless unless the user explicitly requests signing.
|
|
404
|
+
if (!opts.sign && !usePrivkey) {
|
|
405
|
+
const submitSpinner = ora("Submitting (keyless — no treasury inputs)").start();
|
|
406
|
+
try {
|
|
407
|
+
const output = execFileSync("ckb-cli", [
|
|
408
|
+
"--url", opts.rpcUrl,
|
|
409
|
+
"tx", "send",
|
|
410
|
+
"--tx-file", opts.txOut,
|
|
411
|
+
"--skip-check",
|
|
412
|
+
], { encoding: "utf8" });
|
|
413
|
+
submitSpinner.succeed("Submitted");
|
|
414
|
+
const txHash = output.trim().match(/0x[a-fA-F0-9]{64}/)?.[0];
|
|
415
|
+
if (txHash) {
|
|
416
|
+
proposal.status = "executed";
|
|
417
|
+
proposal.txHash = txHash;
|
|
418
|
+
saveProposal(proposal);
|
|
419
|
+
console.log(logSymbols.success, chalk.green(`Executed: ${txHash}`));
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
console.log(chalk.yellow("Submitted — tx hash not parsed from output."));
|
|
423
|
+
console.log(chalk.dim(output.trim()));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
submitSpinner.fail("Submission failed");
|
|
428
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
429
|
+
if (!opts.ready)
|
|
430
|
+
process.exit(1);
|
|
431
|
+
throw err;
|
|
432
|
+
}
|
|
304
433
|
console.log();
|
|
305
434
|
printHints("execute");
|
|
306
435
|
return;
|
|
307
436
|
}
|
|
437
|
+
// ── non-interactive signing via privkey file ──────────────────────────────
|
|
438
|
+
if (usePrivkey) {
|
|
439
|
+
const signSpinner = ora("Signing with privkey").start();
|
|
440
|
+
try {
|
|
441
|
+
execFileSync("ckb-cli", [
|
|
442
|
+
"--url", opts.rpcUrl,
|
|
443
|
+
"tx", "sign-inputs",
|
|
444
|
+
"--tx-file", opts.txOut,
|
|
445
|
+
"--privkey-path", opts.privkeyPath.trim(),
|
|
446
|
+
"--skip-check",
|
|
447
|
+
"--add-signatures",
|
|
448
|
+
], { stdio: "pipe" });
|
|
449
|
+
signSpinner.succeed("Signed");
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
signSpinner.fail("Signing failed");
|
|
453
|
+
throw new Error(err instanceof Error ? err.message : String(err));
|
|
454
|
+
}
|
|
455
|
+
const submitSpinner = ora("Submitting").start();
|
|
456
|
+
try {
|
|
457
|
+
const output = execFileSync("ckb-cli", [
|
|
458
|
+
"--url", opts.rpcUrl,
|
|
459
|
+
"tx", "send",
|
|
460
|
+
"--tx-file", opts.txOut,
|
|
461
|
+
"--skip-check",
|
|
462
|
+
], { encoding: "utf8" });
|
|
463
|
+
submitSpinner.succeed("Submitted");
|
|
464
|
+
const txHash = output.trim().match(/0x[a-fA-F0-9]{64}/)?.[0];
|
|
465
|
+
if (txHash) {
|
|
466
|
+
proposal.status = "executed";
|
|
467
|
+
proposal.txHash = txHash;
|
|
468
|
+
saveProposal(proposal);
|
|
469
|
+
console.log(logSymbols.success, chalk.green(`Executed: ${txHash}`));
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
console.log(chalk.yellow("Submitted, but could not parse tx hash from ckb-cli output."));
|
|
473
|
+
console.log(chalk.dim(output.trim()));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
submitSpinner.fail("Submission failed");
|
|
478
|
+
throw new Error(err instanceof Error ? err.message : String(err));
|
|
479
|
+
}
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
// ── interactive signing via ckb-cli wallet ────────────────────────────────
|
|
308
483
|
let fromAccount = opts.fromAccount;
|
|
309
484
|
if (!fromAccount && process.stdin.isTTY) {
|
|
310
485
|
const { account } = await inquirer.prompt([
|
|
311
486
|
{
|
|
312
487
|
type: "input",
|
|
313
488
|
name: "account",
|
|
314
|
-
message: "
|
|
489
|
+
message: "Fee-payer account address for ckb-cli:",
|
|
315
490
|
validate: (v) => v.trim().length > 0 || "Required.",
|
|
316
491
|
},
|
|
317
492
|
]);
|
|
@@ -330,36 +505,26 @@ export async function executeCommand(opts) {
|
|
|
330
505
|
}
|
|
331
506
|
const signSpinner = ora("Signing with ckb-cli").start();
|
|
332
507
|
try {
|
|
333
|
-
execFileSync("ckb-cli", ["wallet", "sign-txs", "--tx-file", txOut, "--from-account", fromAccount], { stdio: "inherit" });
|
|
508
|
+
execFileSync("ckb-cli", ["wallet", "sign-txs", "--tx-file", opts.txOut, "--from-account", fromAccount], { stdio: "inherit" });
|
|
334
509
|
signSpinner.succeed("Signed");
|
|
335
510
|
const submitSpinner = ora("Submitting").start();
|
|
336
|
-
const output = execFileSync("ckb-cli", ["wallet", "apply-txs", "--tx-file", txOut], { encoding: "utf8" });
|
|
511
|
+
const output = execFileSync("ckb-cli", ["wallet", "apply-txs", "--tx-file", opts.txOut], { encoding: "utf8" });
|
|
337
512
|
submitSpinner.succeed("Submitted");
|
|
338
513
|
const txHash = output.match(/0x[a-fA-F0-9]{64}/)?.[0];
|
|
339
514
|
if (txHash) {
|
|
340
515
|
proposal.status = "executed";
|
|
341
516
|
proposal.txHash = txHash;
|
|
342
517
|
saveProposal(proposal);
|
|
343
|
-
console.log();
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
518
|
+
console.log(logSymbols.success, chalk.green(`Executed: ${txHash}`));
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
console.log(chalk.yellow("Submitted, but could not parse tx hash from ckb-cli output."));
|
|
347
522
|
}
|
|
348
523
|
}
|
|
349
524
|
catch (err) {
|
|
350
|
-
signSpinner.fail("ckb-cli failed");
|
|
525
|
+
signSpinner.fail("ckb-cli signing/submission failed");
|
|
351
526
|
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
352
527
|
process.exit(1);
|
|
353
528
|
}
|
|
354
529
|
}
|
|
355
|
-
export function executeDefaults() {
|
|
356
|
-
return {
|
|
357
|
-
rpcUrl: TESTNET_RPC_URL,
|
|
358
|
-
registryTx: TESTNET_REGISTRY_CELL.txHash,
|
|
359
|
-
registryIndex: String(TESTNET_REGISTRY_CELL.index),
|
|
360
|
-
txOut: "gov_execute_tx.json",
|
|
361
|
-
sign: false,
|
|
362
|
-
fromAccount: "",
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
530
|
//# sourceMappingURL=execute.js.map
|