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.
- package/LICENSE +201 -0
- package/README.md +883 -0
- package/cli/abi/ContributionRegistry.json +881 -0
- package/cli/agent.js +2173 -0
- package/cli/anchor-artifact.js +853 -0
- package/cli/anchor.js +400 -0
- package/cli/claim.js +881 -0
- package/cli/core/agent-commit.js +448 -0
- package/cli/core/agent-session.js +598 -0
- package/cli/core/anchor-binding.js +663 -0
- package/cli/core/attestation.js +580 -0
- package/cli/core/evidence-plans.js +495 -0
- package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
- package/cli/core/fulfill-intake.js +1082 -0
- package/cli/core/go-live-preflight.js +481 -0
- package/cli/core/license.js +534 -0
- package/cli/core/manifest.js +243 -0
- package/cli/core/packetseal.js +591 -0
- package/cli/core/registryArtifact.js +49 -0
- package/cli/core/revocation.js +539 -0
- package/cli/core/rfc3161.js +389 -0
- package/cli/core/timestamp.js +482 -0
- package/cli/core/trust-asof.js +479 -0
- package/cli/dataset.js +2950 -0
- package/cli/evidence.js +2227 -0
- package/cli/fulfill-webhook-http.js +438 -0
- package/cli/git.js +220 -0
- package/cli/hash.js +550 -0
- package/cli/identity.js +1072 -0
- package/cli/journal-cli.js +1110 -0
- package/cli/journal-log.js +454 -0
- package/cli/journal.js +334 -0
- package/cli/lineage.js +447 -0
- package/cli/list.js +287 -0
- package/cli/parcel.js +1509 -0
- package/cli/proof.js +578 -0
- package/cli/prove.js +300 -0
- package/cli/receipt.js +631 -0
- package/cli/registry.js +331 -0
- package/cli/reputation.js +344 -0
- package/cli/revocation.js +495 -0
- package/cli/serve-verify-http.js +298 -0
- package/cli/serve-verify.js +333 -0
- package/cli/show.js +339 -0
- package/cli/verify.js +383 -0
- package/cli/vh.js +3927 -0
- package/docs/ADOPT.md +183 -0
- package/docs/ADOPTION.json +11 -0
- package/docs/AGENTTRACE.md +247 -0
- package/docs/ANCHORING.md +167 -0
- package/docs/AUDIT.md +55 -0
- package/docs/CONFORMANCE.md +107 -0
- package/docs/DATALEDGER.md +638 -0
- package/docs/DECIDE.md +47 -0
- package/docs/DECISIONS-PENDING.md +27 -0
- package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
- package/docs/ENGINE-LEDGER.json +12 -0
- package/docs/EVIDENCE.md +519 -0
- package/docs/GO-LIVE.md +66 -0
- package/docs/IDENTITY.md +123 -0
- package/docs/INDEPENDENT-VERIFICATION.md +377 -0
- package/docs/INTEGRITY-JOURNAL.md +337 -0
- package/docs/KEY-LIFECYCLE.md +179 -0
- package/docs/LICENSING.md +46 -0
- package/docs/LINEAGE.md +307 -0
- package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
- package/docs/LOOP-HARDENING-PLAN.md +44 -0
- package/docs/MERKLE-LEAVES.md +113 -0
- package/docs/METRICS.jsonl +31 -0
- package/docs/MORNING.md +204 -0
- package/docs/PILOT.md +444 -0
- package/docs/PROOFPARCEL.md +227 -0
- package/docs/PROOFS.md +262 -0
- package/docs/RECEIPTS.md +341 -0
- package/docs/REPUTATION.md +158 -0
- package/docs/SDK.md +301 -0
- package/docs/STRATEGY-ARCHIVE.md +5055 -0
- package/docs/SUPERVISOR-RUNBOOK.md +52 -0
- package/docs/TRUST-BOUNDARIES.md +335 -0
- package/docs/TRUSTLEDGER.md +1976 -0
- package/docs/USAGE-BUDGET.json +121 -0
- package/docs/VERIFY-SERVICE.md +168 -0
- package/index.js +160 -0
- package/package.json +41 -0
- package/trustledger/build-standalone.js +796 -0
- package/trustledger/cli.js +3179 -0
- package/trustledger/close.js +391 -0
- package/trustledger/corpus.js +159 -0
- package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
- package/trustledger/dist/trustledger-standalone.html +6197 -0
- package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
- package/trustledger/door-core.js +442 -0
- package/trustledger/fixtures/bank.csv +7 -0
- package/trustledger/fixtures/bank.malformed.csv +3 -0
- package/trustledger/fixtures/bank.noalias.csv +5 -0
- package/trustledger/fixtures/bank.ofx +34 -0
- package/trustledger/fixtures/bank.real.csv +5 -0
- package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
- package/trustledger/fixtures/e2e/bank.csv +4 -0
- package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
- package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
- package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.csv +6 -0
- package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
- package/trustledger/fixtures/plans/baseline.json +25 -0
- package/trustledger/fixtures/plans/price-binding.example.json +27 -0
- package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
- package/trustledger/fixtures/policy/baseline.json +19 -0
- package/trustledger/fixtures/policy/ca-example.json +12 -0
- package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
- package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
- package/trustledger/fixtures/quickbooks.csv +7 -0
- package/trustledger/fixtures/quickbooks.real.csv +5 -0
- package/trustledger/fixtures/rentroll.csv +6 -0
- package/trustledger/fixtures/rentroll.real.csv +4 -0
- package/trustledger/ingest.js +1163 -0
- package/trustledger/lib/policy-bundled-loader.js +44 -0
- package/trustledger/lib/sha256-vendored.js +227 -0
- package/trustledger/license.js +563 -0
- package/trustledger/match.js +551 -0
- package/trustledger/plans.js +551 -0
- package/trustledger/policy.js +398 -0
- package/trustledger/public/index.html +512 -0
- package/trustledger/reconcile.js +1486 -0
- package/trustledger/report.js +887 -0
- package/trustledger/seal.js +854 -0
- package/trustledger/server.js +391 -0
- package/trustledger/valueproof.js +350 -0
package/cli/registry.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// cli/registry.js — authenticate a deployed ContributionRegistry BEFORE believing any record it
|
|
4
|
+
// returns (T-11.2).
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS EXISTS
|
|
7
|
+
// Every read command (`vh verify` / `show` / `list` / `lineage` / `verify-proof`) historically
|
|
8
|
+
// trusted whatever (rpc, address) pair the caller supplied: it just called `getRecord` and reported
|
|
9
|
+
// the answer. But the answer is only as trustworthy as the assumption that the address really is a
|
|
10
|
+
// verifyhash registry on the network the caller thinks it is. Two failure modes silently produced a
|
|
11
|
+
// confident-looking-but-wrong verdict:
|
|
12
|
+
// * pointed at an address with NO contract (typo'd address, or right address on the WRONG network)
|
|
13
|
+
// — `getRecord` reverts and is read as "not anchored" (a MISMATCH), so a genuinely-anchored
|
|
14
|
+
// contribution is mislabeled as tampered/absent;
|
|
15
|
+
// * pointed at a DEPLOYED but DIFFERENT contract (a look-alike, a fork, an unrelated contract whose
|
|
16
|
+
// storage happens to decode) — it may answer with garbage records that get reported as truth.
|
|
17
|
+
// T-11.1 baked an immutable, ownerless `REGISTRY_ID` / `REGISTRY_VERSION` self-identification marker
|
|
18
|
+
// into the contract precisely so an off-chain verifier can authenticate the bytecode is the right
|
|
19
|
+
// interface. This module is that verifier: a single, reusable, side-effect-free preflight every read
|
|
20
|
+
// command runs FIRST, so no record/verdict is reported until the registry is authenticated.
|
|
21
|
+
//
|
|
22
|
+
// WHAT THE PREFLIGHT PROVES (and what it does NOT)
|
|
23
|
+
// It proves you are talking to a contract that (a) exists at the address, (b) self-identifies as a
|
|
24
|
+
// verifyhash ContributionRegistry of a version this build understands, and (c) — when an
|
|
25
|
+
// artifact/receipt records the chain it was anchored on — lives on THAT chain. That closes the
|
|
26
|
+
// "wrong address / wrong network / wrong contract" gap.
|
|
27
|
+
// It does NOT make the RECORDS honest beyond the contract's own immutable first-writer-wins +
|
|
28
|
+
// commit-reveal rules, and (as the contract NatSpec warns) a FORK can reuse the same REGISTRY_ID — so
|
|
29
|
+
// the marker is a POSITIVE "right interface on the right chain" signal, never a sole root of trust.
|
|
30
|
+
// See docs/TRUST-BOUNDARIES.md and the contract's "ON-CHAIN IDENTITY MARKER" notice.
|
|
31
|
+
//
|
|
32
|
+
// DISCIPLINE (mirrors verify.js's isNotAnchoredError): a genuine RPC/network error is RE-THROWN as
|
|
33
|
+
// itself, never masqueraded as an identity failure. An identity failure is only ever reported when the
|
|
34
|
+
// chain answered and the answer was wrong (no code / wrong id / wrong version / wrong chain).
|
|
35
|
+
|
|
36
|
+
const ARTIFACT = require("./core/registryArtifact");
|
|
37
|
+
const ABI = ARTIFACT.abi;
|
|
38
|
+
|
|
39
|
+
// The DOCUMENTED, frozen identity (T-11.1). Re-derived from the same preimage string the contract's
|
|
40
|
+
// NatSpec pins, so this module and the contract can never silently drift: if the preimage moved, this
|
|
41
|
+
// derived value would move with it AND the pinned digest below would mismatch in tests.
|
|
42
|
+
const REGISTRY_ID_PREIMAGE = "verifyhash.ContributionRegistry.v1";
|
|
43
|
+
// The frozen on-chain digest, pinned literally (same value test/Identity.test.js pins). We compare the
|
|
44
|
+
// contract's REGISTRY_ID against THIS, not against a value we re-derive at call time, so a tampered
|
|
45
|
+
// derivation can't move the goalpost. (deriveRegistryId() lets a test confirm the pin matches the
|
|
46
|
+
// preimage.)
|
|
47
|
+
const EXPECTED_REGISTRY_ID =
|
|
48
|
+
"0x0395e2ec987e96e51cdf619980638100236c5fc7f7c3646f8b759f3cdceb2df3";
|
|
49
|
+
|
|
50
|
+
// The maximum REGISTRY_VERSION this build understands. A registry reporting a HIGHER version made a
|
|
51
|
+
// breaking interface change we cannot safely read, so we refuse it rather than mis-decode its records.
|
|
52
|
+
const MAX_SUPPORTED_REGISTRY_VERSION = 1n;
|
|
53
|
+
|
|
54
|
+
// The "0x" / "0x0" empty-code sentinels eth_getCode returns for an address with no contract. ethers v6
|
|
55
|
+
// returns "0x" for an externally-owned (or non-existent) account.
|
|
56
|
+
const EMPTY_CODE = new Set(["0x", "0x0", ""]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Re-derive REGISTRY_ID from the documented preimage. Used by tests to confirm the pinned digest above
|
|
60
|
+
* actually equals keccak256(preimage) — kept here so the preimage lives in exactly one place.
|
|
61
|
+
* @param {object} ethersLib ethers v6 module
|
|
62
|
+
* @returns {string} the 0x 32-byte id
|
|
63
|
+
*/
|
|
64
|
+
function deriveRegistryId(ethersLib) {
|
|
65
|
+
return ethersLib.keccak256(ethersLib.toUtf8Bytes(REGISTRY_ID_PREIMAGE));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Distinguish a genuine RPC/network error (provider down, bad URL, timeout, decode-layer failure on a
|
|
70
|
+
* malformed response) from a clean negative answer the chain actually gave us. We RE-THROW genuine
|
|
71
|
+
* errors as themselves so the preflight never masquerades a network problem as an identity failure
|
|
72
|
+
* (mirroring verify.js's isNotAnchoredError discipline, in the opposite direction).
|
|
73
|
+
*
|
|
74
|
+
* A call to a method that does not exist on the target contract (e.g. REGISTRY_ID on a non-registry)
|
|
75
|
+
* reverts — ethers v6 surfaces that as a CALL_EXCEPTION / BAD_DATA (the function selector found nothing
|
|
76
|
+
* to decode), which IS a clean "the chain answered, it is not this contract" signal, NOT a network
|
|
77
|
+
* error. Everything else (NETWORK_ERROR, TIMEOUT, SERVER_ERROR, connection refused, …) is genuine.
|
|
78
|
+
*
|
|
79
|
+
* @param {any} err
|
|
80
|
+
* @returns {boolean} true iff this looks like a real network/RPC failure (re-throw), not an identity miss
|
|
81
|
+
*/
|
|
82
|
+
function isGenuineRpcError(err) {
|
|
83
|
+
if (!err) return false;
|
|
84
|
+
const code = err.code;
|
|
85
|
+
// ethers v6 network/transport failure codes — these are genuine and must be re-thrown.
|
|
86
|
+
if (
|
|
87
|
+
code === "NETWORK_ERROR" ||
|
|
88
|
+
code === "TIMEOUT" ||
|
|
89
|
+
code === "SERVER_ERROR" ||
|
|
90
|
+
code === "UNKNOWN_ERROR" ||
|
|
91
|
+
code === "UNSUPPORTED_OPERATION"
|
|
92
|
+
) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
// A reverted/empty call (the method isn't there, or returned no decodable data) is a clean negative,
|
|
96
|
+
// not a network error: CALL_EXCEPTION / BAD_DATA mean "the chain answered; that contract has no such
|
|
97
|
+
// function / returned undecodable data". Those are identity misses, handled by the caller.
|
|
98
|
+
if (code === "CALL_EXCEPTION" || code === "BAD_DATA") return false;
|
|
99
|
+
// Textual fallbacks for providers that don't set a code: connection-shaped messages are genuine.
|
|
100
|
+
const msg = String((err && err.message) || "").toLowerCase();
|
|
101
|
+
if (/econnrefused|enotfound|etimedout|socket hang up|fetch failed|network|timeout|failed to detect/.test(msg)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
// Default: treat an undecorated failure as an identity miss, NOT a network error — the caller turns
|
|
105
|
+
// it into the actionable "not a verifyhash registry" message. (A real network error almost always
|
|
106
|
+
// carries one of the codes/messages above.)
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A typed error for an authentication failure, so a caller can distinguish a registry-identity failure
|
|
112
|
+
* (the chain answered, the answer was wrong) from a re-thrown genuine RPC error.
|
|
113
|
+
*/
|
|
114
|
+
class RegistryAuthError extends Error {
|
|
115
|
+
constructor(message, detail) {
|
|
116
|
+
super(message);
|
|
117
|
+
this.name = "RegistryAuthError";
|
|
118
|
+
this.code = "REGISTRY_AUTH_FAILED";
|
|
119
|
+
if (detail) this.detail = detail;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Authenticate a deployed ContributionRegistry BEFORE any record read. Pure-ish and side-effect-free:
|
|
125
|
+
* it makes only read-only `eth_call` / `eth_getCode` / `eth_chainId` requests through the supplied
|
|
126
|
+
* provider, writes nothing, and constructs nothing persistent.
|
|
127
|
+
*
|
|
128
|
+
* Checks, in order:
|
|
129
|
+
* (a) there IS a contract at `contractAddress` (provider.getCode != "0x"); otherwise hard-error with
|
|
130
|
+
* an actionable "no contract at <addr> on this RPC — wrong address or wrong network?" message;
|
|
131
|
+
* (b) the contract self-identifies as a verifyhash registry: REGISTRY_ID equals the documented,
|
|
132
|
+
* frozen id AND REGISTRY_VERSION is present and <= the max version this build understands;
|
|
133
|
+
* otherwise hard-error "the contract at <addr> is not a verifyhash ContributionRegistry
|
|
134
|
+
* (identity check failed) — refusing to trust its records";
|
|
135
|
+
* (c) if `expectedChainId` is supplied (from a receipt/proof artifact), the provider's chainId equals
|
|
136
|
+
* it; otherwise hard-error "artifact/receipt was anchored on chainId X but this RPC is chainId Y —
|
|
137
|
+
* refusing to report a verdict against the wrong network".
|
|
138
|
+
*
|
|
139
|
+
* A genuine RPC/network error is RE-THROWN as itself (never masqueraded as an identity failure).
|
|
140
|
+
*
|
|
141
|
+
* @param {object} opts
|
|
142
|
+
* @param {object} opts.provider ethers v6 Provider (read-only)
|
|
143
|
+
* @param {string} opts.contractAddress the address to authenticate
|
|
144
|
+
* @param {number|string|bigint} [opts.expectedChainId] chain the artifact/receipt was anchored on
|
|
145
|
+
* @param {object} [opts.ethers] ethers v6 module (defaults to the bundled one)
|
|
146
|
+
* @returns {Promise<{ chainId: number, registryVersion: number, registryId: string,
|
|
147
|
+
* address: string }>} resolved identity on success
|
|
148
|
+
*/
|
|
149
|
+
async function assertRegistry(opts) {
|
|
150
|
+
const ethersLib = (opts && opts.ethers) || require("ethers");
|
|
151
|
+
const provider = opts && opts.provider;
|
|
152
|
+
const rawAddress = opts && opts.contractAddress;
|
|
153
|
+
|
|
154
|
+
if (!provider) {
|
|
155
|
+
throw new Error("assertRegistry requires a provider");
|
|
156
|
+
}
|
|
157
|
+
if (!rawAddress) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
"no contract address: pass --contract <address> or set VH_CONTRACT in the environment"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (!ethersLib.isAddress(rawAddress)) {
|
|
163
|
+
throw new Error(`invalid contract address: ${rawAddress}`);
|
|
164
|
+
}
|
|
165
|
+
const address = ethersLib.getAddress(rawAddress);
|
|
166
|
+
|
|
167
|
+
// (a) Is there a contract at all? An address with NO code answers every call with empty data, which
|
|
168
|
+
// a naive getRecord reads as "not anchored" — exactly the wrong-address/wrong-network silent failure
|
|
169
|
+
// we are closing. Check code FIRST so the message is the actionable one, not a downstream decode error.
|
|
170
|
+
let code;
|
|
171
|
+
try {
|
|
172
|
+
code = await provider.getCode(address);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (isGenuineRpcError(err)) throw err; // never masquerade a network error as an identity failure
|
|
175
|
+
// A non-network failure to even read code is still genuine-ish; surface it rather than guessing.
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
if (typeof code !== "string" || EMPTY_CODE.has(code.toLowerCase())) {
|
|
179
|
+
throw new RegistryAuthError(
|
|
180
|
+
`no contract at ${address} on this RPC — wrong address or wrong network? ` +
|
|
181
|
+
"Nothing is deployed there, so there are no records to trust. " +
|
|
182
|
+
"Check --contract / VH_CONTRACT and --rpc / VH_RPC_URL.",
|
|
183
|
+
{ reason: "no-code", address }
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const contract = new ethersLib.Contract(address, ABI, provider);
|
|
188
|
+
|
|
189
|
+
// (b) Does it self-identify as a verifyhash registry? Read REGISTRY_ID and REGISTRY_VERSION. A
|
|
190
|
+
// contract that lacks these (a non-registry) reverts the call — a CLEAN "not this contract" negative
|
|
191
|
+
// (CALL_EXCEPTION/BAD_DATA), distinct from a network error, which we re-throw.
|
|
192
|
+
let registryId;
|
|
193
|
+
let registryVersion;
|
|
194
|
+
try {
|
|
195
|
+
registryId = await contract.REGISTRY_ID();
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (isGenuineRpcError(err)) throw err;
|
|
198
|
+
throw new RegistryAuthError(
|
|
199
|
+
identityFailureMessage(address) +
|
|
200
|
+
" (it has no REGISTRY_ID() — the on-chain identity marker is absent).",
|
|
201
|
+
{ reason: "no-registry-id", address }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (String(registryId).toLowerCase() !== EXPECTED_REGISTRY_ID.toLowerCase()) {
|
|
205
|
+
throw new RegistryAuthError(
|
|
206
|
+
identityFailureMessage(address) +
|
|
207
|
+
` (REGISTRY_ID mismatch: got ${String(registryId)}, expected ${EXPECTED_REGISTRY_ID}).`,
|
|
208
|
+
{ reason: "registry-id-mismatch", address, got: String(registryId) }
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
registryVersion = await contract.REGISTRY_VERSION();
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (isGenuineRpcError(err)) throw err;
|
|
216
|
+
throw new RegistryAuthError(
|
|
217
|
+
identityFailureMessage(address) +
|
|
218
|
+
" (it has no REGISTRY_VERSION() — the on-chain identity marker is incomplete).",
|
|
219
|
+
{ reason: "no-registry-version", address }
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const versionBig = BigInt(registryVersion);
|
|
223
|
+
if (versionBig > MAX_SUPPORTED_REGISTRY_VERSION) {
|
|
224
|
+
throw new RegistryAuthError(
|
|
225
|
+
`the contract at ${address} reports REGISTRY_VERSION ${versionBig}, but this build only ` +
|
|
226
|
+
`understands up to ${MAX_SUPPORTED_REGISTRY_VERSION} — refusing to mis-read a newer ` +
|
|
227
|
+
"registry's records. Upgrade your verifyhash CLI.",
|
|
228
|
+
{ reason: "version-too-new", address, version: Number(versionBig) }
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Resolve the provider's chainId once (needed for the cross-check AND surfaced to the caller).
|
|
233
|
+
let networkChainId;
|
|
234
|
+
try {
|
|
235
|
+
const net = await provider.getNetwork();
|
|
236
|
+
networkChainId = BigInt(net.chainId);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
if (isGenuineRpcError(err)) throw err;
|
|
239
|
+
throw err; // can't read the network — genuine, surface it
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// (c) Optional chainId cross-check: an artifact/receipt that records WHERE it was anchored must be
|
|
243
|
+
// verified against THE SAME chain, or the on-chain checks are meaningless (a root anchored on chain X
|
|
244
|
+
// says nothing about chain Y). This is the portability promise made trustworthy: the consumer no
|
|
245
|
+
// longer has to trust the prover's RPC blindly.
|
|
246
|
+
if (opts.expectedChainId !== undefined && opts.expectedChainId !== null) {
|
|
247
|
+
const want = BigInt(opts.expectedChainId);
|
|
248
|
+
if (want !== networkChainId) {
|
|
249
|
+
throw new RegistryAuthError(
|
|
250
|
+
`artifact/receipt was anchored on chainId ${want} but this RPC is chainId ${networkChainId} ` +
|
|
251
|
+
"— refusing to report a verdict against the wrong network. " +
|
|
252
|
+
"Point --rpc / VH_RPC_URL at the chain the artifact was anchored on.",
|
|
253
|
+
{ reason: "chainid-mismatch", expected: Number(want), actual: Number(networkChainId) }
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
chainId: Number(networkChainId),
|
|
260
|
+
registryVersion: Number(versionBig),
|
|
261
|
+
registryId: String(registryId).toLowerCase(),
|
|
262
|
+
address,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The shared identity-failure lead sentence, reused for every (b)-class failure so callers/tests see
|
|
267
|
+
* one consistent, actionable message. */
|
|
268
|
+
function identityFailureMessage(address) {
|
|
269
|
+
return (
|
|
270
|
+
`the contract at ${address} is not a verifyhash ContributionRegistry ` +
|
|
271
|
+
"(identity check failed) — refusing to trust its records"
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Format the one-line "registry authenticated" human confirmation a read command prints so a user can
|
|
277
|
+
* SEE the check ran. Kept in one place so every command's confirmation reads identically.
|
|
278
|
+
* @param {{registryVersion:number, chainId:number}} auth the assertRegistry result
|
|
279
|
+
* @returns {string}
|
|
280
|
+
*/
|
|
281
|
+
function formatRegistryLine(auth) {
|
|
282
|
+
return ` registry authenticated: REGISTRY_ID ok (v${auth.registryVersion}), chainId ${auth.chainId}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The loud one-liner printed when the preflight is SKIPPED via --skip-identity-check. It must make
|
|
287
|
+
* unmistakably clear the verdict is only as trustworthy as the RPC the caller pointed at.
|
|
288
|
+
* @returns {string}
|
|
289
|
+
*/
|
|
290
|
+
function formatSkippedLine() {
|
|
291
|
+
return (
|
|
292
|
+
" registry authentication: SKIPPED (--skip-identity-check). " +
|
|
293
|
+
"The verdict is only as trustworthy as the RPC/address you supplied — the contract was NOT " +
|
|
294
|
+
"confirmed to be a verifyhash registry on the expected network."
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Build the machine-readable `registry` block for `--json` output on success.
|
|
300
|
+
* @param {{registryId:string, registryVersion:number, chainId:number}} auth
|
|
301
|
+
* @returns {{id:string, version:number, chainId:number}}
|
|
302
|
+
*/
|
|
303
|
+
function jsonRegistryBlock(auth) {
|
|
304
|
+
return { id: auth.registryId, version: auth.registryVersion, chainId: auth.chainId };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The `registry` block for `--json` when the check was skipped. */
|
|
308
|
+
function jsonSkippedBlock() {
|
|
309
|
+
return {
|
|
310
|
+
skipped: true,
|
|
311
|
+
note:
|
|
312
|
+
"registry authentication SKIPPED (--skip-identity-check): the contract was NOT confirmed to be " +
|
|
313
|
+
"a verifyhash registry on the expected network. The verdict is only as trustworthy as the RPC.",
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = {
|
|
318
|
+
assertRegistry,
|
|
319
|
+
RegistryAuthError,
|
|
320
|
+
isGenuineRpcError,
|
|
321
|
+
deriveRegistryId,
|
|
322
|
+
identityFailureMessage,
|
|
323
|
+
formatRegistryLine,
|
|
324
|
+
formatSkippedLine,
|
|
325
|
+
jsonRegistryBlock,
|
|
326
|
+
jsonSkippedBlock,
|
|
327
|
+
REGISTRY_ID_PREIMAGE,
|
|
328
|
+
EXPECTED_REGISTRY_ID,
|
|
329
|
+
MAX_SUPPORTED_REGISTRY_VERSION,
|
|
330
|
+
ABI,
|
|
331
|
+
};
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// `vh reputation <addr> [--contract a] [--rpc u] [--json] [--skip-identity-check]` — a read-only,
|
|
4
|
+
// verifiable CONTRIBUTION SCORE for one contributor (T-12.2).
|
|
5
|
+
//
|
|
6
|
+
// WHAT THIS IS
|
|
7
|
+
// A TRANSPARENT, DOCUMENTED aggregate over the records the registry holds under one address. It is
|
|
8
|
+
// purely DERIVED from on-chain state and re-derivable by anyone with the same RPC: total records,
|
|
9
|
+
// the authorBound (proven first claimant / commit-reveal) vs anchor-only (front-runnable "first
|
|
10
|
+
// anchorer") breakdown, the lineage-root (parent == 0x0) vs revision (parent != 0x0) breakdown, and
|
|
11
|
+
// the earliest/latest blockNumber + timestamp seen.
|
|
12
|
+
//
|
|
13
|
+
// WHAT THIS IS NOT (the trust posture — see docs/TRUST-BOUNDARIES.md, mirrored from list.js/verify.js)
|
|
14
|
+
// * It is NOT A TOKEN and NOT TRANSFERABLE. It holds no value, grants no rights; it is a view.
|
|
15
|
+
// * A score is only as meaningful as the `authorBound` bar. An anchor-only count is EXPLICITLY
|
|
16
|
+
// WEAKER, because a plain `anchor()` is front-runnable: anyone who saw a contentHash could have
|
|
17
|
+
// anchored it, so an anchor-only record proves order-of-anchoring, never authorship. The breakdown
|
|
18
|
+
// therefore reports authorBound and anchor-only SEPARATELY and NEVER collapses them into one number
|
|
19
|
+
// that would hide the difference.
|
|
20
|
+
// * It does NOT validate the CONTENT of any record. "This address has N records" says nothing about
|
|
21
|
+
// whether those records correspond to real, untampered bytes — re-derive the content hash and run
|
|
22
|
+
// `vh verify` for that.
|
|
23
|
+
// * Any tradeable/reputation-TOKEN layer is a separate, human-gated decision (D-2 / P-1 in
|
|
24
|
+
// STRATEGY.md) and is NOT built here.
|
|
25
|
+
//
|
|
26
|
+
// READ-ONLY BY CONSTRUCTION: takes a PROVIDER only, never a signer and never a key. It runs the
|
|
27
|
+
// EPIC-11 `assertRegistry` preflight FIRST (reused from cli/registry.js) so the score is never reported
|
|
28
|
+
// against an unauthenticated/look-alike contract, then pages through the contract's
|
|
29
|
+
// `getRecordsByContributor` clamped view (the T-12.1 per-contributor index).
|
|
30
|
+
|
|
31
|
+
const ARTIFACT = require("./core/registryArtifact");
|
|
32
|
+
const ABI = ARTIFACT.abi;
|
|
33
|
+
|
|
34
|
+
// Reuse the lineage-root predicate so reputation, show and list never disagree about what a "root"
|
|
35
|
+
// (parent == bytes32(0)) is. T-10.1.
|
|
36
|
+
const { isRoot } = require("./show");
|
|
37
|
+
const { isoFromUnix, ATTRIBUTION_PROVEN, ATTRIBUTION_ANCHOR_ONLY } = require("./list");
|
|
38
|
+
const {
|
|
39
|
+
assertRegistry,
|
|
40
|
+
formatRegistryLine,
|
|
41
|
+
formatSkippedLine,
|
|
42
|
+
jsonRegistryBlock,
|
|
43
|
+
jsonSkippedBlock,
|
|
44
|
+
} = require("./registry");
|
|
45
|
+
|
|
46
|
+
// Default page size for walking getRecordsByContributor(). The contract clamps a window to what that
|
|
47
|
+
// contributor actually owns, so this is purely a request-batching knob; it never affects which records
|
|
48
|
+
// come back. Mirrors list.js's DEFAULT_PAGE.
|
|
49
|
+
const DEFAULT_PAGE = 100;
|
|
50
|
+
|
|
51
|
+
// The trust caveat that LEADS every human-readable run. It is the load-bearing part of this command:
|
|
52
|
+
// a reader must see, before any number, that the score is only as meaningful as the authorBound bar,
|
|
53
|
+
// that it does not validate content, and that it is not a token. Wording is kept consistent with
|
|
54
|
+
// cli/list.js's TRUST_CAVEAT and cli/verify.js's attribution language so the read commands never drift.
|
|
55
|
+
const TRUST_CAVEAT = [
|
|
56
|
+
"NOTE: this score is a TRANSPARENT, on-chain-DERIVED aggregate — NOT a reputation token, NOT",
|
|
57
|
+
"transferable, and re-derivable by anyone from the same registry. It is only as meaningful as the",
|
|
58
|
+
"authorBound bar: authorBound records are proven first claimants (commit-reveal); anchor-only records",
|
|
59
|
+
"are merely the FIRST ANCHORER and are WEAKER, because a plain anchor() is front-runnable (anyone who",
|
|
60
|
+
"saw the contentHash could have anchored it). The two are reported SEPARATELY and never collapsed into",
|
|
61
|
+
"one number. The score does NOT validate the CONTENT of any record — re-derive the hash and run",
|
|
62
|
+
"`vh verify` for that. Any tradeable/reputation-token layer is gated on a human decision (D-2 / P-1).",
|
|
63
|
+
].join("\n");
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Page through ONE contributor's records using the clamped getRecordsByContributor read (T-12.1).
|
|
67
|
+
* Read-only: uses only the provider. Walks fixed-size pages and stops on a short/empty page, exactly
|
|
68
|
+
* as the contract's NatSpec documents (no need to know the count up front, never a boundary revert).
|
|
69
|
+
* Returns normalized record objects in that contributor's own insertion order.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} contract an ethers v6 Contract bound to a provider
|
|
72
|
+
* @param {string} contributor the 20-byte address (already validated/checksummed by the caller)
|
|
73
|
+
* @param {number} [pageSize]
|
|
74
|
+
* @returns {Promise<Array<{
|
|
75
|
+
* contentHash:string, contributor:string, authorBound:boolean,
|
|
76
|
+
* timestamp:bigint, blockNumber:bigint, uri:string, parent:string
|
|
77
|
+
* }>>}
|
|
78
|
+
*/
|
|
79
|
+
async function readContributorRecords(contract, contributor, pageSize = DEFAULT_PAGE) {
|
|
80
|
+
const out = [];
|
|
81
|
+
for (let start = 0; ; start += pageSize) {
|
|
82
|
+
const [contentHashes, records] = await contract.getRecordsByContributor(
|
|
83
|
+
contributor,
|
|
84
|
+
start,
|
|
85
|
+
pageSize
|
|
86
|
+
);
|
|
87
|
+
// The contract clamps to what this contributor owns: a window at/past the end returns empty arrays
|
|
88
|
+
// (never a revert), so an empty/short page is the stop signal.
|
|
89
|
+
if (contentHashes.length === 0) break;
|
|
90
|
+
for (let i = 0; i < contentHashes.length; i++) {
|
|
91
|
+
const r = records[i];
|
|
92
|
+
out.push({
|
|
93
|
+
contentHash: contentHashes[i],
|
|
94
|
+
contributor: r.contributor,
|
|
95
|
+
authorBound: Boolean(r.authorBound),
|
|
96
|
+
timestamp: BigInt(r.timestamp),
|
|
97
|
+
blockNumber: BigInt(r.blockNumber),
|
|
98
|
+
uri: r.uri,
|
|
99
|
+
// The immutable lineage edge (T-10.1). Normalized to a lowercase 0x string; a root reads back
|
|
100
|
+
// as the 32-byte zero hash (isRoot() flags it below).
|
|
101
|
+
parent: String(r.parent).toLowerCase(),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// A short page (fewer than requested) also means we reached the end; stop without an extra read.
|
|
105
|
+
if (contentHashes.length < pageSize) break;
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Compute the TRANSPARENT aggregate score over a contributor's normalized records. Pure: no I/O, fully
|
|
112
|
+
* re-derivable from the same input. Returns the documented breakdowns and block/time bounds. The
|
|
113
|
+
* authorBound and anchor-only counts are kept SEPARATE (never summed into a single opaque number).
|
|
114
|
+
*
|
|
115
|
+
* @param {Array} records normalized records from readContributorRecords (any order)
|
|
116
|
+
* @returns {{
|
|
117
|
+
* total:number,
|
|
118
|
+
* authorBound:number, anchorOnly:number,
|
|
119
|
+
* lineageRoots:number, revisions:number,
|
|
120
|
+
* earliest:{blockNumber:bigint, timestamp:bigint}|null,
|
|
121
|
+
* latest:{blockNumber:bigint, timestamp:bigint}|null
|
|
122
|
+
* }}
|
|
123
|
+
*/
|
|
124
|
+
function computeScore(records) {
|
|
125
|
+
let authorBound = 0;
|
|
126
|
+
let anchorOnly = 0;
|
|
127
|
+
let lineageRoots = 0;
|
|
128
|
+
let revisions = 0;
|
|
129
|
+
let earliest = null; // by blockNumber (on-chain ordering), tie-broken by timestamp
|
|
130
|
+
let latest = null;
|
|
131
|
+
|
|
132
|
+
for (const r of records) {
|
|
133
|
+
if (r.authorBound) authorBound++;
|
|
134
|
+
else anchorOnly++;
|
|
135
|
+
|
|
136
|
+
if (isRoot(r.parent)) lineageRoots++;
|
|
137
|
+
else revisions++;
|
|
138
|
+
|
|
139
|
+
const point = { blockNumber: r.blockNumber, timestamp: r.timestamp };
|
|
140
|
+
if (earliest === null || r.blockNumber < earliest.blockNumber) earliest = point;
|
|
141
|
+
if (latest === null || r.blockNumber > latest.blockNumber) latest = point;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
total: records.length,
|
|
146
|
+
authorBound,
|
|
147
|
+
anchorOnly,
|
|
148
|
+
lineageRoots,
|
|
149
|
+
revisions,
|
|
150
|
+
earliest,
|
|
151
|
+
latest,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Shape the score + bounds for `--json`: BigInts become Numbers (unix seconds / block heights fit
|
|
157
|
+
* safely), block/time bounds carry an ISO string, and a zero-record address serializes with total: 0
|
|
158
|
+
* and null bounds (distinguishable from an error). The breakdowns stay separate fields.
|
|
159
|
+
*/
|
|
160
|
+
function jsonScore(addr, score, registryBlock) {
|
|
161
|
+
const bound = (b) =>
|
|
162
|
+
b === null
|
|
163
|
+
? null
|
|
164
|
+
: {
|
|
165
|
+
blockNumber: Number(b.blockNumber),
|
|
166
|
+
timestamp: Number(b.timestamp),
|
|
167
|
+
timestampISO: isoFromUnix(b.timestamp),
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
address: addr,
|
|
171
|
+
registry: registryBlock,
|
|
172
|
+
total: score.total,
|
|
173
|
+
// Reported SEPARATELY and never collapsed: a consumer that wants a single number must decide how to
|
|
174
|
+
// weight a front-runnable anchor-only record itself.
|
|
175
|
+
authorBound: score.authorBound,
|
|
176
|
+
anchorOnly: score.anchorOnly,
|
|
177
|
+
attribution: {
|
|
178
|
+
authorBound: ATTRIBUTION_PROVEN,
|
|
179
|
+
anchorOnly: ATTRIBUTION_ANCHOR_ONLY,
|
|
180
|
+
},
|
|
181
|
+
lineageRoots: score.lineageRoots,
|
|
182
|
+
revisions: score.revisions,
|
|
183
|
+
earliest: bound(score.earliest),
|
|
184
|
+
latest: bound(score.latest),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Render the score as the human-readable block the CLI prints (after the trust caveat + registry line). */
|
|
189
|
+
function formatScore(addr, score, opts = {}) {
|
|
190
|
+
const lines = [];
|
|
191
|
+
lines.push(` contributor: ${addr}`);
|
|
192
|
+
if (opts.identitySkipped) {
|
|
193
|
+
lines.push(formatSkippedLine());
|
|
194
|
+
} else if (opts.registryAuth) {
|
|
195
|
+
lines.push(formatRegistryLine(opts.registryAuth));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (score.total === 0) {
|
|
199
|
+
lines.push(" no contributions: this address has no records in the registry.");
|
|
200
|
+
return lines.join("\n");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
lines.push(` total records: ${score.total}`);
|
|
204
|
+
lines.push(" attribution breakdown (reported SEPARATELY — never summed):");
|
|
205
|
+
lines.push(` authorBound: ${score.authorBound} (${ATTRIBUTION_PROVEN})`);
|
|
206
|
+
lines.push(` anchor-only: ${score.anchorOnly} (${ATTRIBUTION_ANCHOR_ONLY})`);
|
|
207
|
+
lines.push(" lineage breakdown:");
|
|
208
|
+
lines.push(` lineage roots: ${score.lineageRoots} (parent == 0x0)`);
|
|
209
|
+
lines.push(` revisions: ${score.revisions} (parent != 0x0 — a CLAIMED predecessor edge)`);
|
|
210
|
+
const e = score.earliest;
|
|
211
|
+
const l = score.latest;
|
|
212
|
+
lines.push(
|
|
213
|
+
` earliest: block ${e.blockNumber}, ts ${e.timestamp} (${isoFromUnix(e.timestamp)})`
|
|
214
|
+
);
|
|
215
|
+
lines.push(
|
|
216
|
+
` latest: block ${l.blockNumber}, ts ${l.timestamp} (${isoFromUnix(l.timestamp)})`
|
|
217
|
+
);
|
|
218
|
+
return lines.join("\n");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Compute and report a contributor's verifiable contribution score, read-only. Validates `<addr>` is a
|
|
223
|
+
* 20-byte hex address BEFORE any network call, runs the assertRegistry preflight, pages through
|
|
224
|
+
* getRecordsByContributor, then emits either a human block (led by the trust caveat) or a
|
|
225
|
+
* machine-readable JSON object (`--json`). Requires a provider; NEVER a signer or key.
|
|
226
|
+
*
|
|
227
|
+
* @param {object} opts
|
|
228
|
+
* @param {string} opts.address the contributor address to score
|
|
229
|
+
* @param {string} opts.contractAddress deployed ContributionRegistry address to read from
|
|
230
|
+
* @param {object} opts.provider ethers v6 Provider (read-only RPC connection)
|
|
231
|
+
* @param {boolean} [opts.json] emit a JSON object instead of the human block
|
|
232
|
+
* @param {boolean} [opts.skipIdentityCheck] loudly skip the registry preflight (KNOWN local target only)
|
|
233
|
+
* @param {object} [opts.ethers] ethers v6 module (defaults to the bundled one)
|
|
234
|
+
* @param {(s:string)=>void} [opts.log] sink for output (defaults to process.stdout)
|
|
235
|
+
* @returns {Promise<{ address:string, total:number, authorBound:number, anchorOnly:number,
|
|
236
|
+
* lineageRoots:number, revisions:number, earliest:object|null, latest:object|null,
|
|
237
|
+
* registry:object|null, json:boolean }>}
|
|
238
|
+
*/
|
|
239
|
+
async function runReputation(opts) {
|
|
240
|
+
const ethersLib = opts.ethers || require("ethers");
|
|
241
|
+
const log = opts.log || ((s) => process.stdout.write(s));
|
|
242
|
+
|
|
243
|
+
const { address: rawAddress, contractAddress, provider } = opts;
|
|
244
|
+
|
|
245
|
+
// Validate the address shape FIRST — before touching env, the provider, or the network. A malformed
|
|
246
|
+
// value must hard-error with a usage-grade message and never hit the network (parser parity with the
|
|
247
|
+
// existing read commands; mirrors show.js's normalizeContentHash precedence).
|
|
248
|
+
if (!rawAddress) throw new Error("reputation requires an <addr>");
|
|
249
|
+
if (!ethersLib.isAddress(rawAddress)) {
|
|
250
|
+
throw new Error(`invalid address: ${rawAddress} (expected a 20-byte 0x-hex address)`);
|
|
251
|
+
}
|
|
252
|
+
const address = ethersLib.getAddress(rawAddress);
|
|
253
|
+
|
|
254
|
+
if (!contractAddress) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
"no contract address: pass --contract <address> or set VH_CONTRACT in the environment"
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
if (!ethersLib.isAddress(contractAddress)) {
|
|
260
|
+
throw new Error(`invalid contract address: ${contractAddress}`);
|
|
261
|
+
}
|
|
262
|
+
if (!provider) {
|
|
263
|
+
throw new Error("no provider: pass --rpc <url> or set VH_RPC_URL / AMOY_RPC_URL");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// T-11.2 preflight: authenticate the registry BEFORE reading any record — the score is never reported
|
|
267
|
+
// against an unauthenticated/look-alike contract (unless the caller explicitly, loudly opts out with
|
|
268
|
+
// skipIdentityCheck for a KNOWN not-yet-deployed/local-dev target).
|
|
269
|
+
let registryAuth = null;
|
|
270
|
+
if (!opts.skipIdentityCheck) {
|
|
271
|
+
registryAuth = await assertRegistry({ provider, contractAddress, ethers: ethersLib });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const contract = new ethersLib.Contract(
|
|
275
|
+
ethersLib.getAddress(contractAddress),
|
|
276
|
+
ABI,
|
|
277
|
+
provider
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const records = await readContributorRecords(contract, address);
|
|
281
|
+
const score = computeScore(records);
|
|
282
|
+
|
|
283
|
+
// T-11.2: the machine-readable registry block — the same identity a UI/indexer can depend on to know
|
|
284
|
+
// the score was derived from an authenticated registry (or that the check was skipped).
|
|
285
|
+
const registryBlock = opts.skipIdentityCheck
|
|
286
|
+
? jsonSkippedBlock()
|
|
287
|
+
: registryAuth
|
|
288
|
+
? jsonRegistryBlock(registryAuth)
|
|
289
|
+
: null;
|
|
290
|
+
|
|
291
|
+
if (opts.json) {
|
|
292
|
+
log(JSON.stringify(jsonScore(address, score, registryBlock), null, 2) + "\n");
|
|
293
|
+
} else {
|
|
294
|
+
// Human-readable: LEAD with the trust caveat, then the score block (which carries the registry-auth
|
|
295
|
+
// confirmation or the loud skip warning, then the breakdowns / bounds).
|
|
296
|
+
log(TRUST_CAVEAT + "\n\n");
|
|
297
|
+
log(
|
|
298
|
+
formatScore(address, score, {
|
|
299
|
+
registryAuth,
|
|
300
|
+
identitySkipped: Boolean(opts.skipIdentityCheck),
|
|
301
|
+
}) + "\n"
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
address,
|
|
307
|
+
total: score.total,
|
|
308
|
+
authorBound: score.authorBound,
|
|
309
|
+
anchorOnly: score.anchorOnly,
|
|
310
|
+
lineageRoots: score.lineageRoots,
|
|
311
|
+
revisions: score.revisions,
|
|
312
|
+
// BigInts normalized to JSON-safe numbers in the structured result too, so a programmatic caller
|
|
313
|
+
// gets the same shape as --json.
|
|
314
|
+
earliest:
|
|
315
|
+
score.earliest === null
|
|
316
|
+
? null
|
|
317
|
+
: {
|
|
318
|
+
blockNumber: Number(score.earliest.blockNumber),
|
|
319
|
+
timestamp: Number(score.earliest.timestamp),
|
|
320
|
+
timestampISO: isoFromUnix(score.earliest.timestamp),
|
|
321
|
+
},
|
|
322
|
+
latest:
|
|
323
|
+
score.latest === null
|
|
324
|
+
? null
|
|
325
|
+
: {
|
|
326
|
+
blockNumber: Number(score.latest.blockNumber),
|
|
327
|
+
timestamp: Number(score.latest.timestamp),
|
|
328
|
+
timestampISO: isoFromUnix(score.latest.timestamp),
|
|
329
|
+
},
|
|
330
|
+
registry: registryBlock,
|
|
331
|
+
json: Boolean(opts.json),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = {
|
|
336
|
+
runReputation,
|
|
337
|
+
readContributorRecords,
|
|
338
|
+
computeScore,
|
|
339
|
+
jsonScore,
|
|
340
|
+
formatScore,
|
|
341
|
+
TRUST_CAVEAT,
|
|
342
|
+
DEFAULT_PAGE,
|
|
343
|
+
ABI,
|
|
344
|
+
};
|