chit402-verify 0.3.5 → 0.4.1

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 (108) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +205 -17
  3. package/dist/anchor-trust.d.ts +144 -0
  4. package/dist/anchor-trust.d.ts.map +1 -0
  5. package/dist/anchor-trust.js +441 -0
  6. package/dist/anchor-trust.js.map +1 -0
  7. package/dist/anchor-witness.d.ts +265 -0
  8. package/dist/anchor-witness.d.ts.map +1 -0
  9. package/dist/anchor-witness.js +833 -0
  10. package/dist/anchor-witness.js.map +1 -0
  11. package/dist/base-payer.d.ts +65 -0
  12. package/dist/base-payer.d.ts.map +1 -0
  13. package/dist/base-payer.js +222 -0
  14. package/dist/base-payer.js.map +1 -0
  15. package/dist/binding.d.ts +52 -0
  16. package/dist/binding.d.ts.map +1 -0
  17. package/dist/binding.js +80 -0
  18. package/dist/binding.js.map +1 -0
  19. package/dist/canonical-preimage.d.ts +9 -0
  20. package/dist/canonical-preimage.d.ts.map +1 -0
  21. package/dist/canonical-preimage.js +25 -0
  22. package/dist/canonical-preimage.js.map +1 -0
  23. package/dist/carry-forward.d.ts +79 -0
  24. package/dist/carry-forward.d.ts.map +1 -0
  25. package/dist/carry-forward.js +184 -0
  26. package/dist/carry-forward.js.map +1 -0
  27. package/dist/cli.d.ts +18 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +2086 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/epoch.d.ts +143 -0
  32. package/dist/epoch.d.ts.map +1 -0
  33. package/dist/epoch.js +352 -0
  34. package/dist/epoch.js.map +1 -0
  35. package/dist/head-binding.d.ts +24 -0
  36. package/dist/head-binding.d.ts.map +1 -0
  37. package/dist/head-binding.js +61 -0
  38. package/dist/head-binding.js.map +1 -0
  39. package/dist/index.d.ts +585 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +1648 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/issuer-history.d.ts +151 -0
  44. package/dist/issuer-history.d.ts.map +1 -0
  45. package/dist/issuer-history.js +595 -0
  46. package/dist/issuer-history.js.map +1 -0
  47. package/dist/issuer-pin.d.ts +126 -0
  48. package/dist/issuer-pin.d.ts.map +1 -0
  49. package/dist/issuer-pin.js +567 -0
  50. package/dist/issuer-pin.js.map +1 -0
  51. package/dist/jcs.d.ts +8 -0
  52. package/dist/jcs.d.ts.map +1 -0
  53. package/dist/jcs.js +64 -0
  54. package/dist/jcs.js.map +1 -0
  55. package/dist/jws.d.ts +46 -0
  56. package/dist/jws.d.ts.map +1 -0
  57. package/dist/jws.js +129 -0
  58. package/dist/jws.js.map +1 -0
  59. package/dist/payer.d.ts +65 -0
  60. package/dist/payer.d.ts.map +1 -0
  61. package/dist/payer.js +148 -0
  62. package/dist/payer.js.map +1 -0
  63. package/dist/preimage.d.ts +78 -0
  64. package/dist/preimage.d.ts.map +1 -0
  65. package/dist/preimage.js +363 -0
  66. package/dist/preimage.js.map +1 -0
  67. package/dist/receipt-lane.d.ts +235 -0
  68. package/dist/receipt-lane.d.ts.map +1 -0
  69. package/dist/receipt-lane.js +446 -0
  70. package/dist/receipt-lane.js.map +1 -0
  71. package/dist/receipt-version.d.ts +98 -0
  72. package/dist/receipt-version.d.ts.map +1 -0
  73. package/dist/receipt-version.js +364 -0
  74. package/dist/receipt-version.js.map +1 -0
  75. package/dist/refusal.d.ts +93 -0
  76. package/dist/refusal.d.ts.map +1 -0
  77. package/dist/refusal.js +249 -0
  78. package/dist/refusal.js.map +1 -0
  79. package/dist/request-binding.d.ts +7 -0
  80. package/dist/request-binding.d.ts.map +1 -0
  81. package/dist/request-binding.js +26 -0
  82. package/dist/request-binding.js.map +1 -0
  83. package/dist/row-hash.d.ts +22 -0
  84. package/dist/row-hash.d.ts.map +1 -0
  85. package/dist/row-hash.js +26 -0
  86. package/dist/row-hash.js.map +1 -0
  87. package/dist/shell.d.ts +65 -0
  88. package/dist/shell.d.ts.map +1 -0
  89. package/dist/shell.js +268 -0
  90. package/dist/shell.js.map +1 -0
  91. package/dist/solana-payer.d.ts +99 -0
  92. package/dist/solana-payer.d.ts.map +1 -0
  93. package/dist/solana-payer.js +258 -0
  94. package/dist/solana-payer.js.map +1 -0
  95. package/dist/v11-receipt.d.ts +23 -0
  96. package/dist/v11-receipt.d.ts.map +1 -0
  97. package/dist/v11-receipt.js +368 -0
  98. package/dist/v11-receipt.js.map +1 -0
  99. package/dist/verdict.d.ts +98 -0
  100. package/dist/verdict.d.ts.map +1 -0
  101. package/dist/verdict.js +390 -0
  102. package/dist/verdict.js.map +1 -0
  103. package/issuer-history.snapshot.json +1 -0
  104. package/package.json +32 -22
  105. package/cli.js +0 -42
  106. package/index.cjs +0 -8
  107. package/index.d.ts +0 -5
  108. package/index.js +0 -7
@@ -0,0 +1,833 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NEWEST_ANCHOR_PAGE_CAP = exports.NEWEST_ANCHOR_PAGE_LIMIT = 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.inclusionRoot = inclusionRoot;
7
+ exports.verifyMerkleInclusion = verifyMerkleInclusion;
8
+ exports.parseAnchorMemo = parseAnchorMemo;
9
+ exports.extractMemos = extractMemos;
10
+ exports.fetchSolanaGenesisHash = fetchSolanaGenesisHash;
11
+ exports.fetchBaseAnchorTransaction = fetchBaseAnchorTransaction;
12
+ exports.fetchNewestSolanaAnchorRoot = fetchNewestSolanaAnchorRoot;
13
+ exports.solanaFeePayer = solanaFeePayer;
14
+ exports.verifyAnchoredRoot = verifyAnchoredRoot;
15
+ /**
16
+ * Check a receipt leaf against a signed tree head, then against the two
17
+ * anchors that publish that root: an SPL Memo on Solana and calldata on Base.
18
+ *
19
+ * Inclusion is local. The chain checks use the RPC you pass. A matching memo
20
+ * and matching calldata show the issuer published this root. They do not show
21
+ * the payment.
22
+ */
23
+ const node_crypto_1 = require("node:crypto");
24
+ const anchor_trust_js_1 = require("./anchor-trust.js");
25
+ const base_payer_js_1 = require("./base-payer.js");
26
+ const epoch_js_1 = require("./epoch.js");
27
+ const issuer_pin_js_1 = require("./issuer-pin.js");
28
+ const row_hash_js_1 = require("./row-hash.js");
29
+ const solana_payer_js_1 = require("./solana-payer.js");
30
+ exports.MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
31
+ exports.MEMO_PROGRAM_ID_V1 = 'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo';
32
+ /**
33
+ * Full `getGenesisHash` values. CAIP-2 references keep only the first 32
34
+ * characters, and `getGenesisHash` returns the whole base58 hash. A prefix
35
+ * compare never matches mainnet-beta.
36
+ */
37
+ exports.SOLANA_GENESIS = {
38
+ 'mainnet-beta': '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d',
39
+ devnet: 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG',
40
+ testnet: '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY',
41
+ };
42
+ exports.BASE_MAINNET_CHAIN_ID = 8453;
43
+ exports.ANCHOR_PROVES = [
44
+ 'This receipt leaf is inside the tree of the stated size, under the stated root.',
45
+ 'The tree head carries a valid ES256 issuer signature. The kid is trusted by the production pin, issuer history, or JWKS, and the signed root, size, epoch, and anchors match the head.',
46
+ 'The Solana transaction memo contains that root, its fee payer is the signed fee payer, and the RPC genesis hash is the cluster named on the head.',
47
+ 'The Base transaction calldata is that same root, on chain id 8453, and its sender is the signed anchor wallet.',
48
+ 'Those wallets are on the issuer anchor-wallet list: the package pin, or a signed well-known or issuer-history list.',
49
+ ];
50
+ exports.ANCHOR_DOES_NOT_PROVE = [
51
+ 'It does not prove the payment, the payer, the payee, or the model output.',
52
+ 'It does not prove a receipt appended after this tree size is inside the root.',
53
+ 'It does not prove the issuer included only honest rows. It proves this leaf hashes into the published root.',
54
+ 'It does not prove the RPC itself is honest. It checks the transaction and genesis hash that RPC returned.',
55
+ 'It does not read the on-chain issuer-root registry or the DNS anchor. Those sources are reserved and are not consulted.',
56
+ ];
57
+ function sha256(buf) {
58
+ return (0, node_crypto_1.createHash)('sha256').update(buf).digest();
59
+ }
60
+ function leafHash(bytes) {
61
+ return sha256(Buffer.concat([Buffer.from([0x00]), Buffer.from(bytes)]));
62
+ }
63
+ function nodeHash(left, right) {
64
+ return sha256(Buffer.concat([Buffer.from([0x01]), left, right]));
65
+ }
66
+ function normalizeRoot(root) {
67
+ const hex = String(root || '').replace(/^0x/, '').toLowerCase();
68
+ return /^[0-9a-f]{64}$/.test(hex) ? hex : null;
69
+ }
70
+ function half(n) {
71
+ return Math.floor(n / 2);
72
+ }
73
+ /**
74
+ * RFC 9162 fold. Returns the recomputed root, or null when the proof is the
75
+ * wrong length for `(index, treeSize)` or a sibling is not 32 bytes.
76
+ * Index and tree size choose the side. A `position` label is not read here.
77
+ */
78
+ function inclusionRoot(leaf, index, treeSize, proof) {
79
+ if (!Array.isArray(proof))
80
+ return null;
81
+ const leafIndex = Number(index);
82
+ const size = Number(treeSize);
83
+ if (!Number.isSafeInteger(leafIndex) || !Number.isSafeInteger(size))
84
+ return null;
85
+ if (leafIndex < 0 || leafIndex >= size)
86
+ return null;
87
+ let fn = leafIndex;
88
+ let sn = size - 1;
89
+ let hash = Buffer.from(leaf);
90
+ for (const step of proof) {
91
+ if (sn === 0)
92
+ return null;
93
+ if (!step || !/^[0-9a-fA-F]{64}$/.test(step.hash))
94
+ return null;
95
+ const sib = Buffer.from(step.hash, 'hex');
96
+ const siblingOnLeft = (fn % 2) === 1 || fn === sn;
97
+ if (siblingOnLeft) {
98
+ hash = nodeHash(sib, hash);
99
+ if ((fn % 2) === 0) {
100
+ while ((fn % 2) === 0 && fn !== 0) {
101
+ fn = half(fn);
102
+ sn = half(sn);
103
+ }
104
+ }
105
+ }
106
+ else {
107
+ hash = nodeHash(hash, sib);
108
+ }
109
+ fn = half(fn);
110
+ sn = half(sn);
111
+ }
112
+ if (sn !== 0)
113
+ return null;
114
+ return hash.toString('hex');
115
+ }
116
+ /**
117
+ * A present `position` label must name the side `index` and `treeSize` already
118
+ * chose. An omitted label does not. This is the inclusion-proof check. The
119
+ * public audit path folds with `inclusionRoot` and does not read the label.
120
+ */
121
+ function positionAgrees(index, treeSize, proof) {
122
+ if (!Array.isArray(proof))
123
+ return false;
124
+ const leafIndex = Number(index);
125
+ const size = Number(treeSize);
126
+ if (!Number.isSafeInteger(leafIndex) || !Number.isSafeInteger(size))
127
+ return false;
128
+ if (leafIndex < 0 || leafIndex >= size)
129
+ return false;
130
+ let fn = leafIndex;
131
+ let sn = size - 1;
132
+ for (const step of proof) {
133
+ if (sn === 0)
134
+ return false;
135
+ const siblingOnLeft = (fn % 2) === 1 || fn === sn;
136
+ if (typeof step?.position === 'string' && step.position !== '' && step.position !== (siblingOnLeft ? 'left' : 'right')) {
137
+ return false;
138
+ }
139
+ if (siblingOnLeft && (fn % 2) === 0) {
140
+ while ((fn % 2) === 0 && fn !== 0) {
141
+ fn = half(fn);
142
+ sn = half(sn);
143
+ }
144
+ }
145
+ fn = half(fn);
146
+ sn = half(sn);
147
+ }
148
+ return true;
149
+ }
150
+ /**
151
+ * RFC 9162 §2.1.3.2 inclusion. `index` and `treeSize` choose left or right
152
+ * at each step. A `position` label is not the source of that side. When a
153
+ * label is present it must name the side the index already chose, so a
154
+ * swapped label fails. An omitted label does not. A proof whose length is
155
+ * not the length that pair requires is rejected, as is `index >= treeSize`.
156
+ * Leaf and node bytes stay SHA-256(0x00 || leaf) and SHA-256(0x01 || left ||
157
+ * right). Consistency proofs and the empty root are not this function.
158
+ */
159
+ function verifyMerkleInclusion(leaf, index, treeSize, rootHex, proof) {
160
+ const root = String(rootHex || '').replace(/^0x/, '').toLowerCase();
161
+ if (!/^[0-9a-f]{64}$/.test(root))
162
+ return false;
163
+ if (!positionAgrees(index, treeSize, proof))
164
+ return false;
165
+ return inclusionRoot(leaf, index, treeSize, proof) === root;
166
+ }
167
+ function memoIdentity(scope, day, root, prev) {
168
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(scope))
169
+ return null;
170
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(day))
171
+ return null;
172
+ if (!/^[0-9a-f]{64}$/.test(root) || !/^[0-9a-f]{64}$/.test(prev))
173
+ return null;
174
+ return { scope, day, root, prev };
175
+ }
176
+ /** v1 memos stay valid. v2 adds epoch, prev_epoch_root, prev_epoch_size, and the bundle index hash. */
177
+ function parseAnchorMemo(memo) {
178
+ const parts = String(memo || '').split(':');
179
+ if (parts[0] !== 'chit402' || parts[1] !== 'root')
180
+ return null;
181
+ if (parts.length === 7 && parts[2] === 'v1') {
182
+ const base = memoIdentity(parts[3], parts[4], parts[5], parts[6]);
183
+ return base ? { version: 1, ...base } : null;
184
+ }
185
+ if (parts.length === 11 && parts[2] === 'v2') {
186
+ const base = memoIdentity(parts[3], parts[4], parts[5], parts[6]);
187
+ if (!base)
188
+ return null;
189
+ const epoch = Number(parts[7]);
190
+ const prevEpoch = parts[8];
191
+ const size = Number(parts[9]);
192
+ const bundle = parts[10];
193
+ if (!Number.isInteger(epoch) || epoch < 1)
194
+ return null;
195
+ if (!/^[0-9a-f]{64}$/.test(prevEpoch) || !/^[0-9a-f]{64}$/.test(bundle))
196
+ return null;
197
+ if (!Number.isInteger(size) || size < 0)
198
+ return null;
199
+ return {
200
+ version: 2,
201
+ ...base,
202
+ epoch,
203
+ prev_epoch_root: prevEpoch,
204
+ prev_epoch_size: size,
205
+ bundle_index_hash: bundle,
206
+ };
207
+ }
208
+ return null;
209
+ }
210
+ function isMemoIx(ix) {
211
+ const program = ix.program || '';
212
+ const programId = ix.programId || '';
213
+ return program === 'spl-memo'
214
+ || programId === exports.MEMO_PROGRAM_ID
215
+ || programId === exports.MEMO_PROGRAM_ID_V1;
216
+ }
217
+ function extractMemos(tx) {
218
+ if (!tx)
219
+ return [];
220
+ const found = [];
221
+ const push = (value) => {
222
+ if (value && !found.includes(value))
223
+ found.push(value);
224
+ };
225
+ const visit = (ix) => {
226
+ if (!ix || !isMemoIx(ix))
227
+ return;
228
+ if (typeof ix.parsed === 'string')
229
+ push(ix.parsed);
230
+ else if (ix.parsed && typeof ix.parsed.memo === 'string')
231
+ push(ix.parsed.memo);
232
+ else if (ix.parsed && typeof ix.parsed.info === 'string')
233
+ push(ix.parsed.info);
234
+ };
235
+ for (const ix of tx.transaction?.message?.instructions || [])
236
+ visit(ix);
237
+ for (const inner of tx.meta?.innerInstructions || []) {
238
+ for (const ix of inner.instructions || [])
239
+ visit(ix);
240
+ }
241
+ for (const line of tx.meta?.logMessages || []) {
242
+ const match = /Memo \(len \d+\): "(.*)"/.exec(line);
243
+ if (match)
244
+ push(match[1]);
245
+ }
246
+ return found;
247
+ }
248
+ async function defaultRpc(url, method, params) {
249
+ const res = await fetch(url, {
250
+ method: 'POST',
251
+ headers: { 'content-type': 'application/json' },
252
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
253
+ });
254
+ if (!res.ok)
255
+ throw new Error(`HTTP ${res.status}`);
256
+ const json = await res.json();
257
+ if (json.error)
258
+ throw new Error(json.error.message || 'rpc_error');
259
+ return json.result ?? null;
260
+ }
261
+ async function fetchSolanaGenesisHash(rpcUrl) {
262
+ const result = await defaultRpc(rpcUrl, 'getGenesisHash', []);
263
+ if (typeof result !== 'string' || result.length === 0)
264
+ throw new Error('no_genesis');
265
+ return result;
266
+ }
267
+ async function fetchBaseAnchorTransaction(txHash, rpcUrl) {
268
+ const tx = await defaultRpc(rpcUrl, 'eth_getTransactionByHash', [txHash]);
269
+ const chainHex = await defaultRpc(rpcUrl, 'eth_chainId', []);
270
+ if (!tx)
271
+ return null;
272
+ return {
273
+ hash: tx.hash,
274
+ input: tx.input,
275
+ from: tx.from,
276
+ chainId: chainHex ? Number(chainHex) : undefined,
277
+ };
278
+ }
279
+ /** Newest-first page size for `getSignaturesForAddress`. */
280
+ exports.NEWEST_ANCHOR_PAGE_LIMIT = 25;
281
+ /** Full pages walked before a missing memo is `newest_anchor_unavailable`. */
282
+ exports.NEWEST_ANCHOR_PAGE_CAP = 8;
283
+ function finiteNumber(value) {
284
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
285
+ }
286
+ function asSafeInt(value) {
287
+ if (typeof value === 'number')
288
+ return Number.isSafeInteger(value) ? value : null;
289
+ if (typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)) {
290
+ const parsed = Number(value);
291
+ return Number.isSafeInteger(parsed) ? parsed : null;
292
+ }
293
+ return null;
294
+ }
295
+ /**
296
+ * Pending is a leaf past the anchored head. An index inside that head is
297
+ * `pending_leaf_in_head`. Any other broken bound is `pending_bounds`.
298
+ * `live_tree_size` is checked only when the inclusion names it.
299
+ */
300
+ function pendingGeometryReason(inclusion) {
301
+ const size = asSafeInt(inclusion?.tree_size);
302
+ const index = asSafeInt(inclusion?.leaf_index);
303
+ if (size == null || index == null || size < 1)
304
+ return 'pending_bounds';
305
+ if (index < size)
306
+ return 'pending_leaf_in_head';
307
+ const liveRaw = inclusion?.live_tree_size;
308
+ if (liveRaw != null) {
309
+ const live = asSafeInt(liveRaw);
310
+ if (live == null || size >= live || index >= live)
311
+ return 'pending_bounds';
312
+ }
313
+ return null;
314
+ }
315
+ /**
316
+ * Newest chit402 root memo paid by this fee payer. `getSignaturesForAddress`
317
+ * is newest-first and includes transactions that only mention the address.
318
+ * A memo whose fee payer is anyone else is ignored and is not newer.
319
+ * Pages use `before` until the first memo this payer actually paid. A listed
320
+ * signature whose transaction is null or whose fetch throws fails the scan.
321
+ * Walking `NEWEST_ANCHOR_PAGE_CAP` full pages without such a memo fails the scan.
322
+ * A short page with no memo from this payer means this payer has no anchor.
323
+ */
324
+ async function fetchNewestSolanaAnchorRoot(feePayer, rpcUrl, fetchTx) {
325
+ const load = fetchTx || ((signature, url) => (0, solana_payer_js_1.fetchSolanaTransaction)(signature, url));
326
+ let before;
327
+ for (let page = 0; page < exports.NEWEST_ANCHOR_PAGE_CAP; page += 1) {
328
+ const query = { limit: exports.NEWEST_ANCHOR_PAGE_LIMIT };
329
+ if (before)
330
+ query.before = before;
331
+ const listed = await defaultRpc(rpcUrl, 'getSignaturesForAddress', [feePayer, query]);
332
+ if (!Array.isArray(listed))
333
+ throw new Error('newest_anchor_unavailable');
334
+ if (listed.length === 0)
335
+ return null;
336
+ for (const row of listed) {
337
+ if (!row || typeof row !== 'object')
338
+ continue;
339
+ const record = row;
340
+ if (record.err)
341
+ continue;
342
+ if (typeof record.signature !== 'string' || record.signature.length === 0)
343
+ continue;
344
+ let tx;
345
+ try {
346
+ tx = await load(record.signature, rpcUrl);
347
+ }
348
+ catch {
349
+ throw new Error('newest_anchor_unavailable');
350
+ }
351
+ if (!tx)
352
+ throw new Error('newest_anchor_unavailable');
353
+ if (solanaFeePayer(tx) !== feePayer)
354
+ continue;
355
+ for (const memo of extractMemos(tx)) {
356
+ const parsed = parseAnchorMemo(memo);
357
+ if (!parsed?.root)
358
+ continue;
359
+ const rowSlot = finiteNumber(record.slot);
360
+ return {
361
+ root: parsed.root,
362
+ signature: record.signature,
363
+ slot: rowSlot != null ? rowSlot : finiteNumber(tx.slot),
364
+ blockTime: finiteNumber(record.blockTime),
365
+ };
366
+ }
367
+ }
368
+ if (listed.length < exports.NEWEST_ANCHOR_PAGE_LIMIT)
369
+ return null;
370
+ const tail = listed[listed.length - 1];
371
+ if (typeof tail?.signature !== 'string' || tail.signature.length === 0) {
372
+ throw new Error('newest_anchor_unavailable');
373
+ }
374
+ if (page + 1 === exports.NEWEST_ANCHOR_PAGE_CAP)
375
+ throw new Error('newest_anchor_unavailable');
376
+ before = tail.signature;
377
+ }
378
+ throw new Error('newest_anchor_unavailable');
379
+ }
380
+ /** Fee payer is the first account. A parsed key that is explicitly not a signer does not count. */
381
+ function solanaFeePayer(tx) {
382
+ const keys = tx?.transaction?.message?.accountKeys || [];
383
+ if (!keys.length)
384
+ return null;
385
+ const first = keys[0];
386
+ if (typeof first === 'string')
387
+ return first;
388
+ if (first?.signer === false)
389
+ return null;
390
+ return first?.pubkey || null;
391
+ }
392
+ function rowHashFrom(receipt, inclusion) {
393
+ const bound = (0, row_hash_js_1.boundRowHash)(receipt, inclusion);
394
+ if (!bound.ok)
395
+ return { reason: 'row_hash_mismatch' };
396
+ if (bound.row == null || bound.row === '')
397
+ return null;
398
+ const fromReceipt = typeof receipt.row_hash === 'string' || typeof receipt.book_chain?.row_hash === 'string';
399
+ return { rowHash: bound.row, source: fromReceipt ? 'receipt' : 'inclusion' };
400
+ }
401
+ function solanaSignature(head) {
402
+ const sol = head.anchors?.solana;
403
+ if (!sol || sol.status === 'pending')
404
+ return null;
405
+ return sol.signature || null;
406
+ }
407
+ function baseTxHash(head) {
408
+ const base = head.anchors?.base;
409
+ if (base?.status === 'pending')
410
+ return null;
411
+ return base?.tx || head.anchor?.tx || head.anchor_tx || null;
412
+ }
413
+ /**
414
+ * Verify inclusion, then the Solana memo and the Base calldata for that root.
415
+ */
416
+ async function verifyAnchoredRoot(input) {
417
+ const errors = [];
418
+ const doesNotProve = [...exports.ANCHOR_DOES_NOT_PROVE];
419
+ const root = normalizeRoot(input.head?.root);
420
+ const inclusionRoot = normalizeRoot(input.inclusion?.root);
421
+ let leafHex = null;
422
+ let leafSource = 'none';
423
+ let inclusionValid = false;
424
+ let inclusionReason;
425
+ const taskId = input.receipt?.task_id ? String(input.receipt.task_id) : '';
426
+ const inclusionError = input.inclusion?.error;
427
+ const pendingLeaf = input.inclusion?.status === 'pending_anchor';
428
+ if (pendingLeaf) {
429
+ if (!taskId)
430
+ inclusionReason = 'missing_task_id';
431
+ else if (input.inclusion?.task_id && String(input.inclusion.task_id) !== taskId)
432
+ inclusionReason = 'task_mismatch';
433
+ else if (!root)
434
+ inclusionReason = 'bad_root';
435
+ else if (inclusionRoot && inclusionRoot !== root)
436
+ inclusionReason = 'root_mismatch';
437
+ else if (input.head.tree_size != null && input.inclusion.tree_size != null
438
+ && Number(input.head.tree_size) !== Number(input.inclusion.tree_size)) {
439
+ inclusionReason = 'tree_size_mismatch';
440
+ }
441
+ else
442
+ inclusionReason = pendingGeometryReason(input.inclusion) || 'pending_anchor';
443
+ }
444
+ else if (inclusionError === 'not_in_tree') {
445
+ inclusionReason = 'not_in_tree';
446
+ }
447
+ else if (!taskId) {
448
+ inclusionReason = 'missing_task_id';
449
+ }
450
+ else if (input.inclusion?.task_id && String(input.inclusion.task_id) !== taskId) {
451
+ inclusionReason = 'task_mismatch';
452
+ }
453
+ else if (!root || !inclusionRoot) {
454
+ inclusionReason = 'bad_root';
455
+ }
456
+ else if (root !== inclusionRoot) {
457
+ inclusionReason = 'root_mismatch';
458
+ }
459
+ else if (input.head.tree_size != null && Number(input.head.tree_size) !== Number(input.inclusion.tree_size)) {
460
+ inclusionReason = 'tree_size_mismatch';
461
+ }
462
+ else {
463
+ const row = rowHashFrom(input.receipt, input.inclusion);
464
+ let leaf = null;
465
+ if (row && 'reason' in row) {
466
+ inclusionReason = row.reason;
467
+ }
468
+ else if (row) {
469
+ leaf = leafHash(Buffer.from(`${taskId}|${row.rowHash}`));
470
+ leafSource = row.source;
471
+ if (input.inclusion.leaf && input.inclusion.leaf.toLowerCase() !== leaf.toString('hex')) {
472
+ inclusionReason = 'leaf_mismatch';
473
+ leaf = null;
474
+ }
475
+ }
476
+ else {
477
+ inclusionReason = 'no_leaf';
478
+ }
479
+ if (leaf) {
480
+ leafHex = leaf.toString('hex');
481
+ inclusionValid = verifyMerkleInclusion(leaf, Number(input.inclusion.leaf_index), Number(input.inclusion.tree_size), root, input.inclusion.proof || []);
482
+ if (!inclusionValid)
483
+ inclusionReason = 'inclusion_failed';
484
+ }
485
+ }
486
+ if (!(pendingLeaf && inclusionReason === 'pending_anchor') && !inclusionValid && inclusionReason) {
487
+ errors.push(inclusionReason);
488
+ }
489
+ let epochReason;
490
+ if (input.head?.epoch == null) {
491
+ const link = (0, epoch_js_1.verifyEpochLink)(input.head, null);
492
+ if (!link.ok)
493
+ epochReason = link.reason || 'epoch_missing';
494
+ }
495
+ const needsEpoch = input.head?.epoch != null || input.epochRecord != null;
496
+ if (!epochReason && needsEpoch) {
497
+ if (!input.epochRecord)
498
+ epochReason = 'epoch_record_missing';
499
+ else {
500
+ const checked = (0, epoch_js_1.verifyEpochRecord)(input.epochRecord, { verifySignature: input.verifyEpochSignature });
501
+ if (!checked.ok)
502
+ epochReason = checked.reason || 'epoch_record';
503
+ else if (input.head?.epoch != null && Number(input.head.epoch) > 1) {
504
+ const prev = (input.epochRecord.epochs || []).find((row) => Number(row.epoch) === Number(input.head.epoch) - 1);
505
+ const link = (0, epoch_js_1.verifyEpochLink)(input.head, prev ? {
506
+ root: prev.final_root || prev.opening_root || null,
507
+ tree_size: prev.final_size ?? prev.opening_size ?? null,
508
+ } : null);
509
+ if (!link.ok)
510
+ epochReason = link.reason || 'epoch_link';
511
+ }
512
+ else if (input.head) {
513
+ const link = (0, epoch_js_1.verifyEpochLink)(input.head, null);
514
+ if (!link.ok)
515
+ epochReason = link.reason || 'epoch_link';
516
+ }
517
+ }
518
+ }
519
+ if (epochReason)
520
+ errors.push(epochReason);
521
+ const headTrust = (0, anchor_trust_js_1.verifyTreeHeadTrust)(input.head, {
522
+ jwks: input.jwks,
523
+ trustedKids: input.trustedKids,
524
+ issuerHistory: input.issuerHistory ?? null,
525
+ strictIssuerHistory: input.strictIssuerHistory === true,
526
+ });
527
+ if (!headTrust.ok && headTrust.reason)
528
+ errors.push(headTrust.reason);
529
+ const legacyHead = headTrust.ok && (0, anchor_trust_js_1.headOmitsPinnedSigner)(headTrust.payload);
530
+ if (legacyHead)
531
+ errors.push(anchor_trust_js_1.LEGACY_HEAD_UNPINNED_SIGNER);
532
+ let publishedWallets = null;
533
+ if (input.anchorWalletDocument) {
534
+ const listed = (0, anchor_trust_js_1.verifyAnchorWalletDocument)(input.anchorWalletDocument, {
535
+ jwks: input.jwks,
536
+ trustedKids: input.trustedKids,
537
+ });
538
+ if (!listed.ok)
539
+ errors.push(listed.reason);
540
+ else
541
+ publishedWallets = { base: listed.base, solana: listed.solana };
542
+ }
543
+ const walletList = (0, anchor_trust_js_1.compileAnchorWallets)({
544
+ jwks: input.jwks,
545
+ trustedKids: input.trustedKids,
546
+ issuerHistory: headTrust.ok ? (input.issuerHistory ?? null) : null,
547
+ published: publishedWallets,
548
+ });
549
+ const anchorWalletsInvalid = errors.includes('anchor_wallets_invalid');
550
+ const solanaRpc = input.solanaRpcUrl || process.env.SOLANA_RPC_URL || solana_payer_js_1.SOLANA_RPC_URL;
551
+ const baseRpc = input.baseRpcUrl || base_payer_js_1.BASE_RPC_URL;
552
+ const signature = root && inclusionValid ? solanaSignature(input.head) : null;
553
+ const txHash = root && inclusionValid ? baseTxHash(input.head) : null;
554
+ const solana = {
555
+ checked: false,
556
+ valid: false,
557
+ signature: input.head?.anchors?.solana?.signature ?? null,
558
+ slot: input.head?.anchors?.solana?.slot ?? null,
559
+ cluster: input.head?.anchors?.solana?.cluster ?? null,
560
+ memo: input.head?.anchors?.solana?.memo ?? null,
561
+ fee_payer: input.head?.anchors?.solana?.fee_payer ?? null,
562
+ reason: undefined,
563
+ };
564
+ const base = {
565
+ checked: false,
566
+ valid: false,
567
+ tx: input.head?.anchors?.base?.tx || input.head?.anchor?.tx || input.head?.anchor_tx || null,
568
+ chain_id: input.head?.anchors?.base?.chain_id ?? input.head?.anchor?.chain_id ?? null,
569
+ from: input.head?.anchors?.base?.from ?? null,
570
+ reason: undefined,
571
+ };
572
+ const signedFrom = input.head?.anchors?.base?.from || null;
573
+ const signedPayer = input.head?.anchors?.solana?.fee_payer || null;
574
+ let walletFailed = anchorWalletsInvalid;
575
+ if (signedFrom && !(0, anchor_trust_js_1.walletListed)(walletList.base, signedFrom, 'base')) {
576
+ walletFailed = true;
577
+ errors.push('anchor_sender_unlisted');
578
+ }
579
+ if (signedPayer && !(0, anchor_trust_js_1.walletListed)(walletList.solana, signedPayer, 'solana')) {
580
+ walletFailed = true;
581
+ errors.push('fee_payer_unlisted');
582
+ }
583
+ const claimsAnchored = input.inclusion?.status === 'anchored' || input.inclusion?.anchor_status === 'anchored';
584
+ const headHasAnchor = Boolean(solanaSignature(input.head) || baseTxHash(input.head));
585
+ if (!headHasAnchor && (claimsAnchored || (pendingLeaf && inclusionReason === 'pending_anchor'))) {
586
+ errors.push('head_not_anchored');
587
+ }
588
+ if (pendingLeaf && inclusionReason === 'pending_anchor') {
589
+ solana.reason = 'pending_anchor';
590
+ base.reason = 'pending_anchor';
591
+ }
592
+ else if (!inclusionValid) {
593
+ solana.reason = 'inclusion_failed';
594
+ base.reason = 'inclusion_failed';
595
+ }
596
+ else if (!headTrust.ok || walletFailed) {
597
+ const reason = !headTrust.ok
598
+ ? (headTrust.reason || 'head_signature_invalid')
599
+ : (errors.find((code) => code.startsWith('anchor_') || code.startsWith('fee_payer')) || 'anchor_wallets_invalid');
600
+ solana.reason = solana.fee_payer && errors.includes('fee_payer_unlisted') ? 'fee_payer_unlisted' : reason;
601
+ base.reason = base.from && errors.includes('anchor_sender_unlisted') ? 'anchor_sender_unlisted' : reason;
602
+ }
603
+ else if (legacyHead) {
604
+ solana.reason = anchor_trust_js_1.LEGACY_HEAD_UNPINNED_SIGNER;
605
+ base.reason = anchor_trust_js_1.LEGACY_HEAD_UNPINNED_SIGNER;
606
+ }
607
+ else {
608
+ if (!signature) {
609
+ solana.reason = 'pending';
610
+ errors.push('solana_pending');
611
+ }
612
+ else if (!signedPayer) {
613
+ solana.reason = 'fee_payer_missing';
614
+ errors.push('fee_payer_missing');
615
+ }
616
+ else {
617
+ solana.checked = true;
618
+ try {
619
+ const fetchTx = input.fetchSolanaTx || ((sig, url) => (0, solana_payer_js_1.fetchSolanaTransaction)(sig, url));
620
+ const fetchGenesis = input.fetchGenesis || fetchSolanaGenesisHash;
621
+ const tx = await fetchTx(signature, solanaRpc);
622
+ if (!tx) {
623
+ solana.reason = 'tx_not_found';
624
+ }
625
+ else if (tx.meta?.err) {
626
+ solana.reason = 'tx_failed';
627
+ }
628
+ else {
629
+ const memos = extractMemos(tx);
630
+ const recorded = input.head.anchors?.solana?.memo || null;
631
+ const matched = memos.filter((memo) => {
632
+ const parsed = parseAnchorMemo(memo);
633
+ return Boolean(parsed && parsed.root === root && memo.includes(root) && (!recorded || memo === recorded));
634
+ });
635
+ const cluster = input.head.anchors?.solana?.cluster || null;
636
+ const expectedGenesis = cluster ? exports.SOLANA_GENESIS[cluster] : null;
637
+ if (matched.length === 0)
638
+ solana.reason = 'memo_mismatch';
639
+ else if (!cluster)
640
+ solana.reason = 'no_cluster';
641
+ else if (!expectedGenesis)
642
+ solana.reason = 'unknown_cluster';
643
+ else {
644
+ const genesis = await fetchGenesis(solanaRpc);
645
+ if (genesis !== expectedGenesis)
646
+ solana.reason = 'cluster_mismatch';
647
+ }
648
+ const headSlot = input.head.anchors?.solana?.slot;
649
+ if (!solana.reason && headSlot != null && tx.slot != null && Number(tx.slot) !== Number(headSlot)) {
650
+ solana.reason = 'slot_mismatch';
651
+ }
652
+ const payer = solanaFeePayer(tx);
653
+ if (!solana.reason && payer !== signedPayer) {
654
+ solana.reason = 'fee_payer_mismatch';
655
+ }
656
+ if (!solana.reason) {
657
+ solana.valid = true;
658
+ solana.memo = matched[0];
659
+ if (tx.slot != null)
660
+ solana.slot = tx.slot;
661
+ }
662
+ }
663
+ }
664
+ catch (err) {
665
+ solana.reason = err instanceof Error ? err.message : 'solana_rpc_error';
666
+ }
667
+ if (!solana.valid && solana.reason)
668
+ errors.push(`solana:${solana.reason}`);
669
+ }
670
+ if (!txHash) {
671
+ base.reason = 'pending';
672
+ errors.push('base_pending');
673
+ }
674
+ else if (!signedFrom) {
675
+ base.reason = 'anchor_sender_missing';
676
+ errors.push('anchor_sender_missing');
677
+ }
678
+ else {
679
+ base.checked = true;
680
+ try {
681
+ const fetchBase = input.fetchBaseTx || fetchBaseAnchorTransaction;
682
+ const tx = await fetchBase(txHash, baseRpc);
683
+ const expectedChain = input.head.anchors?.base?.chain_id || input.head.anchor?.chain_id || exports.BASE_MAINNET_CHAIN_ID;
684
+ if (!tx) {
685
+ base.reason = 'tx_not_found';
686
+ }
687
+ else if (tx.chainId != null && Number(tx.chainId) !== Number(expectedChain)) {
688
+ base.reason = 'chain_mismatch';
689
+ base.chain_id = Number(tx.chainId);
690
+ }
691
+ else {
692
+ const inputData = String(tx.input || '').toLowerCase().replace(/^0x/, '');
693
+ if (inputData !== root)
694
+ base.reason = 'calldata_mismatch';
695
+ else if (!tx.from || !(0, anchor_trust_js_1.sameBaseAddress)(tx.from, signedFrom))
696
+ base.reason = 'sender_mismatch';
697
+ else {
698
+ base.valid = true;
699
+ base.tx = tx.hash || txHash;
700
+ base.chain_id = tx.chainId ?? expectedChain;
701
+ }
702
+ }
703
+ }
704
+ catch (err) {
705
+ base.reason = err instanceof Error ? err.message : 'base_rpc_error';
706
+ }
707
+ if (!base.valid && base.reason)
708
+ errors.push(`base:${base.reason}`);
709
+ }
710
+ }
711
+ const pinInput = input.issuerPin || {};
712
+ const issuerPin = (0, issuer_pin_js_1.assessIssuerPin)({
713
+ ...pinInput,
714
+ receipt: pinInput.receipt ?? input.receipt,
715
+ head: pinInput.head ?? input.head,
716
+ required: pinInput.required === true || input.issuerPin != null,
717
+ });
718
+ if (issuerPin.checked && !issuerPin.ok && issuerPin.code)
719
+ errors.push(issuerPin.code);
720
+ const pinFailed = issuerPin.checked && !issuerPin.ok;
721
+ let staleReason;
722
+ const claimsAnchor = Boolean(solanaSignature(input.head) || baseTxHash(input.head));
723
+ if (input.checkNewestAnchor === true && claimsAnchor) {
724
+ const payerOnList = Boolean(signedPayer && (0, anchor_trust_js_1.walletListed)(walletList.solana, signedPayer, 'solana'));
725
+ if (!signedPayer || !root || !payerOnList) {
726
+ staleReason = 'newest_anchor_unavailable';
727
+ }
728
+ else {
729
+ try {
730
+ const found = input.fetchNewestAnchor
731
+ ? await input.fetchNewestAnchor(signedPayer, solanaRpc)
732
+ : await fetchNewestSolanaAnchorRoot(signedPayer, solanaRpc, input.fetchSolanaTx);
733
+ if (!found?.root)
734
+ staleReason = 'newest_anchor_unavailable';
735
+ else if (found.root !== root)
736
+ staleReason = 'stale_head';
737
+ }
738
+ catch {
739
+ staleReason = 'newest_anchor_unavailable';
740
+ }
741
+ }
742
+ if (staleReason)
743
+ errors.push(staleReason);
744
+ }
745
+ let overall;
746
+ const anchorSideFailed = (solana.reason && solana.reason !== 'pending' && solana.reason !== 'pending_anchor')
747
+ || (base.reason && base.reason !== 'pending' && base.reason !== 'pending_anchor');
748
+ const pendingOk = pendingLeaf
749
+ && inclusionReason === 'pending_anchor'
750
+ && headTrust.ok
751
+ && !walletFailed
752
+ && !legacyHead
753
+ && !epochReason
754
+ && !staleReason
755
+ && !pinFailed
756
+ && headHasAnchor
757
+ && !errors.includes('head_not_anchored');
758
+ // A receipt that is not a leaf, and has no other anchor failure, is
759
+ // NOT_IN_LOG. Pre-log payments such as chit-1ebc5616 are this case.
760
+ // A pin downgrade or a bad head stays failed. The book row is not rewritten.
761
+ const chainAsideFromInclusion = (reason) => Boolean(reason && reason !== 'pending' && reason !== 'pending_anchor' && reason !== 'inclusion_failed');
762
+ const otherAnchorFailure = Boolean(epochReason)
763
+ || !headTrust.ok
764
+ || walletFailed
765
+ || legacyHead
766
+ || chainAsideFromInclusion(solana.reason)
767
+ || chainAsideFromInclusion(base.reason)
768
+ || Boolean(staleReason)
769
+ || errors.includes('head_not_anchored')
770
+ || pinFailed;
771
+ if (inclusionReason === 'not_in_tree' && !otherAnchorFailure)
772
+ overall = 'not_in_log';
773
+ else if (pendingOk)
774
+ overall = 'pending';
775
+ else if (!inclusionValid
776
+ || epochReason
777
+ || !headTrust.ok
778
+ || walletFailed
779
+ || legacyHead
780
+ || anchorSideFailed
781
+ || staleReason
782
+ || errors.includes('head_not_anchored')
783
+ || (solana.checked && !solana.valid)
784
+ || (base.checked && !base.valid)
785
+ || pinFailed)
786
+ overall = 'failed';
787
+ else if (solana.valid && base.valid)
788
+ overall = 'verified';
789
+ else
790
+ overall = 'partial';
791
+ let unloggedReason;
792
+ if (!inclusionValid && !epochReason) {
793
+ const listed = (0, epoch_js_1.unloggedReasonForTask)(input.epochRecord, taskId);
794
+ if (listed)
795
+ unloggedReason = listed.reason;
796
+ }
797
+ return {
798
+ overall,
799
+ root,
800
+ inclusion: {
801
+ valid: inclusionValid,
802
+ leaf: leafHex,
803
+ leaf_source: leafSource,
804
+ reason: inclusionReason,
805
+ ...(unloggedReason ? { unlogged_reason: unloggedReason } : {}),
806
+ ...(input.inclusion?.anchored_tree_size != null ? { anchored_tree_size: input.inclusion.anchored_tree_size } : {}),
807
+ ...(input.inclusion?.live_tree_size != null ? { live_tree_size: input.inclusion.live_tree_size } : {}),
808
+ },
809
+ solana,
810
+ base,
811
+ head_signature: {
812
+ checked: Boolean(input.head?.issuer_signature?.jws) || !headTrust.ok,
813
+ valid: headTrust.ok,
814
+ kid: headTrust.kid,
815
+ trust: headTrust.trust,
816
+ reason: headTrust.reason || undefined,
817
+ message: headTrust.message || undefined,
818
+ },
819
+ anchor_wallets: {
820
+ base_from: signedFrom,
821
+ solana_fee_payer: signedPayer,
822
+ sources: walletList.sources,
823
+ not_consulted: walletList.not_consulted,
824
+ },
825
+ proves: overall === 'pending'
826
+ ? ['This leaf is past the anchored tree size. It waits for the next anchor.']
827
+ : exports.ANCHOR_PROVES,
828
+ does_not_prove: doesNotProve,
829
+ errors,
830
+ issuer_pin: issuerPin,
831
+ };
832
+ }
833
+ //# sourceMappingURL=anchor-witness.js.map