@xfuel/verify 0.1.1 → 0.3.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 (54) hide show
  1. package/README.md +62 -17
  2. package/dist/anchor-witness.d.ts +138 -0
  3. package/dist/anchor-witness.d.ts.map +1 -0
  4. package/dist/anchor-witness.js +365 -0
  5. package/dist/anchor-witness.js.map +1 -0
  6. package/dist/base-payer.d.ts +8 -1
  7. package/dist/base-payer.d.ts.map +1 -1
  8. package/dist/base-payer.js +64 -20
  9. package/dist/base-payer.js.map +1 -1
  10. package/dist/canonical-preimage.d.ts +9 -0
  11. package/dist/canonical-preimage.d.ts.map +1 -0
  12. package/dist/canonical-preimage.js +25 -0
  13. package/dist/canonical-preimage.js.map +1 -0
  14. package/dist/cli.d.ts +1 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +424 -25
  17. package/dist/cli.js.map +1 -1
  18. package/dist/head-binding.d.ts +24 -0
  19. package/dist/head-binding.d.ts.map +1 -0
  20. package/dist/head-binding.js +61 -0
  21. package/dist/head-binding.js.map +1 -0
  22. package/dist/index.d.ts +263 -48
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +919 -140
  25. package/dist/index.js.map +1 -1
  26. package/dist/issuer-history.d.ts +91 -0
  27. package/dist/issuer-history.d.ts.map +1 -0
  28. package/dist/issuer-history.js +292 -0
  29. package/dist/issuer-history.js.map +1 -0
  30. package/dist/jcs.d.ts +8 -0
  31. package/dist/jcs.d.ts.map +1 -0
  32. package/dist/jcs.js +64 -0
  33. package/dist/jcs.js.map +1 -0
  34. package/dist/jws.d.ts +23 -0
  35. package/dist/jws.d.ts.map +1 -1
  36. package/dist/jws.js +71 -1
  37. package/dist/jws.js.map +1 -1
  38. package/dist/payer.d.ts +16 -5
  39. package/dist/payer.d.ts.map +1 -1
  40. package/dist/payer.js +38 -26
  41. package/dist/payer.js.map +1 -1
  42. package/dist/preimage.d.ts +49 -0
  43. package/dist/preimage.d.ts.map +1 -0
  44. package/dist/preimage.js +203 -0
  45. package/dist/preimage.js.map +1 -0
  46. package/dist/receipt-lane.d.ts +234 -0
  47. package/dist/receipt-lane.d.ts.map +1 -0
  48. package/dist/receipt-lane.js +443 -0
  49. package/dist/receipt-lane.js.map +1 -0
  50. package/dist/refusal.d.ts +90 -0
  51. package/dist/refusal.d.ts.map +1 -0
  52. package/dist/refusal.js +226 -0
  53. package/dist/refusal.js.map +1 -0
  54. package/package.json +9 -5
package/README.md CHANGED
@@ -25,6 +25,7 @@ console.log(result.matches); // true if binding verified
25
25
  // Full verification including on-chain nullifier check
26
26
  const fullResult = await verifyReceipt(receipt, { checkNullifier: true });
27
27
  console.log(fullResult.overall); // 'verified' | 'partial' | 'failed'
28
+ console.log(fullResult.receipt_lane.freeze); // unsigned; does not change overall
28
29
  ```
29
30
 
30
31
  ### CLI
@@ -33,6 +34,15 @@ console.log(fullResult.overall); // 'verified' | 'partial' | 'failed'
33
34
  # Local binding verification (no network required)
34
35
  npx xfuel-verify receipt.json
35
36
 
37
+ # Hash the canonical object (must match signed payload_hash) and read not_after
38
+ # from a pinned issuer-history snapshot. Both flags are offline.
39
+ npx xfuel-verify receipt.json --canonical-preimage preimage.json --issuer-history-file issuer-history.json
40
+
41
+ # A signed refusal (schema chit402.refusal.v1) is not a payment.
42
+ # The check proves the issuer refused, at the signed anchor, for that code.
43
+ # It does not prove a payment or that the block still stands.
44
+ npx xfuel-verify refusal.json
45
+
36
46
  # With on-chain nullifier check (requires network)
37
47
  npx xfuel-verify receipt.json --check-nullifier
38
48
 
@@ -40,7 +50,10 @@ npx xfuel-verify receipt.json --check-nullifier
40
50
  npx xfuel-verify receipt.json --json
41
51
 
42
52
  # From stdin
43
- curl -s https://api.xfuel.app/receipt/task-123?format=json | npx xfuel-verify -
53
+ curl -s https://api.chit402.com/receipt/task-123?format=json | npx xfuel-verify -
54
+
55
+ # Dual anchor: inclusion, Solana memo, Base calldata
56
+ npx xfuel-verify receipt.json inclusion.json head.json --rpc
44
57
  ```
45
58
 
46
59
  ## What This Verifies
@@ -48,42 +61,70 @@ curl -s https://api.xfuel.app/receipt/task-123?format=json | npx xfuel-verify -
48
61
  | Check | Requires Network? | Description |
49
62
  |-------|-------------------|-------------|
50
63
  | Payment binding | No | Recompute commitment from receipt fields |
51
- | Issuer signature | No | ES256 verification — pinned `issuer_jwk` on receipt, or JWKS file |
64
+ | Issuer signature | No, unless `--fetch-jwks` | ES256. Trusted only via JWKS (by kid) or a pinned RFC 7638 kid |
52
65
  | Output hash | No | Hash is on the receipt |
53
66
  | On-chain settlement | Yes | Query Base RPC for tx |
54
67
  | Nullifier anchor | Yes | Query ZKVerifierSP1 contract |
68
+ | Anchored receipt root | Yes, with `--rpc` | Inclusion proof, then Solana memo and Base calldata for that root |
69
+ | Canonical preimage | No | SHA-256 of `--canonical-preimage`, or the stored canonical object, matches signed `payload_hash` |
70
+ | Issuer history | No, with `--issuer-history-file` | `iat` is inside the kid's `not_before` / `not_after`. A payload v10 pin must match the snapshot hash |
55
71
 
56
72
  ## Issuer Signature Verification (ES256)
57
73
 
58
- Receipts include an `issuer_signature` signed with ES256 (P-256). Verify offline:
74
+ Receipts include `issuer_signature.jws` (compact ES256 / P-256). The signature
75
+ counts only when the verifying key is trusted:
59
76
 
60
- ### Pin-first (receipts with `issuer_signature.issuer_jwk`)
77
+ 1. **JWKS by kid** — a key in `--jwks-file`, `--jwks-url`, or `--fetch-jwks`
78
+ (allowlisted host, default `api.chit402.com`) whose `kid` matches the JWS.
79
+ 2. **Pinned thumbprint** — the embedded `issuer_jwk` is used only when its
80
+ RFC 7638 thumbprint equals a trusted kid. The default offline pin is the
81
+ production kid `IvFpmC-vPhkY_v0vidsrWVT9uzlE5XWKZgAEOeJTq1Q`. Override with
82
+ `--trusted-kid`, or disable with `--no-trusted-kid`.
61
83
 
62
- Newer receipts pin the issuer public key directly in the receipt. No JWKS file needed:
84
+ `issuer_jwk` on the receipt is not a trust root. A copy re-signed with an
85
+ arbitrary P-256 key reports `key untrusted` (`issuer_signature.valid === false`),
86
+ including when `--jwks-file` points at the real JWKS.
63
87
 
64
88
  ```bash
65
- # Offline verify — uses pinned issuer_jwk from the receipt
89
+ # Offline: default production pin, no network
66
90
  npx xfuel-verify receipt.json
91
+
92
+ # Recompute published preimages and check the kid window.
93
+ # A payload v10 receipt pins issuer_history. Hash the stored canonical object:
94
+ curl -sS "https://api.chit402.com/receipt/RECEIPT_ID?format=json" -o receipt.json
95
+ curl -sS "https://api.chit402.com/receipt/RECEIPT_ID/preimage" -o preimage.json
96
+ curl -sS "https://api.chit402.com/.well-known/issuer-history.json?version=1" -o issuer-history.json
97
+ npx xfuel-verify receipt.json --canonical-preimage preimage.json --issuer-history-file issuer-history.json
98
+
99
+ # Trust the published JWKS instead of (or in addition to) the pin
100
+ curl -o issuer-jwks.json https://api.chit402.com/.well-known/jwks.json
101
+ npx xfuel-verify receipt.json --jwks-file issuer-jwks.json --no-trusted-kid
102
+
103
+ # Fetch the issuer JWKS (https://api.chit402.com/.well-known/jwks.json)
104
+ npx xfuel-verify receipt.json --fetch-jwks
67
105
  ```
68
106
 
69
107
  ```typescript
70
108
  import { verifyReceipt } from '@xfuel/verify';
71
109
 
72
- const result = await verifyReceipt(receipt); // no jwks option required
73
- console.log(result.issuer_signature.valid); // true when signature intact
110
+ const result = await verifyReceipt(receipt);
111
+ console.log(result.issuer_signature.valid); // true only for a trusted key
112
+ console.log(result.issuer_signature.key_trusted);
113
+ console.log(result.amount_usdc); // from verified claims, not the outer copy
114
+ console.log(result.claim_mismatches); // outer payment / caller_binding vs JWS
74
115
  ```
75
116
 
76
- ### Legacy JWKS file (older receipts without pin)
117
+ Amount, payer, payee, asset, and tx are read from the verified JWS claims.
118
+ If the unsigned outer `payment` or `caller_binding` disagrees, verification
119
+ fails. `--check-payer` on Base confirms payer, payee, asset, and amount in the
120
+ USDC `Transfer` log.
77
121
 
78
- ```bash
79
- # Download JWKS once (or obtain from trusted source)
80
- curl -o issuer-jwks.json https://api.chit402.com/.well-known/jwks.json
81
-
82
- # Verify receipt with JWKS file (no network during verification)
83
- npx xfuel-verify receipt.json --jwks-file issuer-jwks.json
84
- ```
122
+ A paid USDC receipt whose signed `binding.expected_commitment` is null is
123
+ reported as “No payment-binding commitment”, not as an unmetered or TFUEL receipt.
85
124
 
86
- The CLI does **not** automatically fetch JWKS to ensure offline verification. For pinned receipts, `--jwks-file` is optional. Exit code 1 (failed) is returned if the signature is invalid or tampered.
125
+ The CLI does **not** fetch JWKS unless `--fetch-jwks` or `--jwks-url` is set.
126
+ Exit code 1 is returned when the key is untrusted, the signature is invalid, or
127
+ a signed claim disagrees with the outer copy.
87
128
 
88
129
  ## Frozen Fields
89
130
 
@@ -181,6 +222,10 @@ Full verification combining binding and optional nullifier check.
181
222
  }
182
223
  ```
183
224
 
225
+ ## Build digest
226
+
227
+ `npm run digest` writes `BUILD_DIGEST.txt`. That file is the SHA-256 of the sorted `src/**/*.ts` listing, not a bit-reproducible `tsc` binary. The same sources produce the same digest. The receipt-tree genesis leaf copies this digest so a holder can see which verifier source the issuer vouches for. It does not prove the process that served the receipt ran that exact binary.
228
+
184
229
  ## License
185
230
 
186
231
  Apache-2.0
@@ -0,0 +1,138 @@
1
+ export declare const MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
2
+ export declare const MEMO_PROGRAM_ID_V1 = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo";
3
+ /** Genesis hashes from the Solana cluster CAIP-2 references. */
4
+ export declare const SOLANA_GENESIS: Record<string, string>;
5
+ export declare const BASE_MAINNET_CHAIN_ID = 8453;
6
+ export declare const ANCHOR_PROVES: string[];
7
+ export declare const ANCHOR_DOES_NOT_PROVE: string[];
8
+ export interface InclusionStep {
9
+ hash: string;
10
+ position: string;
11
+ }
12
+ export interface AnchorReceipt {
13
+ task_id?: string;
14
+ row_hash?: string | null;
15
+ book_chain?: {
16
+ row_hash?: string | null;
17
+ } | null;
18
+ }
19
+ export interface AnchorInclusion {
20
+ task_id?: string;
21
+ leaf_index: number;
22
+ tree_size: number;
23
+ root: string;
24
+ leaf?: string;
25
+ row_hash?: string | null;
26
+ proof: InclusionStep[];
27
+ }
28
+ export interface AnchorHead {
29
+ root: string;
30
+ tree_size?: number;
31
+ anchor_tx?: string | null;
32
+ anchor?: {
33
+ tx?: string | null;
34
+ calldata?: string | null;
35
+ chain_id?: number | null;
36
+ } | null;
37
+ anchors?: {
38
+ base?: {
39
+ tx?: string | null;
40
+ calldata?: string | null;
41
+ chain_id?: number | null;
42
+ status?: string;
43
+ } | null;
44
+ solana?: {
45
+ signature?: string | null;
46
+ slot?: number | null;
47
+ cluster?: string | null;
48
+ memo?: string | null;
49
+ status?: string | null;
50
+ } | null;
51
+ } | null;
52
+ }
53
+ interface SolanaIx {
54
+ program?: string;
55
+ programId?: string;
56
+ parsed?: string | {
57
+ memo?: string;
58
+ info?: string;
59
+ };
60
+ }
61
+ export interface SolanaAnchorTx {
62
+ slot?: number;
63
+ meta?: {
64
+ err?: unknown;
65
+ logMessages?: string[];
66
+ innerInstructions?: Array<{
67
+ instructions?: SolanaIx[];
68
+ }>;
69
+ } | null;
70
+ transaction?: {
71
+ message?: {
72
+ instructions?: SolanaIx[];
73
+ };
74
+ };
75
+ }
76
+ export interface BaseAnchorTx {
77
+ hash?: string;
78
+ input?: string;
79
+ chainId?: number;
80
+ }
81
+ export interface AnchorWitnessResult {
82
+ overall: 'verified' | 'partial' | 'failed';
83
+ root: string | null;
84
+ inclusion: {
85
+ valid: boolean;
86
+ leaf: string | null;
87
+ leaf_source: string;
88
+ reason?: string;
89
+ };
90
+ solana: {
91
+ checked: boolean;
92
+ valid: boolean;
93
+ signature: string | null;
94
+ slot: number | null;
95
+ cluster: string | null;
96
+ memo: string | null;
97
+ reason?: string;
98
+ };
99
+ base: {
100
+ checked: boolean;
101
+ valid: boolean;
102
+ tx: string | null;
103
+ chain_id: number | null;
104
+ reason?: string;
105
+ };
106
+ proves: string[];
107
+ does_not_prove: string[];
108
+ errors: string[];
109
+ }
110
+ export declare function leafHash(bytes: Uint8Array): Buffer;
111
+ export declare function normalizeRoot(root: string | null | undefined): string | null;
112
+ /** RFC 6962-style inclusion. Same rule as the gateway tree. */
113
+ export declare function verifyMerkleInclusion(leaf: Buffer, index: number, treeSize: number, rootHex: string, proof: InclusionStep[]): boolean;
114
+ export declare function parseAnchorMemo(memo: string): {
115
+ scope: string;
116
+ day: string;
117
+ root: string;
118
+ prev: string;
119
+ } | null;
120
+ export declare function extractMemos(tx: SolanaAnchorTx | null | undefined): string[];
121
+ export declare function fetchSolanaGenesisHash(rpcUrl: string): Promise<string>;
122
+ export declare function fetchBaseAnchorTransaction(txHash: string, rpcUrl: string): Promise<BaseAnchorTx | null>;
123
+ export interface VerifyAnchoredRootInput {
124
+ receipt: AnchorReceipt;
125
+ inclusion: AnchorInclusion;
126
+ head: AnchorHead;
127
+ baseRpcUrl?: string;
128
+ solanaRpcUrl?: string;
129
+ fetchSolanaTx?: (signature: string, rpcUrl: string) => Promise<SolanaAnchorTx | null>;
130
+ fetchGenesis?: (rpcUrl: string) => Promise<string>;
131
+ fetchBaseTx?: (txHash: string, rpcUrl: string) => Promise<BaseAnchorTx | null>;
132
+ }
133
+ /**
134
+ * Verify inclusion, then the Solana memo and the Base calldata for that root.
135
+ */
136
+ export declare function verifyAnchoredRoot(input: VerifyAnchoredRootInput): Promise<AnchorWitnessResult>;
137
+ export {};
138
+ //# sourceMappingURL=anchor-witness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anchor-witness.d.ts","sourceRoot":"","sources":["../src/anchor-witness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,365 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ANCHOR_DOES_NOT_PROVE = exports.ANCHOR_PROVES = exports.BASE_MAINNET_CHAIN_ID = exports.SOLANA_GENESIS = exports.MEMO_PROGRAM_ID_V1 = exports.MEMO_PROGRAM_ID = void 0;
4
+ exports.leafHash = leafHash;
5
+ exports.normalizeRoot = normalizeRoot;
6
+ exports.verifyMerkleInclusion = verifyMerkleInclusion;
7
+ exports.parseAnchorMemo = parseAnchorMemo;
8
+ exports.extractMemos = extractMemos;
9
+ exports.fetchSolanaGenesisHash = fetchSolanaGenesisHash;
10
+ exports.fetchBaseAnchorTransaction = fetchBaseAnchorTransaction;
11
+ exports.verifyAnchoredRoot = verifyAnchoredRoot;
12
+ /**
13
+ * Check a receipt leaf against a signed tree head, then against the two
14
+ * anchors that publish that root: an SPL Memo on Solana and calldata on Base.
15
+ *
16
+ * Inclusion is local. The chain checks use the RPC you pass. A matching memo
17
+ * and matching calldata show the issuer published this root. They do not show
18
+ * the payment.
19
+ */
20
+ const node_crypto_1 = require("node:crypto");
21
+ const base_payer_js_1 = require("./base-payer.js");
22
+ const solana_payer_js_1 = require("./solana-payer.js");
23
+ exports.MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
24
+ exports.MEMO_PROGRAM_ID_V1 = 'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo';
25
+ /** Genesis hashes from the Solana cluster CAIP-2 references. */
26
+ exports.SOLANA_GENESIS = {
27
+ 'mainnet-beta': '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
28
+ devnet: 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG',
29
+ testnet: '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY',
30
+ };
31
+ exports.BASE_MAINNET_CHAIN_ID = 8453;
32
+ exports.ANCHOR_PROVES = [
33
+ 'This receipt leaf is inside the issuer append-only tree of the stated size, under the stated root.',
34
+ 'The Solana transaction memo contains that root, and the RPC genesis hash is the cluster named on the head.',
35
+ 'The Base transaction calldata is that same root, on chain id 8453.',
36
+ ];
37
+ exports.ANCHOR_DOES_NOT_PROVE = [
38
+ 'It does not prove the payment, the payer, the payee, or the model output.',
39
+ 'It does not prove a receipt appended after this tree size is inside the root.',
40
+ 'It does not prove the issuer included only honest rows. It proves this leaf hashes into the published root.',
41
+ 'It does not prove the RPC itself is honest. It checks the transaction and genesis hash that RPC returned.',
42
+ ];
43
+ function sha256(buf) {
44
+ return (0, node_crypto_1.createHash)('sha256').update(buf).digest();
45
+ }
46
+ function leafHash(bytes) {
47
+ return sha256(Buffer.concat([Buffer.from([0x00]), Buffer.from(bytes)]));
48
+ }
49
+ function nodeHash(left, right) {
50
+ return sha256(Buffer.concat([Buffer.from([0x01]), left, right]));
51
+ }
52
+ function normalizeRoot(root) {
53
+ const hex = String(root || '').replace(/^0x/, '').toLowerCase();
54
+ return /^[0-9a-f]{64}$/.test(hex) ? hex : null;
55
+ }
56
+ /** RFC 6962-style inclusion. Same rule as the gateway tree. */
57
+ function verifyMerkleInclusion(leaf, index, treeSize, rootHex, proof) {
58
+ if (!Array.isArray(proof) || index < 0 || index >= treeSize)
59
+ return false;
60
+ let hash = new Uint8Array(leaf);
61
+ for (const step of proof) {
62
+ if (!step || !/^[0-9a-fA-F]{64}$/.test(step.hash))
63
+ return false;
64
+ const sib = Buffer.from(step.hash, 'hex');
65
+ const next = step.position === 'left' ? nodeHash(sib, hash) : nodeHash(hash, sib);
66
+ hash = new Uint8Array(next);
67
+ }
68
+ return Buffer.from(hash).toString('hex') === String(rootHex).replace(/^0x/, '').toLowerCase();
69
+ }
70
+ function parseAnchorMemo(memo) {
71
+ const parts = String(memo || '').split(':');
72
+ if (parts.length !== 7)
73
+ return null;
74
+ if (parts[0] !== 'chit402' || parts[1] !== 'root' || parts[2] !== 'v1')
75
+ return null;
76
+ const scope = parts[3];
77
+ const day = parts[4];
78
+ const root = parts[5];
79
+ const prev = parts[6];
80
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(scope))
81
+ return null;
82
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(day))
83
+ return null;
84
+ if (!/^[0-9a-f]{64}$/.test(root) || !/^[0-9a-f]{64}$/.test(prev))
85
+ return null;
86
+ return { scope, day, root, prev };
87
+ }
88
+ function isMemoIx(ix) {
89
+ const program = ix.program || '';
90
+ const programId = ix.programId || '';
91
+ return program === 'spl-memo'
92
+ || programId === exports.MEMO_PROGRAM_ID
93
+ || programId === exports.MEMO_PROGRAM_ID_V1;
94
+ }
95
+ function extractMemos(tx) {
96
+ if (!tx)
97
+ return [];
98
+ const found = [];
99
+ const push = (value) => {
100
+ if (value && !found.includes(value))
101
+ found.push(value);
102
+ };
103
+ const visit = (ix) => {
104
+ if (!ix || !isMemoIx(ix))
105
+ return;
106
+ if (typeof ix.parsed === 'string')
107
+ push(ix.parsed);
108
+ else if (ix.parsed && typeof ix.parsed.memo === 'string')
109
+ push(ix.parsed.memo);
110
+ else if (ix.parsed && typeof ix.parsed.info === 'string')
111
+ push(ix.parsed.info);
112
+ };
113
+ for (const ix of tx.transaction?.message?.instructions || [])
114
+ visit(ix);
115
+ for (const inner of tx.meta?.innerInstructions || []) {
116
+ for (const ix of inner.instructions || [])
117
+ visit(ix);
118
+ }
119
+ for (const line of tx.meta?.logMessages || []) {
120
+ const match = /Memo \(len \d+\): "(.*)"/.exec(line);
121
+ if (match)
122
+ push(match[1]);
123
+ }
124
+ return found;
125
+ }
126
+ async function defaultRpc(url, method, params) {
127
+ const res = await fetch(url, {
128
+ method: 'POST',
129
+ headers: { 'content-type': 'application/json' },
130
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
131
+ });
132
+ if (!res.ok)
133
+ throw new Error(`HTTP ${res.status}`);
134
+ const json = await res.json();
135
+ if (json.error)
136
+ throw new Error(json.error.message || 'rpc_error');
137
+ return json.result ?? null;
138
+ }
139
+ async function fetchSolanaGenesisHash(rpcUrl) {
140
+ const result = await defaultRpc(rpcUrl, 'getGenesisHash', []);
141
+ if (typeof result !== 'string' || result.length === 0)
142
+ throw new Error('no_genesis');
143
+ return result;
144
+ }
145
+ async function fetchBaseAnchorTransaction(txHash, rpcUrl) {
146
+ const tx = await defaultRpc(rpcUrl, 'eth_getTransactionByHash', [txHash]);
147
+ const chainHex = await defaultRpc(rpcUrl, 'eth_chainId', []);
148
+ if (!tx)
149
+ return null;
150
+ return {
151
+ hash: tx.hash,
152
+ input: tx.input,
153
+ chainId: chainHex ? Number(chainHex) : undefined,
154
+ };
155
+ }
156
+ function rowHashFrom(receipt, inclusion) {
157
+ if (typeof receipt.row_hash === 'string')
158
+ return { rowHash: receipt.row_hash, source: 'receipt' };
159
+ if (typeof receipt.book_chain?.row_hash === 'string')
160
+ return { rowHash: receipt.book_chain.row_hash, source: 'receipt' };
161
+ if (typeof inclusion.row_hash === 'string')
162
+ return { rowHash: inclusion.row_hash, source: 'inclusion' };
163
+ return null;
164
+ }
165
+ function solanaSignature(head) {
166
+ const sol = head.anchors?.solana;
167
+ if (!sol || sol.status === 'pending')
168
+ return null;
169
+ return sol.signature || null;
170
+ }
171
+ function baseTxHash(head) {
172
+ const base = head.anchors?.base;
173
+ if (base?.status === 'pending')
174
+ return null;
175
+ return base?.tx || head.anchor?.tx || head.anchor_tx || null;
176
+ }
177
+ /**
178
+ * Verify inclusion, then the Solana memo and the Base calldata for that root.
179
+ */
180
+ async function verifyAnchoredRoot(input) {
181
+ const errors = [];
182
+ const doesNotProve = [...exports.ANCHOR_DOES_NOT_PROVE];
183
+ const root = normalizeRoot(input.head?.root);
184
+ const inclusionRoot = normalizeRoot(input.inclusion?.root);
185
+ let leafHex = null;
186
+ let leafSource = 'none';
187
+ let inclusionValid = false;
188
+ let inclusionReason;
189
+ const taskId = input.receipt?.task_id ? String(input.receipt.task_id) : '';
190
+ if (!taskId) {
191
+ inclusionReason = 'missing_task_id';
192
+ }
193
+ else if (input.inclusion?.task_id && String(input.inclusion.task_id) !== taskId) {
194
+ inclusionReason = 'task_mismatch';
195
+ }
196
+ else if (!root || !inclusionRoot) {
197
+ inclusionReason = 'bad_root';
198
+ }
199
+ else if (root !== inclusionRoot) {
200
+ inclusionReason = 'root_mismatch';
201
+ }
202
+ else if (input.head.tree_size != null && Number(input.head.tree_size) !== Number(input.inclusion.tree_size)) {
203
+ inclusionReason = 'tree_size_mismatch';
204
+ }
205
+ else {
206
+ const row = rowHashFrom(input.receipt, input.inclusion);
207
+ let leaf = null;
208
+ if (row) {
209
+ leaf = leafHash(Buffer.from(`${taskId}|${row.rowHash}`));
210
+ leafSource = row.source;
211
+ if (input.inclusion.leaf && input.inclusion.leaf.toLowerCase() !== leaf.toString('hex')) {
212
+ inclusionReason = 'leaf_mismatch';
213
+ leaf = null;
214
+ }
215
+ }
216
+ else if (input.inclusion.leaf && /^[0-9a-fA-F]{64}$/.test(input.inclusion.leaf)) {
217
+ leaf = Buffer.from(input.inclusion.leaf, 'hex');
218
+ leafSource = 'inclusion';
219
+ doesNotProve.push('The leaf was taken from the inclusion object. The receipt had no row_hash, so this check did not recompute the leaf from the receipt bytes.');
220
+ }
221
+ else {
222
+ inclusionReason = 'no_leaf';
223
+ }
224
+ if (leaf) {
225
+ leafHex = leaf.toString('hex');
226
+ inclusionValid = verifyMerkleInclusion(leaf, Number(input.inclusion.leaf_index), Number(input.inclusion.tree_size), root, input.inclusion.proof || []);
227
+ if (!inclusionValid)
228
+ inclusionReason = 'inclusion_failed';
229
+ }
230
+ }
231
+ if (!inclusionValid && inclusionReason)
232
+ errors.push(inclusionReason);
233
+ const solanaRpc = input.solanaRpcUrl || process.env.SOLANA_RPC_URL || solana_payer_js_1.SOLANA_RPC_URL;
234
+ const baseRpc = input.baseRpcUrl || base_payer_js_1.BASE_RPC_URL;
235
+ const signature = root && inclusionValid ? solanaSignature(input.head) : null;
236
+ const txHash = root && inclusionValid ? baseTxHash(input.head) : null;
237
+ const solana = {
238
+ checked: false,
239
+ valid: false,
240
+ signature: input.head?.anchors?.solana?.signature ?? null,
241
+ slot: input.head?.anchors?.solana?.slot ?? null,
242
+ cluster: input.head?.anchors?.solana?.cluster ?? null,
243
+ memo: input.head?.anchors?.solana?.memo ?? null,
244
+ reason: undefined,
245
+ };
246
+ const base = {
247
+ checked: false,
248
+ valid: false,
249
+ tx: input.head?.anchors?.base?.tx || input.head?.anchor?.tx || input.head?.anchor_tx || null,
250
+ chain_id: input.head?.anchors?.base?.chain_id ?? input.head?.anchor?.chain_id ?? null,
251
+ reason: undefined,
252
+ };
253
+ if (!inclusionValid) {
254
+ solana.reason = 'inclusion_failed';
255
+ base.reason = 'inclusion_failed';
256
+ }
257
+ else {
258
+ if (!signature) {
259
+ solana.reason = 'pending';
260
+ errors.push('solana_pending');
261
+ }
262
+ else {
263
+ solana.checked = true;
264
+ try {
265
+ const fetchTx = input.fetchSolanaTx || ((sig, url) => (0, solana_payer_js_1.fetchSolanaTransaction)(sig, url));
266
+ const fetchGenesis = input.fetchGenesis || fetchSolanaGenesisHash;
267
+ const tx = await fetchTx(signature, solanaRpc);
268
+ if (!tx) {
269
+ solana.reason = 'tx_not_found';
270
+ }
271
+ else if (tx.meta?.err) {
272
+ solana.reason = 'tx_failed';
273
+ }
274
+ else {
275
+ const memos = extractMemos(tx);
276
+ const recorded = input.head.anchors?.solana?.memo || null;
277
+ const matched = memos.filter((memo) => {
278
+ const parsed = parseAnchorMemo(memo);
279
+ return Boolean(parsed && parsed.root === root && memo.includes(root) && (!recorded || memo === recorded));
280
+ });
281
+ const cluster = input.head.anchors?.solana?.cluster || null;
282
+ const expectedGenesis = cluster ? exports.SOLANA_GENESIS[cluster] : null;
283
+ if (matched.length === 0)
284
+ solana.reason = 'memo_mismatch';
285
+ else if (!cluster)
286
+ solana.reason = 'no_cluster';
287
+ else if (!expectedGenesis)
288
+ solana.reason = 'unknown_cluster';
289
+ else {
290
+ const genesis = await fetchGenesis(solanaRpc);
291
+ if (genesis !== expectedGenesis)
292
+ solana.reason = 'cluster_mismatch';
293
+ }
294
+ const headSlot = input.head.anchors?.solana?.slot;
295
+ if (!solana.reason && headSlot != null && tx.slot != null && Number(tx.slot) !== Number(headSlot)) {
296
+ solana.reason = 'slot_mismatch';
297
+ }
298
+ if (!solana.reason) {
299
+ solana.valid = true;
300
+ solana.memo = matched[0];
301
+ if (tx.slot != null)
302
+ solana.slot = tx.slot;
303
+ }
304
+ }
305
+ }
306
+ catch (err) {
307
+ solana.reason = err instanceof Error ? err.message : 'solana_rpc_error';
308
+ }
309
+ if (!solana.valid && solana.reason)
310
+ errors.push(`solana:${solana.reason}`);
311
+ }
312
+ if (!txHash) {
313
+ base.reason = 'pending';
314
+ errors.push('base_pending');
315
+ }
316
+ else {
317
+ base.checked = true;
318
+ try {
319
+ const fetchBase = input.fetchBaseTx || fetchBaseAnchorTransaction;
320
+ const tx = await fetchBase(txHash, baseRpc);
321
+ const expectedChain = input.head.anchors?.base?.chain_id || input.head.anchor?.chain_id || exports.BASE_MAINNET_CHAIN_ID;
322
+ if (!tx) {
323
+ base.reason = 'tx_not_found';
324
+ }
325
+ else if (tx.chainId != null && Number(tx.chainId) !== Number(expectedChain)) {
326
+ base.reason = 'chain_mismatch';
327
+ base.chain_id = Number(tx.chainId);
328
+ }
329
+ else {
330
+ const inputData = String(tx.input || '').toLowerCase().replace(/^0x/, '');
331
+ if (inputData !== root)
332
+ base.reason = 'calldata_mismatch';
333
+ else {
334
+ base.valid = true;
335
+ base.tx = tx.hash || txHash;
336
+ base.chain_id = tx.chainId ?? expectedChain;
337
+ }
338
+ }
339
+ }
340
+ catch (err) {
341
+ base.reason = err instanceof Error ? err.message : 'base_rpc_error';
342
+ }
343
+ if (!base.valid && base.reason)
344
+ errors.push(`base:${base.reason}`);
345
+ }
346
+ }
347
+ let overall;
348
+ if (!inclusionValid || (solana.checked && !solana.valid) || (base.checked && !base.valid))
349
+ overall = 'failed';
350
+ else if (solana.valid && base.valid)
351
+ overall = 'verified';
352
+ else
353
+ overall = 'partial';
354
+ return {
355
+ overall,
356
+ root,
357
+ inclusion: { valid: inclusionValid, leaf: leafHex, leaf_source: leafSource, reason: inclusionReason },
358
+ solana,
359
+ base,
360
+ proves: exports.ANCHOR_PROVES,
361
+ does_not_prove: doesNotProve,
362
+ errors,
363
+ };
364
+ }
365
+ //# sourceMappingURL=anchor-witness.js.map