verifyhash 0.1.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 (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +883 -0
  3. package/cli/abi/ContributionRegistry.json +881 -0
  4. package/cli/agent.js +2173 -0
  5. package/cli/anchor-artifact.js +853 -0
  6. package/cli/anchor.js +400 -0
  7. package/cli/claim.js +881 -0
  8. package/cli/core/agent-commit.js +448 -0
  9. package/cli/core/agent-session.js +598 -0
  10. package/cli/core/anchor-binding.js +663 -0
  11. package/cli/core/attestation.js +580 -0
  12. package/cli/core/evidence-plans.js +495 -0
  13. package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
  14. package/cli/core/fulfill-intake.js +1082 -0
  15. package/cli/core/go-live-preflight.js +481 -0
  16. package/cli/core/license.js +534 -0
  17. package/cli/core/manifest.js +243 -0
  18. package/cli/core/packetseal.js +591 -0
  19. package/cli/core/registryArtifact.js +49 -0
  20. package/cli/core/revocation.js +539 -0
  21. package/cli/core/rfc3161.js +389 -0
  22. package/cli/core/timestamp.js +482 -0
  23. package/cli/core/trust-asof.js +479 -0
  24. package/cli/dataset.js +2950 -0
  25. package/cli/evidence.js +2227 -0
  26. package/cli/fulfill-webhook-http.js +438 -0
  27. package/cli/git.js +220 -0
  28. package/cli/hash.js +550 -0
  29. package/cli/identity.js +1072 -0
  30. package/cli/journal-cli.js +1110 -0
  31. package/cli/journal-log.js +454 -0
  32. package/cli/journal.js +334 -0
  33. package/cli/lineage.js +447 -0
  34. package/cli/list.js +287 -0
  35. package/cli/parcel.js +1509 -0
  36. package/cli/proof.js +578 -0
  37. package/cli/prove.js +300 -0
  38. package/cli/receipt.js +631 -0
  39. package/cli/registry.js +331 -0
  40. package/cli/reputation.js +344 -0
  41. package/cli/revocation.js +495 -0
  42. package/cli/serve-verify-http.js +298 -0
  43. package/cli/serve-verify.js +333 -0
  44. package/cli/show.js +339 -0
  45. package/cli/verify.js +383 -0
  46. package/cli/vh.js +3927 -0
  47. package/docs/ADOPT.md +183 -0
  48. package/docs/ADOPTION.json +11 -0
  49. package/docs/AGENTTRACE.md +247 -0
  50. package/docs/ANCHORING.md +167 -0
  51. package/docs/AUDIT.md +55 -0
  52. package/docs/CONFORMANCE.md +107 -0
  53. package/docs/DATALEDGER.md +638 -0
  54. package/docs/DECIDE.md +47 -0
  55. package/docs/DECISIONS-PENDING.md +27 -0
  56. package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
  57. package/docs/ENGINE-LEDGER.json +12 -0
  58. package/docs/EVIDENCE.md +519 -0
  59. package/docs/GO-LIVE.md +66 -0
  60. package/docs/IDENTITY.md +123 -0
  61. package/docs/INDEPENDENT-VERIFICATION.md +377 -0
  62. package/docs/INTEGRITY-JOURNAL.md +337 -0
  63. package/docs/KEY-LIFECYCLE.md +179 -0
  64. package/docs/LICENSING.md +46 -0
  65. package/docs/LINEAGE.md +307 -0
  66. package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
  67. package/docs/LOOP-HARDENING-PLAN.md +44 -0
  68. package/docs/MERKLE-LEAVES.md +113 -0
  69. package/docs/METRICS.jsonl +31 -0
  70. package/docs/MORNING.md +204 -0
  71. package/docs/PILOT.md +444 -0
  72. package/docs/PROOFPARCEL.md +227 -0
  73. package/docs/PROOFS.md +262 -0
  74. package/docs/RECEIPTS.md +341 -0
  75. package/docs/REPUTATION.md +158 -0
  76. package/docs/SDK.md +301 -0
  77. package/docs/STRATEGY-ARCHIVE.md +5055 -0
  78. package/docs/SUPERVISOR-RUNBOOK.md +52 -0
  79. package/docs/TRUST-BOUNDARIES.md +335 -0
  80. package/docs/TRUSTLEDGER.md +1976 -0
  81. package/docs/USAGE-BUDGET.json +121 -0
  82. package/docs/VERIFY-SERVICE.md +168 -0
  83. package/index.js +160 -0
  84. package/package.json +41 -0
  85. package/trustledger/build-standalone.js +796 -0
  86. package/trustledger/cli.js +3179 -0
  87. package/trustledger/close.js +391 -0
  88. package/trustledger/corpus.js +159 -0
  89. package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
  90. package/trustledger/dist/trustledger-standalone.html +6197 -0
  91. package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
  92. package/trustledger/door-core.js +442 -0
  93. package/trustledger/fixtures/bank.csv +7 -0
  94. package/trustledger/fixtures/bank.malformed.csv +3 -0
  95. package/trustledger/fixtures/bank.noalias.csv +5 -0
  96. package/trustledger/fixtures/bank.ofx +34 -0
  97. package/trustledger/fixtures/bank.real.csv +5 -0
  98. package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
  99. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
  100. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
  101. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
  102. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
  103. package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
  104. package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
  105. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
  106. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
  107. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
  108. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
  109. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
  110. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
  111. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
  112. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
  113. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
  114. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
  115. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
  116. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
  117. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
  118. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
  119. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
  120. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
  121. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
  122. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
  123. package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
  124. package/trustledger/fixtures/e2e/bank.csv +4 -0
  125. package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
  126. package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
  127. package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
  128. package/trustledger/fixtures/e2e/rentroll.csv +6 -0
  129. package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
  130. package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
  131. package/trustledger/fixtures/plans/baseline.json +25 -0
  132. package/trustledger/fixtures/plans/price-binding.example.json +27 -0
  133. package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
  134. package/trustledger/fixtures/policy/baseline.json +19 -0
  135. package/trustledger/fixtures/policy/ca-example.json +12 -0
  136. package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
  137. package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
  138. package/trustledger/fixtures/quickbooks.csv +7 -0
  139. package/trustledger/fixtures/quickbooks.real.csv +5 -0
  140. package/trustledger/fixtures/rentroll.csv +6 -0
  141. package/trustledger/fixtures/rentroll.real.csv +4 -0
  142. package/trustledger/ingest.js +1163 -0
  143. package/trustledger/lib/policy-bundled-loader.js +44 -0
  144. package/trustledger/lib/sha256-vendored.js +227 -0
  145. package/trustledger/license.js +563 -0
  146. package/trustledger/match.js +551 -0
  147. package/trustledger/plans.js +551 -0
  148. package/trustledger/policy.js +398 -0
  149. package/trustledger/public/index.html +512 -0
  150. package/trustledger/reconcile.js +1486 -0
  151. package/trustledger/report.js +887 -0
  152. package/trustledger/seal.js +854 -0
  153. package/trustledger/server.js +391 -0
  154. package/trustledger/valueproof.js +350 -0
@@ -0,0 +1,853 @@
1
+ "use strict";
2
+
3
+ // cli/anchor-artifact.js — `vh anchor-artifact` / `vh verify-anchored` (T-70.2, EPIC-70).
4
+ //
5
+ // WHAT THIS IS
6
+ // The thin CLI bridge between the PURE anchor-binding core (T-70.1, cli/core/anchor-binding.js)
7
+ // and a live ContributionRegistry:
8
+ //
9
+ // vh anchor-artifact <sealed-file> --contract <addr> --rpc <url>
10
+ // (--key-env <VAR> | --key-file <p>)
11
+ // [--author-bound] [--uri <s>] [--out <receipt>] [--json]
12
+ // [--i-understand-mainnet]
13
+ // read + parse the sealed artifact, extract its ONE canonical digest via the closed T-70.1
14
+ // kind table (each leg re-validates through the artifact's own shipped validator), submit
15
+ // that digest as the registry contentHash, wait for the tx to mine, READ THE RECORD BACK
16
+ // (contributor / authorBound / blockNumber / block timestamp — the D-1 semantics surfaced
17
+ // from the chain, never re-implemented here), and emit the canonical
18
+ // kind:"vh-anchored-receipt@1" container.
19
+ // * default: the ONE-SHOT anchor() write path — the record is NOT author-bound (the
20
+ // contract records the first broadcaster; a mempool copier could have been first).
21
+ // * --author-bound: the commit-reveal claim (D-1): commit(keccak256(abi.encode(digest,
22
+ // committer, salt))) — the SHIPPED cli/claim.js computeCommitment/newSalt, reused
23
+ // verbatim — wait out MIN_REVEAL_DELAY, then reveal(digest, salt, uri). The resulting
24
+ // record reads back authorBound:true and cannot be redirected by a front-runner.
25
+ //
26
+ // vh verify-anchored <receipt> <sealed-file> [--rpc <url> --contract <addr>] [--json]
27
+ // OFFLINE by default: strict T-70.1 verifyAnchoredReceipt — validate the receipt container,
28
+ // RECOMPUTE the artifact's digest through the same closed table, and match the full
29
+ // {kind, digest, how} triple; every deviation is a SPECIFIC named reject. With BOTH --rpc
30
+ // and --contract it ADDITIONALLY (a) authenticates the registry through the EXISTING EPIC-11
31
+ // identity probe (cli/registry.js assertRegistry — no record is believed until the contract
32
+ // self-identifies on the receipt's chainId), then (b) re-checks the receipt's chain facts
33
+ // against the chain: the record for the digest must exist and its contributor / authorBound /
34
+ // blockNumber / block timestamp must equal the receipt's, and the receipt's txHash must be a
35
+ // real mined tx in the recorded block targeting the recorded contract. Each mismatch is a
36
+ // SPECIFIC named reject. verify-anchored NEVER signs and needs NO key.
37
+ //
38
+ // KEY HYGIENE (the house discipline, reused — not re-implemented)
39
+ // The signing key for anchor-artifact comes ONLY from --key-env <VAR> / --key-file <path> via the
40
+ // ONE shared read-used-discarded path, cli/core/attestation.js loadSigningWallet: EXACTLY ONE
41
+ // source (neither/both is a usage error BEFORE anything is read), a missing var / unreadable file /
42
+ // malformed or zero key hard-errors naming only the SOURCE (never echoing key material), and the
43
+ // raw key exists only inside the in-process Wallet. It is never generated, persisted, or logged.
44
+ //
45
+ // MAINNET GUARD (reused verbatim)
46
+ // The EXISTING cli/anchor.js isTestnetChainId set gates every submission: a chainId outside the
47
+ // known local/dev/testnet set refuses to write unless --i-understand-mainnet is passed. The guard
48
+ // runs BEFORE any transaction is built or sent.
49
+ //
50
+ // FREE SURFACE
51
+ // Both verbs are free: no paid gate is consulted anywhere in this module (the acceptance grep
52
+ // pins that), and verify-anchored is verify-only (no key, no signer, nothing written except what
53
+ // --out of the anchor verb explicitly asked for).
54
+ //
55
+ // OUTPUT / EXIT CONTRACT (stable; a future indexer/UI may depend on it)
56
+ // vh anchor-artifact: exit 0 anchored (receipt emitted; --json prints ONE machine object)
57
+ // exit 3 named reject — the artifact failed its own validator/binding, OR
58
+ // the registry itself reverted with a named error (e.g.
59
+ // AlreadyAnchored) — always a clean one-line error, never a stack
60
+ // exit 2 usage (bad flag, missing <sealed-file>/--contract/--rpc, neither or
61
+ // both key sources)
62
+ // exit 1 IO / network / key-source runtime error (unreadable file, RPC down,
63
+ // missing env var, malformed key, non-testnet refusal)
64
+ // vh verify-anchored: exit 0 ACCEPTED / 3 REJECTED (named) / 2 usage / 1 IO — the SHARED 0/3
65
+ // verify contract every vh verify-verb keeps.
66
+ //
67
+ // FILESYSTEM HYGIENE
68
+ // The only file either verb ever writes is the anchored receipt, and only when the caller passed
69
+ // an explicit --out <path> — never silently into cwd. Without --out the receipt is printed to
70
+ // stdout (its canonical one-line serialization) so the caller can redirect it wherever they want.
71
+
72
+ const fs = require("fs");
73
+ const path = require("path");
74
+
75
+ const { isTestnetChainId } = require("./anchor"); // the EXISTING mainnet guard, reused verbatim
76
+ const { computeCommitment, newSalt } = require("./claim"); // the SHIPPED commit-reveal building blocks
77
+ const { loadSigningWallet } = require("./core/attestation"); // the ONE read-used-discarded key path
78
+ const binding = require("./core/anchor-binding"); // T-70.1: the pure digest/receipt/verify core
79
+ const {
80
+ assertRegistry,
81
+ isGenuineRpcError,
82
+ formatRegistryLine,
83
+ jsonRegistryBlock,
84
+ } = require("./registry"); // EPIC-11: the EXISTING authenticated read path (identity probe)
85
+
86
+ const ARTIFACT = require("./core/registryArtifact");
87
+ const ABI = ARTIFACT.abi;
88
+
89
+ // The shared exit contract (matches the wider vh family).
90
+ const EXIT = Object.freeze({ OK: 0, IO: 1, USAGE: 2, REJECT: 3 });
91
+
92
+ const ANCHOR_ARTIFACT_USAGE =
93
+ "usage: vh anchor-artifact <sealed-file> --contract <addr> --rpc <url> " +
94
+ "(--key-env <VAR> | --key-file <p>) [--author-bound] [--uri <s>] [--out <receipt>] [--json] " +
95
+ "[--i-understand-mainnet]\n";
96
+
97
+ const VERIFY_ANCHORED_USAGE =
98
+ "usage: vh verify-anchored <receipt> <sealed-file> [--rpc <url> --contract <addr>] [--json]\n";
99
+
100
+ // The registry's own named custom errors (from the contract ABI). Used only as a last-resort
101
+ // textual fallback when a node surfaces a revert without decodable data.
102
+ const REGISTRY_ERROR_NAMES = Object.freeze([
103
+ "ZeroHash",
104
+ "AlreadyAnchored",
105
+ "NotAnchored",
106
+ "IndexOutOfRange",
107
+ "ZeroCommitment",
108
+ "CommitmentExists",
109
+ "NoSuchCommitment",
110
+ "RevealTooSoon",
111
+ "UnknownParent",
112
+ "SelfParent",
113
+ ]);
114
+
115
+ // ---------------------------------------------------------------------------------------------------
116
+ // argv parsers — throw on unknown/incomplete flags so a typo never silently becomes a real submission.
117
+ // ---------------------------------------------------------------------------------------------------
118
+
119
+ /**
120
+ * Parse `anchor-artifact` argv. One positional (<sealed-file>); throws on unknown flags, a flag
121
+ * missing its value, or extra positionals.
122
+ */
123
+ function parseAnchorArtifactArgs(argv) {
124
+ const opts = {
125
+ artifact: undefined,
126
+ contract: undefined,
127
+ rpc: undefined,
128
+ keyEnv: undefined,
129
+ keyFile: undefined,
130
+ uri: undefined,
131
+ out: undefined,
132
+ authorBound: false,
133
+ json: false,
134
+ iUnderstandMainnet: false,
135
+ };
136
+ for (let i = 0; i < argv.length; i++) {
137
+ const a = argv[i];
138
+ switch (a) {
139
+ case "--author-bound":
140
+ opts.authorBound = true;
141
+ break;
142
+ case "--json":
143
+ opts.json = true;
144
+ break;
145
+ case "--i-understand-mainnet":
146
+ opts.iUnderstandMainnet = true;
147
+ break;
148
+ case "--uri":
149
+ opts.uri = argv[++i];
150
+ if (opts.uri === undefined) throw new Error("--uri requires a value");
151
+ break;
152
+ case "--out":
153
+ opts.out = argv[++i];
154
+ if (opts.out === undefined) throw new Error("--out requires a value");
155
+ break;
156
+ case "--contract":
157
+ opts.contract = argv[++i];
158
+ if (opts.contract === undefined) throw new Error("--contract requires a value");
159
+ break;
160
+ case "--rpc":
161
+ opts.rpc = argv[++i];
162
+ if (opts.rpc === undefined) throw new Error("--rpc requires a value");
163
+ break;
164
+ case "--key-env":
165
+ opts.keyEnv = argv[++i];
166
+ if (opts.keyEnv === undefined) throw new Error("--key-env requires a value");
167
+ break;
168
+ case "--key-file":
169
+ opts.keyFile = argv[++i];
170
+ if (opts.keyFile === undefined) throw new Error("--key-file requires a value");
171
+ break;
172
+ default:
173
+ if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
174
+ if (opts.artifact !== undefined) throw new Error(`unexpected extra argument: ${a}`);
175
+ opts.artifact = a;
176
+ }
177
+ }
178
+ return opts;
179
+ }
180
+
181
+ /**
182
+ * Parse `verify-anchored` argv. TWO positionals, in order: <receipt> then <sealed-file>.
183
+ */
184
+ function parseVerifyAnchoredArgs(argv) {
185
+ const opts = {
186
+ receipt: undefined,
187
+ artifact: undefined,
188
+ contract: undefined,
189
+ rpc: undefined,
190
+ json: false,
191
+ };
192
+ for (let i = 0; i < argv.length; i++) {
193
+ const a = argv[i];
194
+ switch (a) {
195
+ case "--json":
196
+ opts.json = true;
197
+ break;
198
+ case "--contract":
199
+ opts.contract = argv[++i];
200
+ if (opts.contract === undefined) throw new Error("--contract requires a value");
201
+ break;
202
+ case "--rpc":
203
+ opts.rpc = argv[++i];
204
+ if (opts.rpc === undefined) throw new Error("--rpc requires a value");
205
+ break;
206
+ default:
207
+ if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
208
+ if (opts.receipt === undefined) opts.receipt = a;
209
+ else if (opts.artifact === undefined) opts.artifact = a;
210
+ else throw new Error(`unexpected extra argument: ${a}`);
211
+ }
212
+ }
213
+ return opts;
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------------------------------
217
+ // Small shared helpers.
218
+ // ---------------------------------------------------------------------------------------------------
219
+
220
+ /** Read + JSON.parse a file; on failure write an actionable error and return null (caller exits 1). */
221
+ function readJson(label, filePath, writeErr) {
222
+ let text;
223
+ try {
224
+ text = fs.readFileSync(path.resolve(filePath), "utf8");
225
+ } catch (e) {
226
+ writeErr(`error: cannot read ${label} ${filePath}: ${e.message}\n`);
227
+ return null;
228
+ }
229
+ try {
230
+ return JSON.parse(text);
231
+ } catch (e) {
232
+ writeErr(`error: ${label} ${filePath} is not valid JSON: ${e.message}\n`);
233
+ return null;
234
+ }
235
+ }
236
+
237
+ /** Pull raw revert data out of the several places ethers/hardhat nodes stash it. */
238
+ function extractRevertData(err) {
239
+ if (!err || typeof err !== "object") return null;
240
+ const candidates = [
241
+ err.data,
242
+ err.info && err.info.error && err.info.error.data,
243
+ err.info && err.info.error && err.info.error.data && err.info.error.data.data,
244
+ err.error && err.error.data,
245
+ ];
246
+ for (const c of candidates) {
247
+ if (typeof c === "string" && c.startsWith("0x") && c.length >= 10) return c;
248
+ }
249
+ return null;
250
+ }
251
+
252
+ /**
253
+ * Resolve a send/read failure to the REGISTRY'S OWN named error (e.g. "AlreadyAnchored(0x…, 0x…)"),
254
+ * or null when the failure is not a decodable contract revert. Tries, in order: ethers' decoded
255
+ * `err.revert`, parsing raw revert data against the registry ABI, ethers' `err.reason`, and finally
256
+ * a known-error-name match in the message (some nodes surface only "unknown custom error" text).
257
+ */
258
+ function namedRegistryReject(err, ethersLib) {
259
+ if (err && err.revert && err.revert.name) {
260
+ const args = Array.isArray(err.revert.args) ? Array.from(err.revert.args).map(String).join(", ") : "";
261
+ return `${err.revert.name}(${args})`;
262
+ }
263
+ const data = extractRevertData(err);
264
+ if (data) {
265
+ try {
266
+ const parsed = new ethersLib.Interface(ABI).parseError(data);
267
+ if (parsed) return `${parsed.name}(${Array.from(parsed.args).map(String).join(", ")})`;
268
+ } catch (_) {
269
+ /* not one of the registry's errors */
270
+ }
271
+ }
272
+ if (err && typeof err.reason === "string" && /[A-Za-z]/.test(err.reason)) return err.reason;
273
+ const msg = err && err.message ? String(err.message) : "";
274
+ for (const name of REGISTRY_ERROR_NAMES) {
275
+ if (msg.includes(name)) return name;
276
+ }
277
+ return null;
278
+ }
279
+
280
+ /**
281
+ * Wait until the chain has advanced past the MIN_REVEAL_DELAY window for a commit mined in
282
+ * `commitBlock` (a reveal needs `current > commitBlock + minDelay`). Mirrors the shipped
283
+ * cli/claim.js window wait: an injectable `waitForBlock` lets a test mine the blocks itself; the
284
+ * real path polls the node until blocks are produced.
285
+ */
286
+ async function waitRevealWindow({ provider, commitBlock, minDelay, waitForBlock }) {
287
+ const revealAfter = commitBlock + minDelay;
288
+ if (waitForBlock) {
289
+ await waitForBlock(revealAfter + 1n);
290
+ return;
291
+ }
292
+ /* eslint-disable no-await-in-loop */
293
+ while (BigInt(await provider.getBlockNumber()) <= revealAfter) {
294
+ await new Promise((r) => setTimeout(r, 1500));
295
+ }
296
+ /* eslint-enable no-await-in-loop */
297
+ }
298
+
299
+ // ---------------------------------------------------------------------------------------------------
300
+ // vh anchor-artifact
301
+ // ---------------------------------------------------------------------------------------------------
302
+
303
+ /**
304
+ * Run `vh anchor-artifact` end to end. Returns the process exit code.
305
+ *
306
+ * @param {object} opts
307
+ * @param {string} opts.artifact path to the sealed artifact file (JSON)
308
+ * @param {string} opts.contract deployed ContributionRegistry address
309
+ * @param {string} [opts.rpc] RPC endpoint URL (or inject opts.provider)
310
+ * @param {string} [opts.keyEnv] env var NAME holding the key (EXACTLY ONE of keyEnv/keyFile)
311
+ * @param {string} [opts.keyFile] path to a key file the caller created
312
+ * @param {boolean}[opts.authorBound] use the commit-reveal claim (record reads back authorBound:true)
313
+ * @param {string} [opts.uri] optional untrusted off-chain pointer hint
314
+ * @param {string} [opts.out] write the anchored receipt to THIS explicit path (else stdout)
315
+ * @param {boolean}[opts.json] emit ONE machine-readable JSON object instead of human lines
316
+ * @param {boolean}[opts.iUnderstandMainnet] bypass the non-testnet refusal (the EXISTING guard)
317
+ * @param {object} [opts.provider] injected ethers Provider (tests; else built from opts.rpc)
318
+ * @param {bigint|number}[opts.chainId] override/short-circuit the chainId lookup (tests — same
319
+ * hook the shipped runAnchor exposes)
320
+ * @param {(target:bigint)=>Promise<void>}[opts.waitForBlock] test hook to advance/await blocks
321
+ * @param {object} [opts.ethers] ethers v6 module
322
+ * @param {{write?:Function, writeErr?:Function}} [io]
323
+ * @returns {Promise<number>} exit code (see the module-header contract)
324
+ */
325
+ async function runAnchorArtifact(opts, io = {}) {
326
+ const write = io.write || ((s) => process.stdout.write(s));
327
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
328
+ const ethersLib = opts.ethers || require("ethers");
329
+
330
+ const reject = (reason, detail) => {
331
+ if (opts.json) {
332
+ write(JSON.stringify({ ok: false, verdict: "REJECTED", reason, detail }, null, 2) + "\n");
333
+ } else {
334
+ writeErr(`anchor-artifact: REJECTED (${reason}): ${detail}\n`);
335
+ }
336
+ return EXIT.REJECT;
337
+ };
338
+
339
+ // ---- usage-shape validation FIRST (nothing read, no key touched, no network) ----
340
+ if (!opts.artifact) {
341
+ writeErr("error: `vh anchor-artifact` requires a <sealed-file>\n" + ANCHOR_ARTIFACT_USAGE);
342
+ return EXIT.USAGE;
343
+ }
344
+ if (!opts.contract) {
345
+ writeErr(
346
+ "error: no contract address: pass --contract <address> or set VH_CONTRACT in the environment\n" +
347
+ ANCHOR_ARTIFACT_USAGE
348
+ );
349
+ return EXIT.USAGE;
350
+ }
351
+ if (!ethersLib.isAddress(opts.contract)) {
352
+ writeErr(`error: invalid contract address: ${opts.contract}\n`);
353
+ return EXIT.USAGE;
354
+ }
355
+ const hasEnv = opts.keyEnv !== undefined && opts.keyEnv !== null;
356
+ const hasFile = opts.keyFile !== undefined && opts.keyFile !== null;
357
+ if (!hasEnv && !hasFile) {
358
+ writeErr(
359
+ "error: `vh anchor-artifact` requires EXACTLY ONE signing-key source: --key-env <VAR> or " +
360
+ "--key-file <path>\n" +
361
+ ANCHOR_ARTIFACT_USAGE
362
+ );
363
+ return EXIT.USAGE;
364
+ }
365
+ if (hasEnv && hasFile) {
366
+ writeErr("error: --key-env and --key-file are mutually exclusive; pass EXACTLY ONE signing-key source\n");
367
+ return EXIT.USAGE;
368
+ }
369
+ if (!opts.provider && !opts.rpc) {
370
+ writeErr(
371
+ "error: no RPC endpoint; pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL in the environment\n" +
372
+ ANCHOR_ARTIFACT_USAGE
373
+ );
374
+ return EXIT.USAGE;
375
+ }
376
+
377
+ // ---- read the artifact + extract its ONE canonical digest (offline; the T-70.1 closed table) ----
378
+ const artifactPath = path.resolve(opts.artifact);
379
+ const artifact = readJson("artifact", opts.artifact, writeErr);
380
+ if (artifact === null) return EXIT.IO;
381
+ const d = binding.artifactDigest(artifact);
382
+ if (!d.ok) {
383
+ // The artifact's own named validation reject — nothing was signed or sent.
384
+ return reject(d.reason, d.detail || "the artifact failed its own validator; refusing to anchor it");
385
+ }
386
+
387
+ // ---- signing key: the ONE house read-used-discarded path. Loaded only AFTER the artifact proved
388
+ // anchorable, and BEFORE any network use; errors name only the SOURCE, never key material. ----
389
+ let wallet;
390
+ try {
391
+ ({ wallet } = loadSigningWallet({ keyEnv: opts.keyEnv, keyFile: opts.keyFile }));
392
+ } catch (e) {
393
+ writeErr(`error: ${e.message}\n`);
394
+ return EXIT.IO;
395
+ }
396
+
397
+ // ---- chain resolution + the EXISTING mainnet guard (BEFORE any transaction is built/sent) ----
398
+ const provider = opts.provider || new ethersLib.JsonRpcProvider(opts.rpc);
399
+ let chainId = opts.chainId;
400
+ if (chainId == null) {
401
+ try {
402
+ chainId = (await provider.getNetwork()).chainId;
403
+ } catch (e) {
404
+ writeErr(`error: cannot reach the RPC endpoint to determine the chainId: ${e.message}\n`);
405
+ return EXIT.IO;
406
+ }
407
+ }
408
+ chainId = BigInt(chainId);
409
+ if (!isTestnetChainId(chainId) && !opts.iUnderstandMainnet) {
410
+ writeErr(
411
+ `error: refusing to anchor on chainId ${chainId.toString()} (not a known testnet). ` +
412
+ "If you really mean to write to this chain, re-run with --i-understand-mainnet.\n"
413
+ );
414
+ return EXIT.IO;
415
+ }
416
+
417
+ // NonceManager keeps back-to-back sends (the --author-bound commit + reveal pair, possibly
418
+ // interleaved with externally mined blocks) from tripping ethers' briefly-cached nonce reads —
419
+ // the same wrapper the shipped commit-reveal test discipline uses.
420
+ const signer = new ethersLib.NonceManager(wallet.connect(provider));
421
+ const contractAddr = ethersLib.getAddress(opts.contract);
422
+ const contract = new ethersLib.Contract(contractAddr, ABI, signer);
423
+ const uri = opts.uri == null ? "" : String(opts.uri);
424
+
425
+ // ---- submit: one-shot anchor() by default; commit-reveal (D-1) with --author-bound ----
426
+ let txHash;
427
+ try {
428
+ if (opts.authorBound) {
429
+ const salt = newSalt(ethersLib); // fresh random 32-byte secret (public after reveal)
430
+ const committer = await signer.getAddress();
431
+ // The SHIPPED commitment construction, reused verbatim (sender-bound + salt-blinded).
432
+ const commitment = computeCommitment({ contentHash: d.digest, committer, salt, ethers: ethersLib });
433
+ if (!opts.json) {
434
+ write(`anchor-artifact: committing digest ${d.digest} (author-bound commit-reveal) as ${committer}...\n`);
435
+ }
436
+ const commitMined = await (await contract.commit(commitment)).wait();
437
+ const minDelay = BigInt(await contract.MIN_REVEAL_DELAY());
438
+ if (!opts.json) {
439
+ write(` commit tx: ${commitMined.hash} (block ${commitMined.blockNumber}); revealing after ${minDelay} block(s)...\n`);
440
+ }
441
+ await waitRevealWindow({
442
+ provider,
443
+ commitBlock: BigInt(commitMined.blockNumber),
444
+ minDelay,
445
+ waitForBlock: opts.waitForBlock,
446
+ });
447
+ const revealMined = await (await contract.reveal(d.digest, salt, uri)).wait();
448
+ txHash = revealMined.hash;
449
+ } else {
450
+ if (!opts.json) {
451
+ write(`anchor-artifact: anchoring digest ${d.digest} (one-shot; the record will NOT be author-bound)...\n`);
452
+ }
453
+ const mined = await (await contract.anchor(d.digest, uri)).wait();
454
+ txHash = mined.hash;
455
+ }
456
+ } catch (e) {
457
+ // The registry's OWN named revert (e.g. AlreadyAnchored) is a clean, named reject — never a
458
+ // stack trace. Anything else is a genuine runtime/network failure.
459
+ const named = namedRegistryReject(e, ethersLib);
460
+ if (named) {
461
+ return reject("registry-reject", `the registry rejected this write: ${named}`);
462
+ }
463
+ if (isGenuineRpcError(e)) {
464
+ writeErr(`error: RPC failure while anchoring: ${e.message}\n`);
465
+ return EXIT.IO;
466
+ }
467
+ writeErr(`error: ${e.message}\n`);
468
+ return EXIT.IO;
469
+ }
470
+
471
+ // ---- read the record BACK from the chain (the D-1 semantics surfaced, not re-implemented):
472
+ // contributor / authorBound / blockNumber / block timestamp come from the registry itself ----
473
+ let rec;
474
+ try {
475
+ rec = await contract.getRecord(d.digest);
476
+ } catch (e) {
477
+ writeErr(`error: the anchor tx mined (${txHash}) but the record could not be read back: ${e.message}\n`);
478
+ return EXIT.IO;
479
+ }
480
+ const chain = {
481
+ authorBound: Boolean(rec.authorBound),
482
+ blockNumber: Number(rec.blockNumber),
483
+ blockTime: Number(rec.timestamp),
484
+ chainId: Number(chainId),
485
+ contract: contractAddr.toLowerCase(),
486
+ contributor: String(rec.contributor).toLowerCase(),
487
+ txHash: String(txHash).toLowerCase(),
488
+ };
489
+
490
+ const built = binding.buildAnchoredReceipt({
491
+ digest: d.digest,
492
+ kind: d.kind,
493
+ how: d.how,
494
+ artifactLabel: path.basename(artifactPath),
495
+ chain,
496
+ });
497
+ if (!built.ok) {
498
+ writeErr(
499
+ `error: the anchor tx mined (${txHash}) but the anchored receipt could not be assembled ` +
500
+ `(${built.reason}): ${built.detail || ""}\n`
501
+ );
502
+ return EXIT.IO;
503
+ }
504
+ const receipt = built.receipt;
505
+ // The canonical byte serialization (the core's sorted-key container: stringify + newline IS it).
506
+ const receiptBytes = JSON.stringify(receipt) + "\n";
507
+
508
+ let outPath = null;
509
+ if (opts.out) {
510
+ outPath = path.resolve(opts.out); // explicit, caller-chosen path — never a silent cwd drop
511
+ try {
512
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
513
+ fs.writeFileSync(outPath, receiptBytes);
514
+ } catch (e) {
515
+ writeErr(`error: cannot write receipt ${opts.out}: ${e.message}\n`);
516
+ return EXIT.IO;
517
+ }
518
+ }
519
+
520
+ if (opts.json) {
521
+ write(
522
+ JSON.stringify(
523
+ {
524
+ ok: true,
525
+ verdict: "ANCHORED",
526
+ artifact: opts.artifact,
527
+ digest: d.digest,
528
+ artifactKind: d.kind,
529
+ how: d.how,
530
+ chain,
531
+ receiptPath: outPath,
532
+ receipt,
533
+ },
534
+ null,
535
+ 2
536
+ ) + "\n"
537
+ );
538
+ } else {
539
+ write("anchor-artifact: ANCHORED\n");
540
+ write(` digest: ${d.digest}\n`);
541
+ write(` kind: ${d.kind}\n`);
542
+ write(` chainId: ${chain.chainId} contract: ${chain.contract}\n`);
543
+ write(` tx: ${chain.txHash} (block ${chain.blockNumber}, blockTime ${chain.blockTime})\n`);
544
+ write(` contributor: ${chain.contributor} authorBound: ${chain.authorBound}\n`);
545
+ if (outPath) {
546
+ write(` receipt written: ${outPath}\n`);
547
+ } else {
548
+ write(" receipt (NOT written to disk; pass --out <path> to save it):\n");
549
+ write(receiptBytes);
550
+ }
551
+ write(` NOTE: ${binding.ANCHOR_TRUST_NOTE}\n`);
552
+ }
553
+ return EXIT.OK;
554
+ }
555
+
556
+ // ---------------------------------------------------------------------------------------------------
557
+ // vh verify-anchored
558
+ // ---------------------------------------------------------------------------------------------------
559
+
560
+ /**
561
+ * Run `vh verify-anchored`. OFFLINE by default (pure T-70.1 binding verify); with BOTH an endpoint
562
+ * (--rpc, or an injected provider) AND --contract it additionally authenticates the registry (the
563
+ * EXISTING EPIC-11 identity probe) and re-checks the receipt's chain facts against the chain.
564
+ * Never signs; needs no key. Returns the process exit code (0 ACCEPTED / 3 REJECTED / 2 / 1).
565
+ *
566
+ * @param {object} opts { receipt, artifact, rpc?, contract?, json?, provider?, ethers? }
567
+ * @param {{write?:Function, writeErr?:Function}} [io]
568
+ * @returns {Promise<number>}
569
+ */
570
+ async function runVerifyAnchored(opts, io = {}) {
571
+ const write = io.write || ((s) => process.stdout.write(s));
572
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
573
+ const ethersLib = opts.ethers || require("ethers");
574
+
575
+ const hasEndpoint = opts.provider !== undefined || opts.rpc !== undefined;
576
+ const hasContract = opts.contract !== undefined;
577
+ const mode = hasEndpoint || hasContract ? "rpc" : "offline";
578
+
579
+ const reject = (reason, detail, field) => {
580
+ if (opts.json) {
581
+ write(JSON.stringify({ ok: false, verdict: "REJECTED", mode, reason, field, detail }, null, 2) + "\n");
582
+ } else {
583
+ writeErr(`verify-anchored: REJECTED (${reason})${detail ? `: ${detail}` : ""}\n`);
584
+ }
585
+ return EXIT.REJECT;
586
+ };
587
+
588
+ if (!opts.receipt || !opts.artifact) {
589
+ writeErr("error: `vh verify-anchored` requires a <receipt> and a <sealed-file>\n" + VERIFY_ANCHORED_USAGE);
590
+ return EXIT.USAGE;
591
+ }
592
+ if (hasEndpoint !== hasContract) {
593
+ writeErr(
594
+ "error: the on-chain re-check needs BOTH --rpc <url> AND --contract <address> " +
595
+ "(omit both for the offline binding check)\n" +
596
+ VERIFY_ANCHORED_USAGE
597
+ );
598
+ return EXIT.USAGE;
599
+ }
600
+ if (hasContract && !ethersLib.isAddress(opts.contract)) {
601
+ writeErr(`error: invalid contract address: ${opts.contract}\n`);
602
+ return EXIT.USAGE;
603
+ }
604
+
605
+ const receipt = readJson("receipt", opts.receipt, writeErr);
606
+ if (receipt === null) return EXIT.IO;
607
+ const artifact = readJson("artifact", opts.artifact, writeErr);
608
+ if (artifact === null) return EXIT.IO;
609
+
610
+ // ---- leg 1 (always): the pure, offline binding verify — the T-70.1 core, reused verbatim ----
611
+ const v = binding.verifyAnchoredReceipt({ receipt, artifact });
612
+ if (!v.ok) return reject(v.reason, v.detail, v.field);
613
+
614
+ // ---- offline mode stops here: the binding holds; the chain facts remain the anchorer's CLAIM ----
615
+ if (!hasContract) {
616
+ if (opts.json) {
617
+ write(
618
+ JSON.stringify(
619
+ {
620
+ ok: true,
621
+ verdict: "ACCEPTED",
622
+ mode,
623
+ digest: v.digest,
624
+ artifactKind: receipt.artifactKind,
625
+ chain: v.chain,
626
+ registry: null,
627
+ note:
628
+ "OFFLINE verify: the receipt binds this exact artifact, but its chain facts were NOT " +
629
+ "re-checked. Pass --rpc <url> --contract <addr> to confirm them against the chain.",
630
+ },
631
+ null,
632
+ 2
633
+ ) + "\n"
634
+ );
635
+ } else {
636
+ write("verify-anchored: ACCEPTED (offline binding check)\n");
637
+ write(` digest: ${v.digest}\n`);
638
+ write(` kind: ${receipt.artifactKind}\n`);
639
+ write(
640
+ ` chain CLAIM: chainId ${v.chain.chainId}, contract ${v.chain.contract}, tx ${v.chain.txHash}, ` +
641
+ `block ${v.chain.blockNumber}, blockTime ${v.chain.blockTime}, contributor ${v.chain.contributor}, ` +
642
+ `authorBound ${v.chain.authorBound}\n`
643
+ );
644
+ write(
645
+ " NOTE: offline mode did NOT re-check the chain facts — they are the anchorer's claim. " +
646
+ "Pass --rpc <url> --contract <addr> to confirm them against the chain.\n"
647
+ );
648
+ }
649
+ return EXIT.OK;
650
+ }
651
+
652
+ // ---- leg 2 (--rpc --contract): authenticate the registry FIRST (the EXISTING EPIC-11 identity
653
+ // probe — no record is believed until the contract self-identifies on the receipt's chain),
654
+ // then re-check every chain fact the receipt claims. ----
655
+ const provider = opts.provider || new ethersLib.JsonRpcProvider(opts.rpc);
656
+ let auth;
657
+ try {
658
+ auth = await assertRegistry({
659
+ provider,
660
+ contractAddress: opts.contract,
661
+ expectedChainId: v.chain.chainId,
662
+ ethers: ethersLib,
663
+ });
664
+ } catch (e) {
665
+ if (e && e.code === "REGISTRY_AUTH_FAILED") {
666
+ // The EXISTING identity-probe reject, surfaced verbatim (wrong address / non-registry / wrong chain).
667
+ return reject("registry-auth-failed", e.message);
668
+ }
669
+ if (isGenuineRpcError(e)) {
670
+ writeErr(`error: RPC failure during the registry identity check: ${e.message}\n`);
671
+ return EXIT.IO;
672
+ }
673
+ writeErr(`error: ${e.message}\n`);
674
+ return EXIT.IO;
675
+ }
676
+
677
+ const contractLc = ethersLib.getAddress(opts.contract).toLowerCase();
678
+ if (contractLc !== v.chain.contract) {
679
+ return reject(
680
+ "contract-mismatch",
681
+ `the receipt was anchored on contract ${v.chain.contract} but you passed --contract ${contractLc} — ` +
682
+ "a record on a different contract says nothing about this receipt"
683
+ );
684
+ }
685
+
686
+ const contract = new ethersLib.Contract(ethersLib.getAddress(opts.contract), ABI, provider);
687
+ let rec;
688
+ try {
689
+ rec = await contract.getRecord(v.digest);
690
+ } catch (e) {
691
+ const named = namedRegistryReject(e, ethersLib);
692
+ if (named && named.startsWith("NotAnchored")) {
693
+ return reject(
694
+ "not-anchored-on-chain",
695
+ `the registry has NO record for digest ${v.digest} (${named}) — the receipt's chain facts are not real`
696
+ );
697
+ }
698
+ if (isGenuineRpcError(e)) {
699
+ writeErr(`error: RPC failure while reading the record back: ${e.message}\n`);
700
+ return EXIT.IO;
701
+ }
702
+ writeErr(`error: ${e.message}\n`);
703
+ return EXIT.IO;
704
+ }
705
+ const onchain = {
706
+ contributor: String(rec.contributor).toLowerCase(),
707
+ authorBound: Boolean(rec.authorBound),
708
+ blockNumber: Number(rec.blockNumber),
709
+ blockTime: Number(rec.timestamp),
710
+ };
711
+ if (onchain.contributor !== v.chain.contributor) {
712
+ return reject(
713
+ "contributor-mismatch",
714
+ `on-chain contributor ${onchain.contributor} != receipt contributor ${v.chain.contributor}`
715
+ );
716
+ }
717
+ if (onchain.authorBound !== v.chain.authorBound) {
718
+ return reject(
719
+ "author-bound-mismatch",
720
+ `on-chain authorBound ${onchain.authorBound} != receipt authorBound ${v.chain.authorBound}`
721
+ );
722
+ }
723
+ if (onchain.blockNumber !== v.chain.blockNumber) {
724
+ return reject(
725
+ "block-number-mismatch",
726
+ `on-chain record block ${onchain.blockNumber} != receipt blockNumber ${v.chain.blockNumber}`
727
+ );
728
+ }
729
+ if (onchain.blockTime !== v.chain.blockTime) {
730
+ return reject(
731
+ "block-time-mismatch",
732
+ `on-chain record timestamp ${onchain.blockTime} != receipt blockTime ${v.chain.blockTime}`
733
+ );
734
+ }
735
+
736
+ // The receipt's txHash must be a REAL mined transaction, in the recorded block, targeting the
737
+ // recorded contract — an edited txHash cannot masquerade as the anchoring write.
738
+ let txr;
739
+ try {
740
+ txr = await provider.getTransactionReceipt(v.chain.txHash);
741
+ } catch (e) {
742
+ if (isGenuineRpcError(e)) {
743
+ writeErr(`error: RPC failure while re-checking the anchoring tx: ${e.message}\n`);
744
+ return EXIT.IO;
745
+ }
746
+ writeErr(`error: ${e.message}\n`);
747
+ return EXIT.IO;
748
+ }
749
+ if (!txr) {
750
+ return reject(
751
+ "tx-not-found",
752
+ `no transaction ${v.chain.txHash} exists on this chain — the receipt's txHash is not real`
753
+ );
754
+ }
755
+ if (Number(txr.blockNumber) !== v.chain.blockNumber) {
756
+ return reject(
757
+ "tx-block-mismatch",
758
+ `tx ${v.chain.txHash} mined in block ${Number(txr.blockNumber)}, not the receipt's block ${v.chain.blockNumber}`
759
+ );
760
+ }
761
+ if (txr.to && String(txr.to).toLowerCase() !== v.chain.contract) {
762
+ return reject(
763
+ "tx-target-mismatch",
764
+ `tx ${v.chain.txHash} targets ${String(txr.to).toLowerCase()}, not the receipt's contract ${v.chain.contract}`
765
+ );
766
+ }
767
+
768
+ if (opts.json) {
769
+ write(
770
+ JSON.stringify(
771
+ {
772
+ ok: true,
773
+ verdict: "ACCEPTED",
774
+ mode,
775
+ digest: v.digest,
776
+ artifactKind: receipt.artifactKind,
777
+ chain: v.chain,
778
+ registry: jsonRegistryBlock(auth),
779
+ onchain,
780
+ note:
781
+ "The registry was authenticated (EPIC-11 identity probe) and every chain fact in the " +
782
+ "receipt matches the on-chain record and its mined transaction.",
783
+ },
784
+ null,
785
+ 2
786
+ ) + "\n"
787
+ );
788
+ } else {
789
+ write("verify-anchored: ACCEPTED (offline binding + on-chain re-check)\n");
790
+ write(formatRegistryLine(auth) + "\n");
791
+ write(` digest: ${v.digest}\n`);
792
+ write(` kind: ${receipt.artifactKind}\n`);
793
+ write(
794
+ ` on-chain: contributor ${onchain.contributor}, authorBound ${onchain.authorBound}, ` +
795
+ `block ${onchain.blockNumber}, blockTime ${onchain.blockTime} — ALL match the receipt\n`
796
+ );
797
+ write(` tx: ${v.chain.txHash} found in block ${Number(txr.blockNumber)}, targeting the recorded contract\n`);
798
+ }
799
+ return EXIT.OK;
800
+ }
801
+
802
+ // ---------------------------------------------------------------------------------------------------
803
+ // cmd wrappers (argv -> opts -> run). io is optional and defaults to the process streams.
804
+ // ---------------------------------------------------------------------------------------------------
805
+
806
+ async function cmdAnchorArtifact(argv, io = {}) {
807
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
808
+ let opts;
809
+ try {
810
+ opts = parseAnchorArtifactArgs(argv);
811
+ } catch (e) {
812
+ writeErr(`error: ${e.message}\n` + ANCHOR_ARTIFACT_USAGE);
813
+ return EXIT.USAGE;
814
+ }
815
+ return runAnchorArtifact(
816
+ {
817
+ ...opts,
818
+ // The same env fallbacks the other write verbs honor — for the ADDRESS and ENDPOINT only.
819
+ // The signing key NEVER has an implicit env fallback: only --key-env/--key-file name it.
820
+ contract: opts.contract || process.env.VH_CONTRACT,
821
+ rpc: opts.rpc || process.env.VH_RPC_URL || process.env.AMOY_RPC_URL,
822
+ },
823
+ io
824
+ );
825
+ }
826
+
827
+ async function cmdVerifyAnchored(argv, io = {}) {
828
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
829
+ let opts;
830
+ try {
831
+ opts = parseVerifyAnchoredArgs(argv);
832
+ } catch (e) {
833
+ writeErr(`error: ${e.message}\n` + VERIFY_ANCHORED_USAGE);
834
+ return EXIT.USAGE;
835
+ }
836
+ // No env fallbacks here on purpose: verify-anchored is OFFLINE unless the caller EXPLICITLY passes
837
+ // both --rpc and --contract (an env var must never silently flip a verify onto a network).
838
+ return runVerifyAnchored(opts, io);
839
+ }
840
+
841
+ module.exports = {
842
+ EXIT,
843
+ ANCHOR_ARTIFACT_USAGE,
844
+ VERIFY_ANCHORED_USAGE,
845
+ parseAnchorArtifactArgs,
846
+ parseVerifyAnchoredArgs,
847
+ runAnchorArtifact,
848
+ runVerifyAnchored,
849
+ cmdAnchorArtifact,
850
+ cmdVerifyAnchored,
851
+ namedRegistryReject,
852
+ ABI,
853
+ };