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
package/cli/anchor.js ADDED
@@ -0,0 +1,400 @@
1
+ "use strict";
2
+
3
+ // `vh anchor <path> [--uri <uri>]` — submit a contribution's content hash on-chain.
4
+ //
5
+ // The flow:
6
+ // 1. Hash the target path (file -> keccak256 of bytes; directory -> sorted-leaf Merkle root),
7
+ // reusing the exact logic the contract's verifyLeaf convention expects (see cli/hash.js).
8
+ // 2. Encode a call to ContributionRegistry.anchor(contentHash, uri).
9
+ // 3. Either *print* that transaction (`--dry-run`, no key required) or *send* it with a signer.
10
+ //
11
+ // Safety rails:
12
+ // * --dry-run never touches a key and never broadcasts; it only shows the tx that would be sent.
13
+ // * Submitting refuses to run against a chainId that is not a known testnet/dev chain unless the
14
+ // operator passes --i-understand-mainnet. This keeps the auto-build loop (and a sleepy human)
15
+ // from accidentally spending real funds / writing to a production chain.
16
+ //
17
+ // This module is split into small, side-effect-free pieces (buildAnchorTx, chainId guard) so the
18
+ // integration test can drive it against a local hardhat node and assert the on-chain Anchored event.
19
+
20
+ const { hashPath, hashGit } = require("./hash");
21
+ const { buildAnchorReceipt, writeReceipt } = require("./receipt");
22
+
23
+ const ARTIFACT = require("./core/registryArtifact");
24
+ const ABI = ARTIFACT.abi;
25
+
26
+ // Chains we consider safe to anchor on without an explicit override. These are local dev chains and
27
+ // public testnets only — never mainnets. Anything outside this set is treated as "could be mainnet"
28
+ // and is blocked unless --i-understand-mainnet is given.
29
+ //
30
+ // 31337 Hardhat (local) 1337 Ganache / generic local dev
31
+ // 80002 Polygon Amoy testnet 80001 Polygon Mumbai testnet (legacy)
32
+ // 11155111 Ethereum Sepolia 17000 Ethereum Holesky
33
+ // 5 Ethereum Goerli (legacy) 11155420 Optimism Sepolia
34
+ // 84532 Base Sepolia 421614 Arbitrum Sepolia
35
+ const KNOWN_TESTNET_CHAIN_IDS = new Set([
36
+ 31337n, 1337n, 80002n, 80001n, 11155111n, 17000n, 5n, 11155420n, 84532n, 421614n,
37
+ ]);
38
+
39
+ /** True iff `chainId` is a known local/dev/testnet chain that is safe to anchor on by default. */
40
+ function isTestnetChainId(chainId) {
41
+ return KNOWN_TESTNET_CHAIN_IDS.has(BigInt(chainId));
42
+ }
43
+
44
+ /**
45
+ * Compute the content hash to anchor for a filesystem path.
46
+ * A file anchors its keccak256 digest; a directory anchors its sorted-leaf Merkle root. For a
47
+ * directory the per-file MANIFEST (sorted `{ path, contentHash, leaf }` — exactly what `vh hash <dir>`
48
+ * produces) is also returned so an anchor receipt can record it and a later `vh verify --receipt`
49
+ * can localize which file diverged.
50
+ *
51
+ * With `opts.git`, the root and manifest are computed over EXACTLY the files git tracks at `opts.ref`
52
+ * (default HEAD) — the same reproducible, untracked-junk-ignoring enumeration as `vh hash --git`
53
+ * (T-8.1) — and a `git` provenance block `{ commit, scope }` is returned so the receipt can record
54
+ * the resolved commit oid and the repo-relative scope used to enumerate the tracked set. That block
55
+ * is an UNTRUSTED convenience hint (docs/TRUST-BOUNDARIES); the anchored contentHash is unchanged
56
+ * whether it was derived by the filesystem walk or the git walk for the same tracked content.
57
+ *
58
+ * @param {string} targetPath
59
+ * @param {{ git?: boolean, ref?: string }} [opts]
60
+ * @returns {{ contentHash: string, kind: "file"|"dir",
61
+ * manifest: Array<{path:string,contentHash:string,leaf:string}>|null,
62
+ * git: {commit:string,scope:string}|null }}
63
+ */
64
+ function contentHashForPath(targetPath, opts = {}) {
65
+ if (opts.git) {
66
+ // git-scoped: a directory root over the tracked set, with provenance. (--git always means a dir
67
+ // root; hashGit errors clearly on a non-git dir / unknown ref / zero tracked files.)
68
+ const res = hashGit(targetPath, { ref: opts.ref });
69
+ const manifest = res.leaves.map((l) => ({
70
+ path: l.path,
71
+ contentHash: l.contentHash,
72
+ leaf: l.leaf,
73
+ }));
74
+ return {
75
+ contentHash: res.root,
76
+ kind: "dir",
77
+ manifest,
78
+ git: { commit: res.commit, scope: res.scope },
79
+ };
80
+ }
81
+ const res = hashPath(targetPath);
82
+ const manifest =
83
+ res.kind === "dir" && Array.isArray(res.leaves)
84
+ ? res.leaves.map((l) => ({ path: l.path, contentHash: l.contentHash, leaf: l.leaf }))
85
+ : null;
86
+ return { contentHash: res.root, kind: res.kind, manifest, git: null };
87
+ }
88
+
89
+ /**
90
+ * Build (but do not send) the anchor transaction for a path. No private key, signer, or network
91
+ * connection is required — this is exactly what `--dry-run` prints. Returns both the encoded EVM
92
+ * transaction request and the human-readable pieces that went into it.
93
+ *
94
+ * @param {object} opts
95
+ * @param {string} opts.path path to a file or directory to hash & anchor
96
+ * @param {string} [opts.uri] optional off-chain pointer stored alongside the hash
97
+ * @param {boolean}[opts.git] hash EXACTLY the git-tracked files (ignores untracked junk)
98
+ * @param {string} [opts.ref] with git: which commit's tracked set to hash (default HEAD)
99
+ * @param {string} [opts.parent] optional predecessor contentHash (the immutable lineage edge,
100
+ * T-10.1). Omitted/zero -> a lineage root via the plain
101
+ * `anchor()`; a non-zero 32-byte hash routes to
102
+ * `anchorWithParent()` and records the edge child->parent.
103
+ * @param {string} opts.contractAddress deployed ContributionRegistry address (the tx `to`)
104
+ * @param {object} [opts.ethers] an ethers v6 module (defaults to the one bundled here)
105
+ * @returns {{
106
+ * to: string, data: string, value: string,
107
+ * contentHash: string, uri: string, kind: "file"|"dir", path: string,
108
+ * manifest: Array|null, git: {commit:string,scope:string}|null,
109
+ * parent: string|null,
110
+ * functionName: "anchor"|"anchorWithParent"
111
+ * }}
112
+ */
113
+ function buildAnchorTx(opts) {
114
+ const { path: targetPath, contractAddress } = opts;
115
+ const ethersLib = opts.ethers || require("ethers");
116
+ const uri = opts.uri == null ? "" : String(opts.uri);
117
+
118
+ if (!targetPath) throw new Error("anchor requires a <path>");
119
+ if (!contractAddress) {
120
+ throw new Error(
121
+ "no contract address: pass --contract <address> or set VH_CONTRACT in the environment"
122
+ );
123
+ }
124
+ if (!ethersLib.isAddress(contractAddress)) {
125
+ throw new Error(`invalid contract address: ${contractAddress}`);
126
+ }
127
+
128
+ const { contentHash, kind, manifest, git } = contentHashForPath(targetPath, {
129
+ git: opts.git,
130
+ ref: opts.ref,
131
+ });
132
+ // The contract reverts on a zero hash; catch it here with a clearer message before we ever
133
+ // try to build/send a doomed transaction.
134
+ if (/^0x0{64}$/i.test(contentHash)) {
135
+ throw new Error("refusing to anchor the zero hash (contract rejects it)");
136
+ }
137
+
138
+ // Resolve the optional lineage edge. A missing/empty/zero parent means "no predecessor / lineage
139
+ // root" and routes to the legacy `anchor()` (byte-for-byte unchanged). A non-zero parent must be a
140
+ // well-formed 32-byte hash and routes to `anchorWithParent()`. We validate shape + self-reference
141
+ // here so a typo hard-errors BEFORE building a doomed tx; the contract still enforces UnknownParent
142
+ // (the parent must already be anchored) and SelfParent on-chain as the authoritative checks.
143
+ const parent = normalizeParent(opts.parent, ethersLib);
144
+ if (parent !== null && parent.toLowerCase() === contentHash.toLowerCase()) {
145
+ throw new Error(
146
+ "refusing to anchor a record as its own parent (self-reference; the contract rejects it as SelfParent)"
147
+ );
148
+ }
149
+
150
+ const iface = new ethersLib.Interface(ABI);
151
+ const functionName = parent === null ? "anchor" : "anchorWithParent";
152
+ const data =
153
+ parent === null
154
+ ? iface.encodeFunctionData("anchor", [contentHash, uri])
155
+ : iface.encodeFunctionData("anchorWithParent", [contentHash, uri, parent]);
156
+
157
+ return {
158
+ to: ethersLib.getAddress(contractAddress),
159
+ data,
160
+ value: "0x0", // anchor()/anchorWithParent() are non-payable; never attach value.
161
+ contentHash,
162
+ uri,
163
+ kind,
164
+ path: targetPath,
165
+ manifest, // per-file manifest for a dir (null for a file); recorded into a --receipt
166
+ git, // { commit, scope } when --git was used; null otherwise. Recorded into a --receipt.
167
+ parent, // null for a lineage root; the predecessor hash when --parent was given.
168
+ functionName,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Normalize an optional `--parent` value into either null (no edge / lineage root) or a validated,
174
+ * lowercased 32-byte 0x hash. Empty/undefined/null and the zero hash all mean "root" (-> null), so a
175
+ * caller can pass through an unset flag freely. A malformed non-zero value is a usage-grade error so a
176
+ * typo'd parent hard-errors before any tx is built (it never silently becomes a root).
177
+ *
178
+ * @param {string|undefined|null} value
179
+ * @param {object} ethersLib ethers v6 module (for isHexString)
180
+ * @returns {string|null} the normalized parent hash, or null for "no predecessor"
181
+ */
182
+ function normalizeParent(value, ethersLib) {
183
+ if (value === undefined || value === null || value === "") return null;
184
+ if (typeof value !== "string") {
185
+ throw new Error(`invalid --parent: expected a 0x 32-byte hash, got ${typeof value}`);
186
+ }
187
+ if (!ethersLib.isHexString(value, 32)) {
188
+ throw new Error(
189
+ `invalid --parent: must be a 32-byte (0x + 64 hex) content hash, got: ${value}`
190
+ );
191
+ }
192
+ // The zero hash is the explicit "no predecessor" sentinel -> treat as a root (null), matching the
193
+ // contract (a zero parent skips the edge entirely and emits no Linked event).
194
+ if (/^0x0{64}$/i.test(value)) return null;
195
+ return value.toLowerCase();
196
+ }
197
+
198
+ /** Render a built anchor tx as the multi-line block `--dry-run` prints. */
199
+ function formatDryRun(tx, chainId) {
200
+ const lines = [
201
+ "DRY RUN — no transaction will be sent.",
202
+ "",
203
+ ` path: ${tx.path} (${tx.kind})`,
204
+ ` contentHash: ${tx.contentHash}`,
205
+ ` uri: ${tx.uri === "" ? "(none)" : tx.uri}`,
206
+ // Lineage edge (T-10.1): show whether this would be a root or a child of `parent`, and which
207
+ // write path (anchor vs anchorWithParent) it routes to, so a dry-run reader sees the edge.
208
+ ` parent: ${tx.parent == null ? "(none) — lineage root" : tx.parent}`,
209
+ ` function: ${tx.functionName}`,
210
+ ];
211
+ if (tx.git) {
212
+ // Provenance is an untrusted convenience hint — say so, so it is never mistaken for the verdict.
213
+ lines.push(
214
+ ` git commit: ${tx.git.commit} (untrusted provenance hint)`,
215
+ ` git scope: ${tx.git.scope}`
216
+ );
217
+ }
218
+ lines.push("");
219
+ lines.push(
220
+ " Transaction that WOULD be sent:",
221
+ ` to: ${tx.to}`,
222
+ ` value: ${tx.value}`,
223
+ ` data: ${tx.data}`
224
+ );
225
+ if (chainId != null) lines.push(` chainId: ${BigInt(chainId).toString()}`);
226
+ lines.push("");
227
+ return lines.join("\n");
228
+ }
229
+
230
+ /**
231
+ * Run the anchor command end to end.
232
+ *
233
+ * In `--dry-run` mode it returns `{ dryRun: true, tx }` after only building the tx (no key, no
234
+ * network write). Otherwise it enforces the testnet guard, sends the tx with `signer`, waits for it
235
+ * to mine, and parses the `Anchored` event off the receipt.
236
+ *
237
+ * @param {object} opts
238
+ * @param {string} opts.path
239
+ * @param {string} [opts.uri]
240
+ * @param {boolean}[opts.git] hash EXACTLY the git-tracked files (T-8.1 enumeration)
241
+ * @param {string} [opts.ref] with git: which commit's tracked set (default HEAD)
242
+ * @param {string} [opts.parent] optional predecessor contentHash (T-10.1 lineage edge);
243
+ * non-zero routes to anchorWithParent()
244
+ * @param {string} opts.contractAddress
245
+ * @param {boolean}[opts.dryRun]
246
+ * @param {boolean}[opts.iUnderstandMainnet] bypass the non-testnet chainId refusal
247
+ * @param {object} [opts.signer] ethers Signer (required unless dryRun)
248
+ * @param {object} [opts.provider] ethers Provider (used to read chainId; falls back to signer.provider)
249
+ * @param {bigint|number}[opts.chainId] override/short-circuit the chainId lookup (tests)
250
+ * @param {string} [opts.receiptPath] if set, write an anchor receipt here (records the dir
251
+ * manifest so `vh verify --receipt` can localize a tamper)
252
+ * @param {object} [opts.ethers] ethers v6 module
253
+ * @param {(s:string)=>void}[opts.log] sink for human output (defaults to process.stdout)
254
+ * @returns {Promise<object>} result describing what happened (includes `receiptPath` when one was written)
255
+ */
256
+ async function runAnchor(opts) {
257
+ const ethersLib = opts.ethers || require("ethers");
258
+ const log = opts.log || ((s) => process.stdout.write(s));
259
+
260
+ const tx = buildAnchorTx({
261
+ path: opts.path,
262
+ uri: opts.uri,
263
+ git: opts.git,
264
+ ref: opts.ref,
265
+ parent: opts.parent,
266
+ contractAddress: opts.contractAddress,
267
+ ethers: ethersLib,
268
+ });
269
+
270
+ // Resolve the chainId we'd be writing to (override > provider > signer.provider).
271
+ let chainId = opts.chainId;
272
+ const provider = opts.provider || (opts.signer && opts.signer.provider);
273
+ if (chainId == null && provider) {
274
+ const net = await provider.getNetwork();
275
+ chainId = net.chainId;
276
+ }
277
+
278
+ // Write a receipt (recording the dir manifest) when asked. For a dry run we still write it if a
279
+ // path was given, so a user can produce the manifest offline without ever broadcasting; the
280
+ // anchorTxHash/anchorBlockNumber are simply omitted until a real submission fills them in.
281
+ const maybeWriteReceipt = (extra) => {
282
+ if (!opts.receiptPath) return undefined;
283
+ const receipt = buildAnchorReceipt({
284
+ contentHash: tx.contentHash,
285
+ contractAddress: tx.to,
286
+ chainId: chainId == null ? 0 : chainId,
287
+ uri: tx.uri,
288
+ path: tx.path,
289
+ kind: tx.kind,
290
+ manifest: tx.manifest || undefined,
291
+ git: tx.git || undefined, // untrusted provenance hint: { commit, scope } when --git was used
292
+ ...extra,
293
+ });
294
+ writeReceipt(receipt, opts.receiptPath);
295
+ log(` receipt written: ${opts.receiptPath}\n`);
296
+ return receipt;
297
+ };
298
+
299
+ if (opts.dryRun) {
300
+ log(formatDryRun(tx, chainId) + "\n");
301
+ const receipt = maybeWriteReceipt();
302
+ return {
303
+ dryRun: true,
304
+ tx,
305
+ chainId: chainId == null ? null : BigInt(chainId),
306
+ receiptPath: opts.receiptPath,
307
+ receipt,
308
+ };
309
+ }
310
+
311
+ // Real submission from here on — enforce the safety rail first.
312
+ if (chainId == null) {
313
+ throw new Error("cannot determine chainId; refusing to submit without knowing the network");
314
+ }
315
+ if (!isTestnetChainId(chainId) && !opts.iUnderstandMainnet) {
316
+ throw new Error(
317
+ `refusing to anchor on chainId ${BigInt(chainId).toString()} (not a known testnet). ` +
318
+ "If you really mean to write to this chain, re-run with --i-understand-mainnet."
319
+ );
320
+ }
321
+ if (!opts.signer) {
322
+ throw new Error("no signer available to submit the transaction (set PRIVATE_KEY?)");
323
+ }
324
+
325
+ const contract = new ethersLib.Contract(tx.to, ABI, opts.signer);
326
+ const lineageNote = tx.parent == null ? "" : ` with parent ${tx.parent}`;
327
+ log(
328
+ `Anchoring ${tx.path} (${tx.kind}) as ${tx.contentHash}${lineageNote} on chainId ${BigInt(chainId)}...\n`
329
+ );
330
+
331
+ // Route to anchorWithParent() iff a non-zero predecessor was given (T-10.1); otherwise the legacy
332
+ // anchor() path, byte-for-byte unchanged. The contract enforces UnknownParent/SelfParent.
333
+ const sent =
334
+ tx.parent == null
335
+ ? await contract.anchor(tx.contentHash, tx.uri)
336
+ : await contract.anchorWithParent(tx.contentHash, tx.uri, tx.parent);
337
+ log(` tx sent: ${sent.hash}\n`);
338
+ const receipt = await sent.wait();
339
+
340
+ // Pull the Anchored event back out of the receipt so callers see what was recorded. Also surface
341
+ // the parallel Linked(child, parent) edge log (T-10.1) when a parented record was written, so the
342
+ // lineage edge is observable from the same result the caller already gets back.
343
+ const iface = new ethersLib.Interface(ABI);
344
+ let anchored = null;
345
+ let linked = null;
346
+ for (const lg of receipt.logs) {
347
+ try {
348
+ const parsed = iface.parseLog({ topics: lg.topics, data: lg.data });
349
+ if (parsed && parsed.name === "Anchored") {
350
+ anchored = {
351
+ contentHash: parsed.args.contentHash,
352
+ contributor: parsed.args.contributor,
353
+ index: parsed.args.index,
354
+ timestamp: parsed.args.timestamp,
355
+ uri: parsed.args.uri,
356
+ };
357
+ } else if (parsed && parsed.name === "Linked") {
358
+ linked = { child: parsed.args.child, parent: parsed.args.parent };
359
+ }
360
+ } catch (_) {
361
+ // Not one of our events; skip.
362
+ }
363
+ }
364
+
365
+ if (anchored) {
366
+ const lineageMsg = linked ? ` (lineage edge -> parent ${linked.parent})` : "";
367
+ log(
368
+ ` Anchored at index ${anchored.index} by ${anchored.contributor} in tx ${receipt.hash}${lineageMsg}\n`
369
+ );
370
+ }
371
+
372
+ // Persist an anchor receipt (with the dir manifest + the now-known tx hash/block) when asked.
373
+ const anchorReceipt = maybeWriteReceipt({
374
+ anchorTxHash: receipt.hash,
375
+ anchorBlockNumber: receipt.blockNumber,
376
+ });
377
+
378
+ return {
379
+ dryRun: false,
380
+ tx,
381
+ chainId: BigInt(chainId),
382
+ txHash: receipt.hash,
383
+ receipt,
384
+ anchored,
385
+ linked, // { child, parent } when a lineage edge was written (T-10.1); null for a root
386
+ receiptPath: opts.receiptPath,
387
+ anchorReceipt,
388
+ };
389
+ }
390
+
391
+ module.exports = {
392
+ buildAnchorTx,
393
+ runAnchor,
394
+ formatDryRun,
395
+ contentHashForPath,
396
+ normalizeParent,
397
+ isTestnetChainId,
398
+ KNOWN_TESTNET_CHAIN_IDS,
399
+ ABI,
400
+ };