@ckb-firewall/cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +11 -6
  2. package/dist/commands/anchor.d.ts +23 -0
  3. package/dist/commands/anchor.d.ts.map +1 -0
  4. package/dist/commands/anchor.js +412 -0
  5. package/dist/commands/anchor.js.map +1 -0
  6. package/dist/commands/config.d.ts +4 -0
  7. package/dist/commands/config.d.ts.map +1 -0
  8. package/dist/commands/config.js +59 -0
  9. package/dist/commands/config.js.map +1 -0
  10. package/dist/commands/execute.d.ts +9 -1
  11. package/dist/commands/execute.d.ts.map +1 -1
  12. package/dist/commands/execute.js +394 -229
  13. package/dist/commands/execute.js.map +1 -1
  14. package/dist/commands/import.d.ts.map +1 -1
  15. package/dist/commands/import.js +15 -48
  16. package/dist/commands/import.js.map +1 -1
  17. package/dist/commands/inspect.d.ts.map +1 -1
  18. package/dist/commands/inspect.js +32 -0
  19. package/dist/commands/inspect.js.map +1 -1
  20. package/dist/commands/proposals.d.ts.map +1 -1
  21. package/dist/commands/proposals.js +2 -4
  22. package/dist/commands/proposals.js.map +1 -1
  23. package/dist/commands/propose.d.ts +5 -0
  24. package/dist/commands/propose.d.ts.map +1 -1
  25. package/dist/commands/propose.js +91 -7
  26. package/dist/commands/propose.js.map +1 -1
  27. package/dist/commands/reclaim.d.ts +18 -0
  28. package/dist/commands/reclaim.d.ts.map +1 -0
  29. package/dist/commands/reclaim.js +214 -0
  30. package/dist/commands/reclaim.js.map +1 -0
  31. package/dist/commands/sign.d.ts.map +1 -1
  32. package/dist/commands/sign.js +40 -94
  33. package/dist/commands/sign.js.map +1 -1
  34. package/dist/commands/vote.d.ts +2 -0
  35. package/dist/commands/vote.d.ts.map +1 -1
  36. package/dist/commands/vote.js +31 -25
  37. package/dist/commands/vote.js.map +1 -1
  38. package/dist/index.js +91 -16
  39. package/dist/index.js.map +1 -1
  40. package/dist/lib/blkl.d.ts +17 -2
  41. package/dist/lib/blkl.d.ts.map +1 -1
  42. package/dist/lib/blkl.js +133 -17
  43. package/dist/lib/blkl.js.map +1 -1
  44. package/dist/lib/capacity.d.ts +12 -0
  45. package/dist/lib/capacity.d.ts.map +1 -0
  46. package/dist/lib/capacity.js +18 -0
  47. package/dist/lib/capacity.js.map +1 -0
  48. package/dist/lib/config.d.ts +7 -0
  49. package/dist/lib/config.d.ts.map +1 -0
  50. package/dist/lib/config.js +35 -0
  51. package/dist/lib/config.js.map +1 -0
  52. package/dist/lib/defaults.d.ts +19 -0
  53. package/dist/lib/defaults.d.ts.map +1 -1
  54. package/dist/lib/defaults.js +34 -7
  55. package/dist/lib/defaults.js.map +1 -1
  56. package/dist/lib/governance-v4.d.ts +39 -0
  57. package/dist/lib/governance-v4.d.ts.map +1 -0
  58. package/dist/lib/governance-v4.js +194 -0
  59. package/dist/lib/governance-v4.js.map +1 -0
  60. package/dist/lib/gui-bundle.html +485 -316
  61. package/dist/lib/gui-server.d.ts.map +1 -1
  62. package/dist/lib/gui-server.js +300 -246
  63. package/dist/lib/gui-server.js.map +1 -1
  64. package/dist/lib/hints.d.ts +1 -1
  65. package/dist/lib/hints.d.ts.map +1 -1
  66. package/dist/lib/hints.js +3 -9
  67. package/dist/lib/hints.js.map +1 -1
  68. package/dist/lib/proposals.d.ts +17 -9
  69. package/dist/lib/proposals.d.ts.map +1 -1
  70. package/dist/lib/proposals.js +4 -26
  71. package/dist/lib/proposals.js.map +1 -1
  72. package/dist/lib/rpc.d.ts +5 -0
  73. package/dist/lib/rpc.d.ts.map +1 -1
  74. package/dist/lib/rpc.js +21 -0
  75. package/dist/lib/rpc.js.map +1 -1
  76. package/dist/lib/treasury-status.d.ts +28 -0
  77. package/dist/lib/treasury-status.d.ts.map +1 -0
  78. package/dist/lib/treasury-status.js +70 -0
  79. package/dist/lib/treasury-status.js.map +1 -0
  80. package/dist/lib/treasury.d.ts +15 -0
  81. package/dist/lib/treasury.d.ts.map +1 -0
  82. package/dist/lib/treasury.js +62 -0
  83. package/dist/lib/treasury.js.map +1 -0
  84. package/dist/lib/tx-deps.d.ts +9 -0
  85. package/dist/lib/tx-deps.d.ts.map +1 -0
  86. package/dist/lib/tx-deps.js +15 -0
  87. package/dist/lib/tx-deps.js.map +1 -0
  88. package/dist/lib/witness.d.ts +13 -11
  89. package/dist/lib/witness.d.ts.map +1 -1
  90. package/dist/lib/witness.js +85 -48
  91. package/dist/lib/witness.js.map +1 -1
  92. package/package.json +1 -1
@@ -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 { parseRegistryPayload } from "@ckb-firewall/sdk";
9
- import { getLiveCell } from "../lib/rpc.js";
10
- import { resolveRegistryOutpoint } from "../lib/registry.js";
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 { ckbBlake2b, buildGov1WitnessV3, buildGovernanceSigWitness, buildWitnessArgs, encodeAbsoluteTimestampSince, } from "../lib/witness.js";
15
- import { TESTNET_RPC_URL, TESTNET_REGISTRY_CELL, TESTNET_CONTRACT_OUTPOINTS, SECP256K1_DEP_GROUP, warnIfTrivialTestKeys, } from "../lib/defaults.js";
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
- // ── select proposal ──────────────────────────────────────────────────────
19
- let proposalId = opts.proposal?.trim() ?? "";
20
- if (!proposalId) {
21
- const ready = listProposals().filter(isReadyToExecute);
22
- if (ready.length === 0) {
23
- console.log(logSymbols.warning, chalk.yellow("No proposals are ready to execute."));
24
- console.log(chalk.dim(" A proposal needs: review window passed + vote threshold + 3 signatures."));
25
- process.exit(0);
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
- const { chosen } = await inquirer.prompt([
28
- {
29
- type: "list",
30
- name: "chosen",
31
- message: "Select proposal to execute:",
32
- choices: ready.map((p) => ({
33
- name: `${chalk.bold(p.id)} ${p.action} ${p.lockArgs.slice(0, 24)}…`,
34
- value: p.id,
35
- })),
36
- },
37
- ]);
38
- proposalId = chosen;
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
- // ── checks ───────────────────────────────────────────────────────────────
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 — tx: ${proposal.txHash ?? "unknown"}`));
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
- const ms = new Date(proposal.reviewWindowEndsAt).getTime() - Date.now();
48
- const h = Math.floor(ms / 3_600_000);
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.log(logSymbols.error, chalk.red("Vote threshold not met."));
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.log(logSymbols.error, chalk.red(`Proposal "${proposal.id}" has already expired (${new Date(Number(expiryMs)).toISOString()}).`));
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
- // P4-3: verify every vote carries a valid signature from its claimed pubkey.
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)} has invalid signature length.`));
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]; // length === 65 verified above
256
+ sig65[0] = sigBytes[64];
85
257
  sig65.set(sigBytes.subarray(0, 64), 1);
86
258
  let recoveredPubkey;
87
259
  try {
88
- recoveredPubkey = bytesToHex(new Uint8Array(secp256k1.recoverPublicKey(sig65, msgHash)));
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)} has unrecoverable signature — proposal may have been tampered.`));
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)}… — proposal may have been tampered.`));
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
- // ── fetch current registry cell ──────────────────────────────────────────
100
- if (!/^\d+$/.test(opts.registryIndex.trim())) {
101
- console.error(logSymbols.error, chalk.red("--registry-index must be a non-negative integer."));
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
- const registryIndex = Number.parseInt(opts.registryIndex.trim(), 10);
105
- const spinner = ora("Fetching current registry cell").start();
106
- let cell;
107
- try {
108
- const { txHash, index } = await resolveRegistryOutpoint(opts.rpcUrl, opts.registryTx, registryIndex);
109
- cell = await getLiveCell(opts.rpcUrl, txHash, index);
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
- let currentPayload;
118
- try {
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
- // ── verify vote Merkle proofs against on-chain validator set ─────────────
126
- const oldBlkl = hexToBytes(cell.data);
127
- const oldRoot = ckbBlake2b(oldBlkl);
128
- const govHeaderRaw = extractGovernanceHeaderRaw(cell.data);
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
- if (govHeader && govHeader.validatorCount > 0) {
148
- const rootHex = bytesToHex(govHeader.validatorMerkleRoot);
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
- // Use the on-chain threshold for signer selection; fall back to compile-time constant.
233
- const effectiveThreshold = govHeader?.threshold ?? SIG_THRESHOLD;
234
- const signers = proposal.signatures.slice(0, effectiveThreshold).map((s) => ({
235
- index: s.signerIndex,
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
- reviewWindowEndMs,
314
+ oldRoot: state.oldRoot,
315
+ newRoot: state.newRoot,
316
+ proposalDataHash,
317
+ reviewDelayMs,
245
318
  });
246
- const sigWitness = buildGovernanceSigWitness(signers);
247
- const witnessBytes = buildWitnessArgs({ lock: sigWitness, inputType: gov1 });
248
- // ── summary ───────────────────────────────────────────────────────────────
249
- console.log();
250
- console.log(chalk.bold("Executing proposal:"), proposal.id);
251
- console.log(` Action: ${proposal.action === "add" ? chalk.green("add") : chalk.red("remove")} ${proposal.lockArgs}`);
252
- console.log(` Proposer: ${proposal.proposer}`);
253
- console.log(` Signers: ${signers.map((s) => `#${s.index}`).join(", ")}`);
254
- console.log(` Old new: ${currentPayload.entries.length} ${newEntries.length} entries`);
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
- // Absolute median-time-past since lock: CKB consensus enforces this transaction cannot
283
- // be included in a block until the block's MTP >= reviewWindowEndsAt.
284
- since: encodeAbsoluteTimestampSince(reviewWindowEndMs),
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
- outputs: [{ capacity: cell.capacity, lock: cell.lock, type: cell.type }],
289
- outputs_data: [bytesToHex(newBlkl)],
290
- witnesses: [bytesToHex(witnessBytes)],
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
- const txOut = opts.txOut;
296
- writeFileSync(txOut, JSON.stringify(txJson, null, 2) + "\n");
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
- // ── sign / submit ────────────────────────────────────────────────────────
300
- if (!opts.sign) {
301
- console.log("Sign and submit with ckb-cli:");
302
- console.log(chalk.dim(` ckb-cli wallet sign-txs --tx-file ${txOut} --from-account <address>\n` +
303
- ` ckb-cli wallet apply-txs --tx-file ${txOut}`));
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: "Governance account address (ckb-cli --from-account):",
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
- console.log(logSymbols.success, chalk.green("Registry updated on-chain."));
345
- console.log(` Tx: ${chalk.bold(txHash)}`);
346
- printHints("execute");
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