@xfuel/verify 0.3.1 → 0.3.3

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 (56) hide show
  1. package/README.md +16 -6
  2. package/dist/anchor-trust.d.ts +129 -0
  3. package/dist/anchor-trust.d.ts.map +1 -0
  4. package/dist/anchor-trust.js +395 -0
  5. package/dist/anchor-trust.js.map +1 -0
  6. package/dist/anchor-witness.d.ts +114 -4
  7. package/dist/anchor-witness.d.ts.map +1 -1
  8. package/dist/anchor-witness.js +419 -32
  9. package/dist/anchor-witness.js.map +1 -1
  10. package/dist/carry-forward.d.ts +79 -0
  11. package/dist/carry-forward.d.ts.map +1 -0
  12. package/dist/carry-forward.js +184 -0
  13. package/dist/carry-forward.js.map +1 -0
  14. package/dist/cli.js +525 -41
  15. package/dist/cli.js.map +1 -1
  16. package/dist/epoch.d.ts +41 -7
  17. package/dist/epoch.d.ts.map +1 -1
  18. package/dist/epoch.js +143 -0
  19. package/dist/epoch.js.map +1 -1
  20. package/dist/index.d.ts +64 -6
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +338 -96
  23. package/dist/index.js.map +1 -1
  24. package/dist/issuer-history.d.ts +19 -0
  25. package/dist/issuer-history.d.ts.map +1 -1
  26. package/dist/issuer-history.js +131 -75
  27. package/dist/issuer-history.js.map +1 -1
  28. package/dist/issuer-pin.d.ts +126 -0
  29. package/dist/issuer-pin.d.ts.map +1 -0
  30. package/dist/issuer-pin.js +567 -0
  31. package/dist/issuer-pin.js.map +1 -0
  32. package/dist/preimage.d.ts +29 -0
  33. package/dist/preimage.d.ts.map +1 -1
  34. package/dist/preimage.js +162 -2
  35. package/dist/preimage.js.map +1 -1
  36. package/dist/receipt-lane.d.ts +1 -0
  37. package/dist/receipt-lane.d.ts.map +1 -1
  38. package/dist/receipt-lane.js +5 -2
  39. package/dist/receipt-lane.js.map +1 -1
  40. package/dist/refusal.d.ts +5 -2
  41. package/dist/refusal.d.ts.map +1 -1
  42. package/dist/refusal.js +28 -6
  43. package/dist/refusal.js.map +1 -1
  44. package/dist/request-binding.d.ts +7 -0
  45. package/dist/request-binding.d.ts.map +1 -0
  46. package/dist/request-binding.js +26 -0
  47. package/dist/request-binding.js.map +1 -0
  48. package/dist/row-hash.d.ts +22 -0
  49. package/dist/row-hash.d.ts.map +1 -0
  50. package/dist/row-hash.js +26 -0
  51. package/dist/row-hash.js.map +1 -0
  52. package/dist/v11-receipt.d.ts +21 -0
  53. package/dist/v11-receipt.d.ts.map +1 -0
  54. package/dist/v11-receipt.js +351 -0
  55. package/dist/v11-receipt.js.map +1 -0
  56. package/package.json +1 -1
@@ -1,13 +1,16 @@
1
1
  "use strict";
2
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;
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
4
  exports.leafHash = leafHash;
5
5
  exports.normalizeRoot = normalizeRoot;
6
+ exports.inclusionRoot = inclusionRoot;
6
7
  exports.verifyMerkleInclusion = verifyMerkleInclusion;
7
8
  exports.parseAnchorMemo = parseAnchorMemo;
8
9
  exports.extractMemos = extractMemos;
9
10
  exports.fetchSolanaGenesisHash = fetchSolanaGenesisHash;
10
11
  exports.fetchBaseAnchorTransaction = fetchBaseAnchorTransaction;
12
+ exports.fetchNewestSolanaAnchorRoot = fetchNewestSolanaAnchorRoot;
13
+ exports.solanaFeePayer = solanaFeePayer;
11
14
  exports.verifyAnchoredRoot = verifyAnchoredRoot;
12
15
  /**
13
16
  * Check a receipt leaf against a signed tree head, then against the two
@@ -18,8 +21,11 @@ exports.verifyAnchoredRoot = verifyAnchoredRoot;
18
21
  * the payment.
19
22
  */
20
23
  const node_crypto_1 = require("node:crypto");
24
+ const anchor_trust_js_1 = require("./anchor-trust.js");
21
25
  const base_payer_js_1 = require("./base-payer.js");
22
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");
23
29
  const solana_payer_js_1 = require("./solana-payer.js");
24
30
  exports.MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
25
31
  exports.MEMO_PROGRAM_ID_V1 = 'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo';
@@ -35,15 +41,18 @@ exports.SOLANA_GENESIS = {
35
41
  };
36
42
  exports.BASE_MAINNET_CHAIN_ID = 8453;
37
43
  exports.ANCHOR_PROVES = [
38
- 'This receipt leaf is inside the issuer append-only tree of the stated size, under the stated root.',
39
- 'The Solana transaction memo contains that root, and the RPC genesis hash is the cluster named on the head.',
40
- 'The Base transaction calldata is that same root, on chain id 8453.',
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.',
41
49
  ];
42
50
  exports.ANCHOR_DOES_NOT_PROVE = [
43
51
  'It does not prove the payment, the payer, the payee, or the model output.',
44
52
  'It does not prove a receipt appended after this tree size is inside the root.',
45
53
  'It does not prove the issuer included only honest rows. It proves this leaf hashes into the published root.',
46
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.',
47
56
  ];
48
57
  function sha256(buf) {
49
58
  return (0, node_crypto_1.createHash)('sha256').update(buf).digest();
@@ -58,19 +67,102 @@ function normalizeRoot(root) {
58
67
  const hex = String(root || '').replace(/^0x/, '').toLowerCase();
59
68
  return /^[0-9a-f]{64}$/.test(hex) ? hex : null;
60
69
  }
61
- /** RFC 6962-style inclusion. Same rule as the gateway tree. */
62
- function verifyMerkleInclusion(leaf, index, treeSize, rootHex, proof) {
63
- if (!Array.isArray(proof) || index < 0 || index >= treeSize)
64
- return false;
65
- let hash = new Uint8Array(leaf);
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);
66
90
  for (const step of proof) {
91
+ if (sn === 0)
92
+ return null;
67
93
  if (!step || !/^[0-9a-fA-F]{64}$/.test(step.hash))
68
- return false;
94
+ return null;
69
95
  const sib = Buffer.from(step.hash, 'hex');
70
- const next = step.position === 'left' ? nodeHash(sib, hash) : nodeHash(hash, sib);
71
- hash = new Uint8Array(next);
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);
72
147
  }
73
- return Buffer.from(hash).toString('hex') === String(rootHex).replace(/^0x/, '').toLowerCase();
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;
74
166
  }
75
167
  function memoIdentity(scope, day, root, prev) {
76
168
  if (!/^[A-Za-z0-9_-]{1,64}$/.test(scope))
@@ -180,18 +272,132 @@ async function fetchBaseAnchorTransaction(txHash, rpcUrl) {
180
272
  return {
181
273
  hash: tx.hash,
182
274
  input: tx.input,
275
+ from: tx.from,
183
276
  chainId: chainHex ? Number(chainHex) : undefined,
184
277
  };
185
278
  }
186
- function rowHashFrom(receipt, inclusion) {
187
- if (typeof receipt.row_hash === 'string')
188
- return { rowHash: receipt.row_hash, source: 'receipt' };
189
- if (typeof receipt.book_chain?.row_hash === 'string')
190
- return { rowHash: receipt.book_chain.row_hash, source: 'receipt' };
191
- if (typeof inclusion.row_hash === 'string')
192
- return { rowHash: inclusion.row_hash, source: 'inclusion' };
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
+ }
193
313
  return null;
194
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
+ }
195
401
  function solanaSignature(head) {
196
402
  const sol = head.anchors?.solana;
197
403
  if (!sol || sol.status === 'pending')
@@ -218,7 +424,24 @@ async function verifyAnchoredRoot(input) {
218
424
  let inclusionReason;
219
425
  const taskId = input.receipt?.task_id ? String(input.receipt.task_id) : '';
220
426
  const inclusionError = input.inclusion?.error;
221
- if (inclusionError === 'not_in_tree') {
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') {
222
445
  inclusionReason = 'not_in_tree';
223
446
  }
224
447
  else if (!taskId) {
@@ -239,7 +462,10 @@ async function verifyAnchoredRoot(input) {
239
462
  else {
240
463
  const row = rowHashFrom(input.receipt, input.inclusion);
241
464
  let leaf = null;
242
- if (row) {
465
+ if (row && 'reason' in row) {
466
+ inclusionReason = row.reason;
467
+ }
468
+ else if (row) {
243
469
  leaf = leafHash(Buffer.from(`${taskId}|${row.rowHash}`));
244
470
  leafSource = row.source;
245
471
  if (input.inclusion.leaf && input.inclusion.leaf.toLowerCase() !== leaf.toString('hex')) {
@@ -247,11 +473,6 @@ async function verifyAnchoredRoot(input) {
247
473
  leaf = null;
248
474
  }
249
475
  }
250
- else if (input.inclusion.leaf && /^[0-9a-fA-F]{64}$/.test(input.inclusion.leaf)) {
251
- leaf = Buffer.from(input.inclusion.leaf, 'hex');
252
- leafSource = 'inclusion';
253
- 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.');
254
- }
255
476
  else {
256
477
  inclusionReason = 'no_leaf';
257
478
  }
@@ -262,8 +483,9 @@ async function verifyAnchoredRoot(input) {
262
483
  inclusionReason = 'inclusion_failed';
263
484
  }
264
485
  }
265
- if (!inclusionValid && inclusionReason)
486
+ if (!(pendingLeaf && inclusionReason === 'pending_anchor') && !inclusionValid && inclusionReason) {
266
487
  errors.push(inclusionReason);
488
+ }
267
489
  let epochReason;
268
490
  if (input.head?.epoch == null) {
269
491
  const link = (0, epoch_js_1.verifyEpochLink)(input.head, null);
@@ -296,6 +518,35 @@ async function verifyAnchoredRoot(input) {
296
518
  }
297
519
  if (epochReason)
298
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');
299
550
  const solanaRpc = input.solanaRpcUrl || process.env.SOLANA_RPC_URL || solana_payer_js_1.SOLANA_RPC_URL;
300
551
  const baseRpc = input.baseRpcUrl || base_payer_js_1.BASE_RPC_URL;
301
552
  const signature = root && inclusionValid ? solanaSignature(input.head) : null;
@@ -307,6 +558,7 @@ async function verifyAnchoredRoot(input) {
307
558
  slot: input.head?.anchors?.solana?.slot ?? null,
308
559
  cluster: input.head?.anchors?.solana?.cluster ?? null,
309
560
  memo: input.head?.anchors?.solana?.memo ?? null,
561
+ fee_payer: input.head?.anchors?.solana?.fee_payer ?? null,
310
562
  reason: undefined,
311
563
  };
312
564
  const base = {
@@ -314,17 +566,53 @@ async function verifyAnchoredRoot(input) {
314
566
  valid: false,
315
567
  tx: input.head?.anchors?.base?.tx || input.head?.anchor?.tx || input.head?.anchor_tx || null,
316
568
  chain_id: input.head?.anchors?.base?.chain_id ?? input.head?.anchor?.chain_id ?? null,
569
+ from: input.head?.anchors?.base?.from ?? null,
317
570
  reason: undefined,
318
571
  };
319
- if (!inclusionValid) {
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) {
320
593
  solana.reason = 'inclusion_failed';
321
594
  base.reason = 'inclusion_failed';
322
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
+ }
323
607
  else {
324
608
  if (!signature) {
325
609
  solana.reason = 'pending';
326
610
  errors.push('solana_pending');
327
611
  }
612
+ else if (!signedPayer) {
613
+ solana.reason = 'fee_payer_missing';
614
+ errors.push('fee_payer_missing');
615
+ }
328
616
  else {
329
617
  solana.checked = true;
330
618
  try {
@@ -361,6 +649,10 @@ async function verifyAnchoredRoot(input) {
361
649
  if (!solana.reason && headSlot != null && tx.slot != null && Number(tx.slot) !== Number(headSlot)) {
362
650
  solana.reason = 'slot_mismatch';
363
651
  }
652
+ const payer = solanaFeePayer(tx);
653
+ if (!solana.reason && payer !== signedPayer) {
654
+ solana.reason = 'fee_payer_mismatch';
655
+ }
364
656
  if (!solana.reason) {
365
657
  solana.valid = true;
366
658
  solana.memo = matched[0];
@@ -379,6 +671,10 @@ async function verifyAnchoredRoot(input) {
379
671
  base.reason = 'pending';
380
672
  errors.push('base_pending');
381
673
  }
674
+ else if (!signedFrom) {
675
+ base.reason = 'anchor_sender_missing';
676
+ errors.push('anchor_sender_missing');
677
+ }
382
678
  else {
383
679
  base.checked = true;
384
680
  try {
@@ -396,6 +692,8 @@ async function verifyAnchoredRoot(input) {
396
692
  const inputData = String(tx.input || '').toLowerCase().replace(/^0x/, '');
397
693
  if (inputData !== root)
398
694
  base.reason = 'calldata_mismatch';
695
+ else if (!tx.from || !(0, anchor_trust_js_1.sameBaseAddress)(tx.from, signedFrom))
696
+ base.reason = 'sender_mismatch';
399
697
  else {
400
698
  base.valid = true;
401
699
  base.tx = tx.hash || txHash;
@@ -410,22 +708,111 @@ async function verifyAnchoredRoot(input) {
410
708
  errors.push(`base:${base.reason}`);
411
709
  }
412
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
+ }
413
745
  let overall;
414
- if (!inclusionValid || epochReason || (solana.checked && !solana.valid) || (base.checked && !base.valid))
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
+ if (pendingOk)
759
+ overall = 'pending';
760
+ else if (!inclusionValid
761
+ || epochReason
762
+ || !headTrust.ok
763
+ || walletFailed
764
+ || legacyHead
765
+ || anchorSideFailed
766
+ || staleReason
767
+ || errors.includes('head_not_anchored')
768
+ || (solana.checked && !solana.valid)
769
+ || (base.checked && !base.valid)
770
+ || pinFailed)
415
771
  overall = 'failed';
416
772
  else if (solana.valid && base.valid)
417
773
  overall = 'verified';
418
774
  else
419
775
  overall = 'partial';
776
+ let unloggedReason;
777
+ if (!inclusionValid && !epochReason) {
778
+ const listed = (0, epoch_js_1.unloggedReasonForTask)(input.epochRecord, taskId);
779
+ if (listed)
780
+ unloggedReason = listed.reason;
781
+ }
420
782
  return {
421
783
  overall,
422
784
  root,
423
- inclusion: { valid: inclusionValid, leaf: leafHex, leaf_source: leafSource, reason: inclusionReason },
785
+ inclusion: {
786
+ valid: inclusionValid,
787
+ leaf: leafHex,
788
+ leaf_source: leafSource,
789
+ reason: inclusionReason,
790
+ ...(unloggedReason ? { unlogged_reason: unloggedReason } : {}),
791
+ ...(input.inclusion?.anchored_tree_size != null ? { anchored_tree_size: input.inclusion.anchored_tree_size } : {}),
792
+ ...(input.inclusion?.live_tree_size != null ? { live_tree_size: input.inclusion.live_tree_size } : {}),
793
+ },
424
794
  solana,
425
795
  base,
426
- proves: exports.ANCHOR_PROVES,
796
+ head_signature: {
797
+ checked: Boolean(input.head?.issuer_signature?.jws) || !headTrust.ok,
798
+ valid: headTrust.ok,
799
+ kid: headTrust.kid,
800
+ trust: headTrust.trust,
801
+ reason: headTrust.reason || undefined,
802
+ message: headTrust.message || undefined,
803
+ },
804
+ anchor_wallets: {
805
+ base_from: signedFrom,
806
+ solana_fee_payer: signedPayer,
807
+ sources: walletList.sources,
808
+ not_consulted: walletList.not_consulted,
809
+ },
810
+ proves: overall === 'pending'
811
+ ? ['This leaf is past the anchored tree size. It waits for the next anchor.']
812
+ : exports.ANCHOR_PROVES,
427
813
  does_not_prove: doesNotProve,
428
814
  errors,
815
+ issuer_pin: issuerPin,
429
816
  };
430
817
  }
431
818
  //# sourceMappingURL=anchor-witness.js.map