@twzrd1111/clawrouter-mpp 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,6 +3,45 @@
3
3
  MPP (Machine Payments Protocol) payment rails for [ClawRouter](https://github.com/BlockRunAI/ClawRouter):
4
4
  pay an MPP seller over **Tempo** or **Solana** instead of x402, under ClawRouter's own spend limits.
5
5
 
6
+ ## Upgrading to 0.5.0 from 0.4.0
7
+
8
+ This release adds opt-in Solana Echo purchase binding (#21). Omit `echo` to preserve existing
9
+ behavior. `echo: {}` offers binding and pays unbound when a seller omits or changes the reflected
10
+ ID; `echo: { requireBinding: true }` refuses those challenges before signing.
11
+
12
+ - Each call sends fresh 16-byte buyer entropy as `X-Buyer-Nonce: <32 hex>`. A cooperating seller
13
+ reflects it as `request.externalId: "twzrd:p:<nonce>"`, which the existing SDK writes as a memo.
14
+ - With `receiptVerification` enabled, a finalized payment passing all chain checks can set
15
+ `receipt_reference_purchase_bound: true` only when exactly one SPL Memo (v1 and v2 counted
16
+ together) matches the retained buyer expectation byte for byte. Malformed UTF-8, a leading BOM,
17
+ or multiple memos cannot silently match.
18
+ - Identical complete challenges share one credential and one spend record; binding remains per
19
+ call. The local nonce is not part of the shared-signing cache key.
20
+ - Fulfillment schema stays `twzrd.mpp.fulfillment.v3`; core peer floor stays `>=0.12.309`.
21
+ `response_bytes` and `body_sha256` are existing fields, first shipped in 0.3.0.
22
+
23
+ Seller reflection is a separate integration. This release does not deploy a seller, verify Tempo
24
+ receipts, prove model execution, or supply a durable replay index. The existing in-memory indexes
25
+ forget on restart; cross-worker uniqueness requires caller-supplied durable, atomic storage.
26
+ See the [seller wire contract](https://github.com/twzrd-sol/ClawRouter-1/blob/899295ef627032260dbff363bbb012d446c9ecc6/docs/echo-protocol.md).
27
+
28
+ ## Upgrading to 0.4.0 from 0.3.0
29
+
30
+ This release includes the standalone Solana receipt verifier (#16), fulfillment wiring (#18),
31
+ and the per-RPC deadline (#19). Verification is opt-in and does not verify Tempo receipts.
32
+ Purchase binding remains `null/no_buyer_nonce`; Echo and a durable reference index are not supplied
33
+ by this release. The reference index is caller-supplied and its operations have no built-in timeout.
34
+
35
+ - Fulfillment rows now use `twzrd.mpp.fulfillment.v3`. v3 adds eight `receipt_reference_*`
36
+ verification fields (below); every other field is unchanged. Readers must accept the new schema.
37
+ The fields are null unless the Solana rail was given `receiptVerification`, so Tempo rows and
38
+ unconfigured Solana rows carry them as null ("not evaluated", never "clean").
39
+ - `createSolanaMppFetch({ receiptVerification: { verify } })` retains the transaction the buyer
40
+ signed and chain-verifies the seller's receipt reference before writing the row.
41
+ - Each `getTransaction` call made by the verifier has a deadline: `rpcTimeoutMs`, default 10000.
42
+ When it passes, the row records `null/rpc_unavailable` and the `signal` passed to the fetcher
43
+ aborts, so a hung RPC cannot hold the row. Pass that `signal` to `fetch()`.
44
+
6
45
  ## Upgrading from 0.2.0
7
46
 
8
47
  - Fulfillment rows now use `twzrd.mpp.fulfillment.v2`. Readers must accept the new schema to retain
@@ -74,12 +113,60 @@ intent match the challenge; it is not an on-chain verification. `seller_reported
74
113
  compares labels only and does not verify model identity. A failed local write does not change an
75
114
  already-paid response; `payFetch.flush()` surfaces persistence failures to the shutdown caller.
76
115
 
77
- Rows use schema `twzrd.mpp.fulfillment.v2`. `response_bytes` and `body_sha256` (lowercase hex
116
+ Rows use schema `twzrd.mpp.fulfillment.v3`. `response_bytes` and `body_sha256` (lowercase hex
78
117
  SHA-256) describe the complete response body as the recorder read it: a JSON body read through its
79
118
  clone, or an SSE body tapped to its end. Both are `null` when the recorder did not read the body,
80
119
  it exceeded 1 MiB, or the stream was cancelled or errored before its end; a partial body is never
81
120
  hashed. The digest is of bytes the seller sent and is not a seller attestation.
82
121
 
122
+ ### Receipt reference chain verification (Solana, opt-in)
123
+
124
+ ```ts
125
+ import { createReceiptChainVerifier, createSolanaMppFetch } from "@twzrd1111/clawrouter-mpp";
126
+
127
+ const verify = createReceiptChainVerifier({
128
+ cluster: "mainnet", // must match the rail's network; a mismatch throws at construction
129
+ getTransaction, // your finalized getTransaction (base64, maxSupportedTransactionVersion 0)
130
+ index, // a durable ReceiptReferenceIndex with an atomic bind
131
+ rpcTimeoutMs: 10_000, // per getTransaction call (the default); pass the given signal to fetch()
132
+ });
133
+ const payFetch = await createSolanaMppFetch({
134
+ signer,
135
+ rpcUrl,
136
+ network: "mainnet",
137
+ spend,
138
+ integration,
139
+ receiptVerification: { verify, finalityWaitMs: 30_000 },
140
+ });
141
+ ```
142
+
143
+ The rail keeps the base64 wire transaction from the SDK's `signed` event, the buyer clock when it
144
+ fired, and a buyer-generated purchase id (every send of one credential is one purchase). After a
145
+ paid response, the recorder passes the seller's `valid_format` receipt reference with those facts to
146
+ the verifier and writes its result into the row:
147
+
148
+ - `receipt_reference_chain_verified` (`true`, `false`, or `null`), `receipt_reference_finalized_slot`,
149
+ `receipt_reference_mismatch` (why `false`), and `receipt_reference_unverified_reason` (why `null`;
150
+ `verifier_error` when the verifier threw).
151
+ - `receipt_reference_purchase_bound` and its reason. With opt-in `echo: { requireBinding: true }`
152
+ and a cooperating seller, one exactly matching buyer memo can establish binding after chain
153
+ verification. Otherwise it remains unbound; see `docs/echo-protocol.md`. Chain verification alone
154
+ does not mean the payment belongs to this purchase alone.
155
+ - `receipt_reference_local_acceptance` (`accepted`, `refused`, or `null`) and
156
+ `receipt_reference_refusal_reason` (`reference_reused`, `stale_by_local_clock`): this sink's
157
+ policy, which never changes a chain field.
158
+
159
+ Verification never delays or changes the response. It delays only the row and `flush()`.
160
+ `finalityWaitMs` (default 0) re-checks a reference that is not finalized yet every `finalityPollMs`
161
+ (default 2000); with the default, an unfinalized reference is recorded as `null/not_finalized`.
162
+ `finalityWaitMs` is not an RPC timeout: each `getTransaction` call has its own deadline,
163
+ `rpcTimeoutMs` on the verifier (default 10000). When it passes, the fetcher's `signal` aborts and
164
+ the reference is recorded as `null/rpc_unavailable`. A row therefore waits at most about
165
+ `finalityWaitMs + rpcTimeoutMs` on the chain. The reference index has no deadline, so a durable
166
+ index must bound its own calls; `flush({ timeoutMs })` bounds only how long the caller waits.
167
+ No RPC URL ships with the package. `InMemoryReceiptReferenceIndex` forgets on restart and is for
168
+ tests. See `docs/receipt-chain-verify.md` for each check.
169
+
83
170
  This candidate requires `@twzrd1111/clawrouter >=0.12.309`, including deferred streaming headers.
84
171
  The fork is the release target. Upstream PRs #545 and #546 are independent of this release.
85
172
 
package/dist/index.d.ts CHANGED
@@ -3,7 +3,138 @@ import { PayFetch, SpendControl, SpendRecord } from '@twzrd1111/clawrouter';
3
3
  export { PayFetch } from '@twzrd1111/clawrouter';
4
4
  import { KeyPairSigner } from '@solana/kit';
5
5
 
6
- declare const MPP_FULFILLMENT_RECORD_SCHEMA = "twzrd.mpp.fulfillment.v2";
6
+ declare const MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
7
+ declare const MEMO_V1_PROGRAM_ID = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo";
8
+ type BindingInstruction = {
9
+ program: string;
10
+ data: Uint8Array;
11
+ };
12
+ type PurchaseBoundOutcome = {
13
+ purchase_bound: boolean | null;
14
+ purchase_bound_reason: "no_buyer_nonce" | "nonce_mismatch" | "chain_unverified" | null;
15
+ };
16
+ /** An expected buyer nonce binds only through one exactly matching, valid UTF-8 SPL Memo. */
17
+ declare function evaluatePurchaseBinding(instructions: readonly BindingInstruction[], expectedExternalId: string | null | undefined): PurchaseBoundOutcome;
18
+ /** A blockhash stays valid for about 150 slots, roughly 60-90 s. */
19
+ declare const LANDING_WINDOW_SECONDS = 90;
20
+ declare const CLUSTER_IDS: {
21
+ readonly mainnet: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
22
+ readonly devnet: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
23
+ };
24
+ type SolanaCluster = keyof typeof CLUSTER_IDS;
25
+ type TrustedMint = {
26
+ decimals: number;
27
+ tokenPrograms: readonly string[];
28
+ };
29
+ /** Classic SPL Token USD mints with 6 decimals: USDC mainnet, USDC devnet, USDT mainnet. */
30
+ declare const DEFAULT_TRUSTED_USD_MINTS: Readonly<Record<string, TrustedMint>>;
31
+ type ChainMismatch = "bad_reference_format" | "wrong_network" | "signature_mismatch" | "message_mismatch" | "buyer_signature_missing" | "untrusted_token_program" | "untrusted_mint" | "decimals_mismatch" | "tx_failed" | "no_matching_transfer" | "multiple_matching_transfers" | "amount_mismatch" | "mint_mismatch" | "payee_mismatch" | "payer_mismatch";
32
+ type ChainUnverifiedReason = "rpc_unavailable" | "not_finalized" | "no_fetcher" | "non_solana" | "no_signed_transaction" | "splits_unsupported" | "unnormalized_terms" | "unsupported_message_format"
33
+ /** The verifier itself threw; recorded by the fulfillment hook, never returned by `verify`. */
34
+ | "verifier_error";
35
+ type ReceiptChainVerification = {
36
+ receipt_reference_chain_verified: boolean | null;
37
+ receipt_reference_finalized_slot: number | null;
38
+ receipt_reference_mismatch: ChainMismatch | null;
39
+ receipt_reference_unverified_reason: ChainUnverifiedReason | null;
40
+ receipt_reference_purchase_bound: boolean | null;
41
+ receipt_reference_purchase_bound_reason: "nonce_mismatch" | "no_buyer_nonce" | "chain_unverified" | null;
42
+ receipt_reference_local_acceptance: "accepted" | "refused" | null;
43
+ receipt_reference_refusal_reason: "reference_reused" | "stale_by_local_clock" | null;
44
+ };
45
+ /** Compiled inner instruction as returned by getTransaction with `encoding: "base64"`. */
46
+ type RpcInnerInstruction = {
47
+ programIdIndex: number;
48
+ accounts: number[];
49
+ data: string;
50
+ };
51
+ /** The subset of a `getTransaction` (`encoding: "base64"`, finalized) result this verifier reads. */
52
+ type RpcTransaction = {
53
+ slot: number;
54
+ blockTime: number | null;
55
+ meta: {
56
+ err: unknown;
57
+ innerInstructions?: {
58
+ index: number;
59
+ instructions: RpcInnerInstruction[];
60
+ }[] | null;
61
+ } | null;
62
+ transaction: [string, "base64"] | readonly [string, string];
63
+ };
64
+ /**
65
+ * Must call getTransaction(signature, {commitment: "finalized", encoding: "base64",
66
+ * maxSupportedTransactionVersion: 0}) on the configured cluster and return its result (null when
67
+ * not found or not finalized). Throw on transport failure. `signal` aborts when the verifier's
68
+ * `rpcTimeoutMs` deadline passes; pass it to fetch() so the request is cancelled, not abandoned.
69
+ */
70
+ type GetFinalizedTransaction = (signature: string, options?: {
71
+ signal?: AbortSignal;
72
+ }) => Promise<RpcTransaction | null>;
73
+ /**
74
+ * Durable, atomic reference -> purchase_id index. `bind` returns true when this purchase owns the
75
+ * reference (newly bound or already bound to the same purchase) and false when another purchase
76
+ * holds it; exactly one concurrent binder may win. `owner` reads without binding.
77
+ */
78
+ type ReceiptReferenceIndex = {
79
+ bind(reference: string, purchaseId: string): Promise<boolean>;
80
+ owner(reference: string): Promise<string | undefined>;
81
+ };
82
+ /** In-memory index for tests and single-process demos. It forgets on restart, so it is not durable. */
83
+ declare class InMemoryReceiptReferenceIndex implements ReceiptReferenceIndex {
84
+ #private;
85
+ bind(reference: string, purchaseId: string): Promise<boolean>;
86
+ owner(reference: string): Promise<string | undefined>;
87
+ }
88
+ type ReceiptChainVerifierConfig = {
89
+ cluster: SolanaCluster;
90
+ getTransaction?: GetFinalizedTransaction;
91
+ index: ReceiptReferenceIndex;
92
+ /** Trusted mint -> decimals and token programs. Defaults to DEFAULT_TRUSTED_USD_MINTS. */
93
+ trustedMints?: Readonly<Record<string, TrustedMint>>;
94
+ /** Default 600. Must exceed maxClockSkewSeconds + LANDING_WINDOW_SECONDS. */
95
+ maxAgeSeconds?: number;
96
+ /** Default 60. */
97
+ maxClockSkewSeconds?: number;
98
+ /**
99
+ * Deadline for one getTransaction call, in ms. Default 10000. When it passes, the fetcher's
100
+ * signal aborts and the reference is recorded as null/rpc_unavailable. The reference index is
101
+ * not covered: a durable index must bound its own calls.
102
+ */
103
+ rpcTimeoutMs?: number;
104
+ };
105
+ /** Default deadline for one getTransaction call. */
106
+ declare const DEFAULT_RPC_TIMEOUT_MS = 10000;
107
+ type ReceiptChainVerifyInput = {
108
+ /** Seller's receipt reference: the settled transaction signature. */
109
+ reference: string;
110
+ /** MPP payment method; only "solana" is verified. */
111
+ method: string;
112
+ /** Challenge terms as the seller sent them. */
113
+ network: string | null;
114
+ asset: string | null;
115
+ payTo: string | null;
116
+ amountAtomic: string | null;
117
+ /** True when the challenge carried split recipients. */
118
+ hasSplits?: boolean;
119
+ /** The buyer's own address. */
120
+ payer: string;
121
+ /** Base64 wire transaction from the rail's `signed` event, or null when not retained. */
122
+ signedTransaction: string | null;
123
+ /** Buyer clock (unix seconds) when the signed event fired. */
124
+ signedAtUnix: number | null;
125
+ /** Buyer-generated id of the logical purchase (not the seller's challenge id). */
126
+ purchaseId: string;
127
+ /** Buyer-generated Echo expectation, retained only after exact pre-sign reflection. */
128
+ expectedExternalId?: string | null;
129
+ };
130
+ /** A built verifier. `cluster` lets a rail refuse a verifier configured for another cluster. */
131
+ type ReceiptChainVerify = ((input: ReceiptChainVerifyInput) => Promise<ReceiptChainVerification>) & {
132
+ readonly cluster: SolanaCluster;
133
+ };
134
+ /** Build a verifier. Throws when the age policy cannot hold an honest payment. */
135
+ declare function createReceiptChainVerifier(config: ReceiptChainVerifierConfig): ReceiptChainVerify;
136
+
137
+ declare const MPP_FULFILLMENT_RECORD_SCHEMA = "twzrd.mpp.fulfillment.v3";
7
138
  declare const MPP_FULFILLMENT_MAX_BODY_BYTES: number;
8
139
  type MppPaymentFacts = {
9
140
  challengeId: string;
@@ -14,7 +145,14 @@ type MppPaymentFacts = {
14
145
  asset: string | null;
15
146
  amountAtomic: string | null;
16
147
  };
17
- type MppFulfillmentRecord = {
148
+ /**
149
+ * Chain verification of the seller's receipt reference. Every field is null when no verifier ran
150
+ * (none configured, a non-Solana rail, or no valid-format receipt): null means "not evaluated",
151
+ * never "clean". See docs/receipt-chain-verify.md for each field.
152
+ */
153
+ type MppReceiptReferenceVerification = ReceiptChainVerification;
154
+ declare const RECEIPT_REFERENCE_NOT_EVALUATED: Readonly<MppReceiptReferenceVerification>;
155
+ type MppFulfillmentRecord = MppReceiptReferenceVerification & {
18
156
  schema: typeof MPP_FULFILLMENT_RECORD_SCHEMA;
19
157
  at_unix_ms: number;
20
158
  resource_origin: string | null;
@@ -29,7 +167,10 @@ type MppFulfillmentRecord = {
29
167
  receipt_state: "valid_format" | "missing" | "invalid_format" | "unavailable";
30
168
  receipt_status: string | null;
31
169
  receipt_method: string | null;
32
- /** Seller-supplied MPP receipt reference (EVM hash or Solana signature); not chain-verified here. */
170
+ /**
171
+ * Seller-supplied MPP receipt reference (EVM hash or Solana signature). Chain-verified only when
172
+ * the rail was given a verifier; see the receipt_reference_* verification fields.
173
+ */
33
174
  receipt_reference: string | null;
34
175
  receipt_timestamp: string | null;
35
176
  requested_model: string | null;
@@ -145,6 +286,15 @@ declare function createMppFetch(account: Account, base: typeof fetch | undefined
145
286
 
146
287
  declare const DEFAULT_SOLANA_USD_MINTS: ReadonlySet<string>;
147
288
  type SolanaNetwork = "mainnet" | "devnet" | "localnet";
289
+ type EchoPolicy = {
290
+ requireBinding?: boolean;
291
+ };
292
+ type EchoBindingContext = {
293
+ buyerNonce: string;
294
+ expectedExternalId: string;
295
+ };
296
+ declare function createEchoContext(): EchoBindingContext;
297
+ declare function assertEchoPreSign(challengeExternalId: unknown, context: EchoBindingContext, policy?: EchoPolicy): "bound" | "unbound";
148
298
  type SolanaMppFetchOptions = {
149
299
  signer: KeyPairSigner;
150
300
  rpcUrl: string;
@@ -155,6 +305,25 @@ type SolanaMppFetchOptions = {
155
305
  integration: MppIntegration;
156
306
  /** Injectable transport; defaults to fetch. The SDK's RPC transport is separate. */
157
307
  fetch?: typeof fetch;
308
+ /**
309
+ * Chain-verify each seller receipt reference before its fulfillment row is written. Off when
310
+ * omitted (the row's receipt_reference_* verification fields stay null). Never delays the response.
311
+ */
312
+ receiptVerification?: SolanaReceiptVerificationOptions;
313
+ /** Opt in to buyer nonce reflection. Omit to preserve existing seller externalId behavior. */
314
+ echo?: EchoPolicy;
315
+ };
316
+ type SolanaReceiptVerificationOptions = {
317
+ /** From createReceiptChainVerifier, built for the same cluster as `network`. */
318
+ verify: ReceiptChainVerify;
319
+ /**
320
+ * How long to keep re-checking a reference that is not finalized yet, in ms. Default 0: one check,
321
+ * and a not-yet-finalized reference is recorded as null/not_finalized. This delays only the row
322
+ * (and flush()), never the response.
323
+ */
324
+ finalityWaitMs?: number;
325
+ /** Delay between finality re-checks, in ms. Default 2000. */
326
+ finalityPollMs?: number;
158
327
  };
159
328
  declare function createSolanaMppFetch(opts: SolanaMppFetchOptions): Promise<MppPayFetch>;
160
329
 
@@ -245,5 +414,5 @@ declare function fulfillPaidProof<T>(opts: {
245
414
  upstream: () => Promise<T>;
246
415
  }): Promise<T>;
247
416
 
248
- export { CreditLedger, DEFAULT_SOLANA_USD_MINTS, DEFAULT_TEMPO_USD_ASSETS, MPP_FULFILLMENT_MAX_BODY_BYTES, MPP_FULFILLMENT_RECORD_SCHEMA, MppFlushTimeoutError, MppGateError, MppPaidNoCompletionError, ReplayRejectedError, SpentProofLedger, createMppFetch, createSolanaMppFetch, creditPaidNoCompletion, fulfillPaidProof, quoteFromChallenge, readCredits, readPaidCallEvidence };
249
- export type { CreditEntry, MppBodyDigest, MppFetchOptions, MppFlushOptions, MppFulfillmentRecord, MppFulfillmentSink, MppGateChallenge, MppGateDecision, MppGateEvaluator, MppIntegration, MppPayFetch, MppPaymentFacts, SolanaMppFetchOptions };
417
+ export { CreditLedger, DEFAULT_RPC_TIMEOUT_MS, DEFAULT_SOLANA_USD_MINTS, DEFAULT_TEMPO_USD_ASSETS, DEFAULT_TRUSTED_USD_MINTS, InMemoryReceiptReferenceIndex, LANDING_WINDOW_SECONDS, MEMO_PROGRAM_ID, MEMO_V1_PROGRAM_ID, MPP_FULFILLMENT_MAX_BODY_BYTES, MPP_FULFILLMENT_RECORD_SCHEMA, MppFlushTimeoutError, MppGateError, MppPaidNoCompletionError, RECEIPT_REFERENCE_NOT_EVALUATED, ReplayRejectedError, SpentProofLedger, assertEchoPreSign, createEchoContext, createMppFetch, createReceiptChainVerifier, createSolanaMppFetch, creditPaidNoCompletion, evaluatePurchaseBinding, fulfillPaidProof, quoteFromChallenge, readCredits, readPaidCallEvidence };
418
+ export type { ChainMismatch, ChainUnverifiedReason, CreditEntry, EchoBindingContext, EchoPolicy, GetFinalizedTransaction, MppBodyDigest, MppFetchOptions, MppFlushOptions, MppFulfillmentRecord, MppFulfillmentSink, MppGateChallenge, MppGateDecision, MppGateEvaluator, MppIntegration, MppPayFetch, MppPaymentFacts, MppReceiptReferenceVerification, PurchaseBoundOutcome, ReceiptChainVerification, ReceiptChainVerifierConfig, ReceiptChainVerify, ReceiptChainVerifyInput, ReceiptReferenceIndex, RpcTransaction, SolanaCluster, SolanaMppFetchOptions, SolanaReceiptVerificationOptions, TrustedMint };
package/dist/index.js CHANGED
@@ -280,8 +280,22 @@ async function evaluateMppGate(evaluator, challenge) {
280
280
 
281
281
  // src/mpp-fulfillment-record.ts
282
282
  import { createHash as createHash2 } from "crypto";
283
- var MPP_FULFILLMENT_RECORD_SCHEMA = "twzrd.mpp.fulfillment.v2";
283
+ var MPP_FULFILLMENT_RECORD_SCHEMA = "twzrd.mpp.fulfillment.v3";
284
284
  var MPP_FULFILLMENT_MAX_BODY_BYTES = 1024 * 1024;
285
+ var RECEIPT_REFERENCE_NOT_EVALUATED = Object.freeze({
286
+ receipt_reference_chain_verified: null,
287
+ receipt_reference_finalized_slot: null,
288
+ receipt_reference_mismatch: null,
289
+ receipt_reference_unverified_reason: null,
290
+ receipt_reference_purchase_bound: null,
291
+ receipt_reference_purchase_bound_reason: null,
292
+ receipt_reference_local_acceptance: null,
293
+ receipt_reference_refusal_reason: null
294
+ });
295
+ var VERIFIER_ERROR = Object.freeze({
296
+ ...RECEIPT_REFERENCE_NOT_EVALUATED,
297
+ receipt_reference_unverified_reason: "verifier_error"
298
+ });
285
299
  var NO_BODY_DIGEST = { response_bytes: null, body_sha256: null };
286
300
  function asRecord(value) {
287
301
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -485,6 +499,15 @@ async function emitRecord(input, receipt, requestValue, responseValue, sink, dig
485
499
  const res = asRecord(responseValue);
486
500
  const requestedModel = string(req?.model);
487
501
  const sellerReportedModel = string(sellerModelOverride) ?? string(res?.model);
502
+ const reference = string(receipt.value?.reference);
503
+ let verification = RECEIPT_REFERENCE_NOT_EVALUATED;
504
+ if (receipt.state === "valid_format" && reference && input.verifyReceiptReference) {
505
+ try {
506
+ verification = await input.verifyReceiptReference(reference);
507
+ } catch {
508
+ verification = VERIFIER_ERROR;
509
+ }
510
+ }
488
511
  const row = {
489
512
  schema: MPP_FULFILLMENT_RECORD_SCHEMA,
490
513
  at_unix_ms: Date.now(),
@@ -500,14 +523,22 @@ async function emitRecord(input, receipt, requestValue, responseValue, sink, dig
500
523
  receipt_state: receipt.state,
501
524
  receipt_status: string(receipt.value?.status),
502
525
  receipt_method: string(receipt.value?.method),
503
- receipt_reference: string(receipt.value?.reference),
526
+ receipt_reference: reference,
504
527
  receipt_timestamp: string(receipt.value?.timestamp),
505
528
  requested_model: requestedModel,
506
529
  seller_reported_model: sellerReportedModel,
507
530
  seller_reported_model_matches_request: requestedModel && sellerReportedModel ? requestedModel.toLowerCase() === sellerReportedModel.toLowerCase() : null,
508
531
  gate: input.gate ?? null,
509
532
  response_bytes: digest.response_bytes,
510
- body_sha256: digest.body_sha256
533
+ body_sha256: digest.body_sha256,
534
+ receipt_reference_chain_verified: verification.receipt_reference_chain_verified,
535
+ receipt_reference_finalized_slot: verification.receipt_reference_finalized_slot,
536
+ receipt_reference_mismatch: verification.receipt_reference_mismatch,
537
+ receipt_reference_unverified_reason: verification.receipt_reference_unverified_reason,
538
+ receipt_reference_purchase_bound: verification.receipt_reference_purchase_bound,
539
+ receipt_reference_purchase_bound_reason: verification.receipt_reference_purchase_bound_reason,
540
+ receipt_reference_local_acceptance: verification.receipt_reference_local_acceptance,
541
+ receipt_reference_refusal_reason: verification.receipt_reference_refusal_reason
511
542
  };
512
543
  await sink(row);
513
544
  }
@@ -881,6 +912,7 @@ function createMppFetch(account, base = fetch, opts) {
881
912
  }
882
913
 
883
914
  // src/mpp-solana-fetch.ts
915
+ import { randomBytes, randomUUID } from "crypto";
884
916
  import {
885
917
  assertSpendPolicyAllows as assertSpendPolicyAllows2,
886
918
  SpendPolicyError as SpendPolicyError3
@@ -890,17 +922,68 @@ var DEFAULT_SOLANA_USD_MINTS = /* @__PURE__ */ new Set([
890
922
  "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
891
923
  "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
892
924
  ]);
925
+ function createEchoContext() {
926
+ const buyerNonce = randomBytes(16).toString("hex");
927
+ return { buyerNonce, expectedExternalId: `twzrd:p:${buyerNonce}` };
928
+ }
929
+ function assertEchoPreSign(challengeExternalId, context, policy = {}) {
930
+ if (challengeExternalId === void 0 || challengeExternalId === null) {
931
+ if (policy.requireBinding)
932
+ throw new SpendPolicyError3("REJECTED_UNBOUND_CHALLENGE: Seller failed to echo buyer nonce");
933
+ return "unbound";
934
+ }
935
+ if (challengeExternalId !== context.expectedExternalId) {
936
+ if (policy.requireBinding)
937
+ throw new SpendPolicyError3("SECURITY_VIOLATION: Challenge externalId mismatch");
938
+ return "unbound";
939
+ }
940
+ return "bound";
941
+ }
893
942
  var CAIP2 = {
894
943
  mainnet: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
895
944
  devnet: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
896
945
  localnet: "solana:localnet"
897
946
  };
947
+ var MAX_TIMER_MS2 = 2147483647;
948
+ function verificationSettings(options, network) {
949
+ if (options === void 0) return void 0;
950
+ if (typeof options?.verify !== "function") {
951
+ throw new TypeError("receiptVerification.verify must come from createReceiptChainVerifier");
952
+ }
953
+ if (options.verify.cluster !== network) {
954
+ throw new TypeError(
955
+ `receiptVerification is built for ${String(options.verify.cluster)}, but the rail network is ${network}`
956
+ );
957
+ }
958
+ const waitMs = options.finalityWaitMs ?? 0;
959
+ const pollMs = options.finalityPollMs ?? 2e3;
960
+ for (const [name, value, min] of [
961
+ ["finalityWaitMs", waitMs, 0],
962
+ ["finalityPollMs", pollMs, 1]
963
+ ]) {
964
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > MAX_TIMER_MS2)
965
+ throw new TypeError(
966
+ `receiptVerification.${name} must be a number from ${min} to ${MAX_TIMER_MS2}`
967
+ );
968
+ }
969
+ return { verify: options.verify, waitMs, pollMs };
970
+ }
971
+ async function verifyUntilFinal(verify, waitMs, pollMs) {
972
+ const deadline = Date.now() + waitMs;
973
+ for (; ; ) {
974
+ const result = await verify();
975
+ if (result.receipt_reference_unverified_reason !== "not_finalized") return result;
976
+ if (Date.now() + pollMs > deadline) return result;
977
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
978
+ }
979
+ }
898
980
  var solanaChargeDeps = {
899
981
  load: () => loadOptional("@solana/mpp", SOLANA_MPP_RANGE, () => import("@solana/mpp/client"))
900
982
  };
901
983
  async function createSolanaMppFetch(opts) {
902
984
  assertMppIntegration(opts?.integration, "Solana");
903
985
  const integration = Object.freeze({ ...opts.integration });
986
+ const verification = verificationSettings(opts.receiptVerification, opts.network);
904
987
  const { charge, selectSolanaChargeChallengeFromResponse, resolveStablecoinMint } = await solanaChargeDeps.load();
905
988
  const base = opts.fetch ?? fetch;
906
989
  const usd = /* @__PURE__ */ new Set([...DEFAULT_SOLANA_USD_MINTS, ...opts.usdMints ?? []]);
@@ -931,6 +1014,8 @@ async function createSolanaMppFetch(opts) {
931
1014
  opts.spend,
932
1015
  async () => {
933
1016
  const request = new Request(input, cleanInit);
1017
+ const echoContext = opts.echo === void 0 ? void 0 : createEchoContext();
1018
+ if (echoContext) request.headers.set("X-Buyer-Nonce", echoContext.buyerNonce);
934
1019
  const requestForRecord = request.clone();
935
1020
  const probe = await base(request.clone());
936
1021
  if (probe.status !== 402) return probe;
@@ -958,6 +1043,7 @@ async function createSolanaMppFetch(opts) {
958
1043
  );
959
1044
  const payment = paymentFactsFromMppChallenge(challenge);
960
1045
  markPaymentChallenge(payment);
1046
+ const expectedExternalId = echoContext && assertEchoPreSign(challenge.request.externalId, echoContext, opts.echo) === "bound" ? echoContext.expectedExternalId : null;
961
1047
  const key = JSON.stringify([
962
1048
  challenge.id,
963
1049
  challenge.realm,
@@ -997,6 +1083,8 @@ async function createSolanaMppFetch(opts) {
997
1083
  let sessionSpendRecorded = false;
998
1084
  let record;
999
1085
  let payload = "";
1086
+ let signedTransaction = null;
1087
+ let signedAtUnix = null;
1000
1088
  try {
1001
1089
  const method = charge({
1002
1090
  signer: opts.signer,
@@ -1007,6 +1095,8 @@ async function createSolanaMppFetch(opts) {
1007
1095
  if (event.type !== "signed") return;
1008
1096
  signed = true;
1009
1097
  payload = event.transaction;
1098
+ signedTransaction = event.transaction;
1099
+ signedAtUnix = Math.floor(Date.now() / 1e3);
1010
1100
  if (!sessionSpendRecorded) {
1011
1101
  budgetContext?.recordSigned(budgetQuoteMicros);
1012
1102
  sessionSpendRecorded = true;
@@ -1032,12 +1122,18 @@ async function createSolanaMppFetch(opts) {
1032
1122
  });
1033
1123
  }
1034
1124
  }
1125
+ const splits = challenge.request.methodDetails.splits;
1035
1126
  return {
1036
1127
  credential,
1037
1128
  payload,
1038
1129
  record,
1039
1130
  gate: gateDecision,
1040
- payment
1131
+ payment,
1132
+ signedTransaction,
1133
+ signedAtUnix,
1134
+ purchaseId: randomUUID(),
1135
+ hasSplits: Array.isArray(splits) && splits.length > 0,
1136
+ expectedExternalId
1041
1137
  };
1042
1138
  } catch (err) {
1043
1139
  if (signed) {
@@ -1071,8 +1167,34 @@ async function createSolanaMppFetch(opts) {
1071
1167
  const response = await base(new Request(request, { headers }));
1072
1168
  const paid = currentPaidCall();
1073
1169
  if (response.status !== 402 && paid?.signed && paid.payment) {
1170
+ const terms = paid.payment;
1171
+ const verifyReceiptReference = verification ? (reference) => verifyUntilFinal(
1172
+ () => verification.verify({
1173
+ reference,
1174
+ method: terms.method,
1175
+ network: terms.network,
1176
+ asset: terms.asset,
1177
+ payTo: terms.payTo,
1178
+ amountAtomic: terms.amountAtomic,
1179
+ hasSplits: result.hasSplits,
1180
+ payer: opts.signer.address,
1181
+ signedTransaction: result.signedTransaction,
1182
+ signedAtUnix: result.signedAtUnix,
1183
+ purchaseId: result.purchaseId,
1184
+ // Binding belongs to this call, not merely a cached credential.
1185
+ expectedExternalId: result.expectedExternalId === echoContext?.expectedExternalId ? result.expectedExternalId : null
1186
+ }),
1187
+ verification.waitMs,
1188
+ verification.pollMs
1189
+ ) : void 0;
1074
1190
  return recordMppFulfillment(
1075
- { input: requestForRecord, response, payment: paid.payment, gate: paid.gate },
1191
+ {
1192
+ input: requestForRecord,
1193
+ response,
1194
+ payment: terms,
1195
+ gate: paid.gate,
1196
+ verifyReceiptReference
1197
+ },
1076
1198
  integration.onFulfillment,
1077
1199
  trackFulfillment
1078
1200
  );
@@ -1146,20 +1268,321 @@ async function fulfillPaidProof(opts) {
1146
1268
  if (!won) throw new ReplayRejectedError(opts.networkId, opts.txSignature, opts.challengeId);
1147
1269
  return opts.upstream();
1148
1270
  }
1271
+
1272
+ // src/receipt-chain-verify.ts
1273
+ var SOLANA_KIT_RANGE = "~6.10.0";
1274
+ var MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
1275
+ var MEMO_V1_PROGRAM_ID = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo";
1276
+ var MEMO_PROGRAMS = /* @__PURE__ */ new Set([MEMO_PROGRAM_ID, MEMO_V1_PROGRAM_ID]);
1277
+ function evaluatePurchaseBinding(instructions, expectedExternalId) {
1278
+ const unbound = { purchase_bound: null, purchase_bound_reason: "no_buyer_nonce" };
1279
+ const mismatch = { purchase_bound: false, purchase_bound_reason: "nonce_mismatch" };
1280
+ if (!expectedExternalId) return unbound;
1281
+ const memos = instructions.filter((ix) => MEMO_PROGRAMS.has(ix.program));
1282
+ if (memos.length === 0) return unbound;
1283
+ if (memos.length !== 1) return mismatch;
1284
+ try {
1285
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(memos[0].data);
1286
+ } catch {
1287
+ return mismatch;
1288
+ }
1289
+ return bytesEqual(memos[0].data, new TextEncoder().encode(expectedExternalId)) ? { purchase_bound: true, purchase_bound_reason: null } : mismatch;
1290
+ }
1291
+ var TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
1292
+ var TOKEN_2022_PROGRAM = "TokenzQdBNbLqP7VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
1293
+ var ASSOCIATED_TOKEN_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
1294
+ var KNOWN_TOKEN_PROGRAMS = /* @__PURE__ */ new Set([TOKEN_PROGRAM, TOKEN_2022_PROGRAM]);
1295
+ var IX_TRANSFER = 3;
1296
+ var IX_TRANSFER_CHECKED = 12;
1297
+ var LANDING_WINDOW_SECONDS = 90;
1298
+ var CLUSTER_IDS = {
1299
+ mainnet: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
1300
+ devnet: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"
1301
+ };
1302
+ var SYMBOL_MINTS = {
1303
+ mainnet: { USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" },
1304
+ devnet: { USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" }
1305
+ };
1306
+ var DEFAULT_TRUSTED_USD_MINTS = Object.freeze({
1307
+ EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: { decimals: 6, tokenPrograms: [TOKEN_PROGRAM] },
1308
+ "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU": { decimals: 6, tokenPrograms: [TOKEN_PROGRAM] },
1309
+ Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: { decimals: 6, tokenPrograms: [TOKEN_PROGRAM] }
1310
+ });
1311
+ var InMemoryReceiptReferenceIndex = class {
1312
+ #owners = /* @__PURE__ */ new Map();
1313
+ async bind(reference, purchaseId) {
1314
+ const current = this.#owners.get(reference);
1315
+ if (current === void 0) {
1316
+ this.#owners.set(reference, purchaseId);
1317
+ return true;
1318
+ }
1319
+ return current === purchaseId;
1320
+ }
1321
+ async owner(reference) {
1322
+ return this.#owners.get(reference);
1323
+ }
1324
+ };
1325
+ var DEFAULT_RPC_TIMEOUT_MS = 1e4;
1326
+ var MAX_TIMER_MS3 = 2147483647;
1327
+ var loadKit = () => loadOptional("@solana/kit", SOLANA_KIT_RANGE, () => import("@solana/kit"));
1328
+ function bytesEqual(a, b) {
1329
+ if (a.length !== b.length) return false;
1330
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
1331
+ return true;
1332
+ }
1333
+ function readU64(data, offset) {
1334
+ let value = 0n;
1335
+ for (let i = 7; i >= 0; i--) value = value << 8n | BigInt(data[offset + i]);
1336
+ return value;
1337
+ }
1338
+ function clusterOf(network) {
1339
+ if (network === "mainnet" || network === "mainnet-beta" || network === CLUSTER_IDS.mainnet)
1340
+ return "mainnet";
1341
+ if (network === "devnet" || network === CLUSTER_IDS.devnet) return "devnet";
1342
+ return void 0;
1343
+ }
1344
+ function createReceiptChainVerifier(config) {
1345
+ const maxAge = config.maxAgeSeconds ?? 600;
1346
+ const skew = config.maxClockSkewSeconds ?? 60;
1347
+ if (!Number.isFinite(maxAge) || !Number.isFinite(skew) || skew < 0) {
1348
+ throw new TypeError("maxAgeSeconds and maxClockSkewSeconds must be finite; skew must be >= 0");
1349
+ }
1350
+ if (maxAge <= skew + LANDING_WINDOW_SECONDS) {
1351
+ throw new RangeError(
1352
+ `maxAgeSeconds (${maxAge}) must exceed maxClockSkewSeconds (${skew}) + the ${LANDING_WINDOW_SECONDS} s landing window`
1353
+ );
1354
+ }
1355
+ if (!config.index) throw new TypeError("a ReceiptReferenceIndex is required");
1356
+ const rpcTimeoutMs = config.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS;
1357
+ if (typeof rpcTimeoutMs !== "number" || !Number.isFinite(rpcTimeoutMs) || rpcTimeoutMs < 1 || rpcTimeoutMs > MAX_TIMER_MS3) {
1358
+ throw new TypeError(`rpcTimeoutMs must be a number from 1 to ${MAX_TIMER_MS3}`);
1359
+ }
1360
+ const trusted = config.trustedMints ?? DEFAULT_TRUSTED_USD_MINTS;
1361
+ const cluster = config.cluster;
1362
+ if (!(cluster in CLUSTER_IDS)) throw new TypeError(`unknown cluster ${String(cluster)}`);
1363
+ async function fetchWithDeadline(getTransaction, reference) {
1364
+ const controller = new AbortController();
1365
+ let timer;
1366
+ const expired = new Promise((_, reject) => {
1367
+ timer = setTimeout(() => {
1368
+ const reason = new Error(`getTransaction exceeded rpcTimeoutMs (${rpcTimeoutMs} ms)`);
1369
+ controller.abort(reason);
1370
+ reject(reason);
1371
+ }, rpcTimeoutMs);
1372
+ });
1373
+ try {
1374
+ return await Promise.race([
1375
+ getTransaction(reference, { signal: controller.signal }),
1376
+ expired
1377
+ ]);
1378
+ } finally {
1379
+ clearTimeout(timer);
1380
+ }
1381
+ }
1382
+ async function verify(input) {
1383
+ const chain = await chainFacts(input);
1384
+ const verified = chain.receipt_reference_chain_verified;
1385
+ const binding = verified === true ? evaluatePurchaseBinding(chain.instructions ?? [], input.expectedExternalId) : { purchase_bound: null, purchase_bound_reason: "chain_unverified" };
1386
+ const purchase = {
1387
+ receipt_reference_purchase_bound: binding.purchase_bound,
1388
+ receipt_reference_purchase_bound_reason: binding.purchase_bound_reason
1389
+ };
1390
+ let refusal = null;
1391
+ if (chain.blockTime !== null && input.signedAtUnix !== null && Number.isFinite(input.signedAtUnix) && input.signedAtUnix - chain.blockTime > maxAge) {
1392
+ refusal = "stale_by_local_clock";
1393
+ }
1394
+ if (verified === true && refusal === null) {
1395
+ const owns = await config.index.bind(input.reference, input.purchaseId);
1396
+ if (!owns) refusal = "reference_reused";
1397
+ } else {
1398
+ const owner = await config.index.owner(input.reference);
1399
+ if (owner !== void 0 && owner !== input.purchaseId) refusal = "reference_reused";
1400
+ }
1401
+ const acceptance = refusal ? "refused" : verified === true ? "accepted" : null;
1402
+ return {
1403
+ receipt_reference_chain_verified: verified,
1404
+ receipt_reference_finalized_slot: chain.receipt_reference_finalized_slot,
1405
+ receipt_reference_mismatch: chain.receipt_reference_mismatch,
1406
+ receipt_reference_unverified_reason: chain.receipt_reference_unverified_reason,
1407
+ ...purchase,
1408
+ receipt_reference_local_acceptance: acceptance,
1409
+ receipt_reference_refusal_reason: refusal
1410
+ };
1411
+ }
1412
+ function result(verified, code, slot = null, blockTime = null) {
1413
+ return {
1414
+ receipt_reference_chain_verified: verified,
1415
+ receipt_reference_finalized_slot: slot,
1416
+ receipt_reference_mismatch: verified === false ? code : null,
1417
+ receipt_reference_unverified_reason: verified === null ? code : null,
1418
+ blockTime
1419
+ };
1420
+ }
1421
+ async function chainFacts(input) {
1422
+ if (input.method !== "solana") return result(null, "non_solana");
1423
+ const kit = await loadKit();
1424
+ const base58 = kit.getBase58Encoder();
1425
+ let referenceBytes;
1426
+ try {
1427
+ referenceBytes = new Uint8Array(base58.encode(input.reference));
1428
+ } catch {
1429
+ return result(false, "bad_reference_format");
1430
+ }
1431
+ if (referenceBytes.length !== 64) return result(false, "bad_reference_format");
1432
+ const challengeCluster = input.network ? clusterOf(input.network) : void 0;
1433
+ if (challengeCluster !== cluster) return result(false, "wrong_network");
1434
+ if (input.hasSplits) return result(null, "splits_unsupported");
1435
+ if (!input.signedTransaction) return result(null, "no_signed_transaction");
1436
+ const symbols = SYMBOL_MINTS[cluster];
1437
+ const asset = input.asset ? Object.prototype.hasOwnProperty.call(symbols, input.asset) ? symbols[input.asset] : input.asset : null;
1438
+ if (!asset || !kit.isAddress(asset)) return result(null, "unnormalized_terms");
1439
+ if (!input.payTo || !kit.isAddress(input.payTo)) return result(null, "unnormalized_terms");
1440
+ if (!kit.isAddress(input.payer)) return result(null, "unnormalized_terms");
1441
+ if (!input.amountAtomic || !/^\d+$/.test(input.amountAtomic))
1442
+ return result(null, "unnormalized_terms");
1443
+ let retained;
1444
+ try {
1445
+ retained = decodeWire(kit, input.signedTransaction);
1446
+ } catch {
1447
+ return result(null, "no_signed_transaction");
1448
+ }
1449
+ if (!config.getTransaction) return result(null, "no_fetcher");
1450
+ let tx;
1451
+ try {
1452
+ tx = await fetchWithDeadline(config.getTransaction, input.reference);
1453
+ } catch {
1454
+ return result(null, "rpc_unavailable");
1455
+ }
1456
+ if (!tx) return result(null, "not_finalized");
1457
+ const slot = typeof tx.slot === "number" ? tx.slot : null;
1458
+ const blockTime = typeof tx.blockTime === "number" ? tx.blockTime : null;
1459
+ const at = (verified, code) => result(verified, code, slot, blockTime);
1460
+ if (!tx.meta) return at(null, "rpc_unavailable");
1461
+ if (tx.meta.err !== null && tx.meta.err !== void 0) return at(false, "tx_failed");
1462
+ let onChain;
1463
+ try {
1464
+ onChain = decodeWire(kit, tx.transaction[0]);
1465
+ } catch {
1466
+ return at(null, "rpc_unavailable");
1467
+ }
1468
+ const firstSignature = Object.values(onChain.signatures)[0];
1469
+ if (!firstSignature || !bytesEqual(firstSignature, referenceBytes))
1470
+ return at(false, "signature_mismatch");
1471
+ if (!bytesEqual(onChain.messageBytes, retained.messageBytes))
1472
+ return at(false, "message_mismatch");
1473
+ const buyerRetained = retained.signatures[input.payer];
1474
+ const buyerOnChain = onChain.signatures[input.payer];
1475
+ if (!buyerRetained || !buyerOnChain || !bytesEqual(buyerRetained, buyerOnChain)) {
1476
+ return at(false, "buyer_signature_missing");
1477
+ }
1478
+ const message = kit.getCompiledTransactionMessageDecoder().decode(onChain.messageBytes);
1479
+ if (message.version !== "legacy" && message.version !== 0) {
1480
+ return at(null, "unsupported_message_format");
1481
+ }
1482
+ const lookups = message.addressTableLookups;
1483
+ if (lookups && lookups.length > 0) return at(null, "unsupported_message_format");
1484
+ const keys = message.staticAccounts.map(String);
1485
+ const transfers = [];
1486
+ const consider = (programIndex, accountIndexes, data) => {
1487
+ const program = keys[programIndex];
1488
+ if (!program || !KNOWN_TOKEN_PROGRAMS.has(program) || data.length === 0) return;
1489
+ if (data[0] !== IX_TRANSFER && data[0] !== IX_TRANSFER_CHECKED) return;
1490
+ transfers.push({ program, tag: data[0], accounts: accountIndexes.map((i) => keys[i]), data });
1491
+ };
1492
+ for (const ix of message.instructions) {
1493
+ consider(ix.programAddressIndex, ix.accountIndices ?? [], new Uint8Array(ix.data ?? []));
1494
+ }
1495
+ const base58Bytes = kit.getBase58Encoder();
1496
+ if (!Array.isArray(tx.meta.innerInstructions)) return at(null, "rpc_unavailable");
1497
+ for (const group of tx.meta.innerInstructions) {
1498
+ for (const ix of group.instructions) {
1499
+ let data;
1500
+ try {
1501
+ data = new Uint8Array(base58Bytes.encode(ix.data));
1502
+ } catch {
1503
+ return at(null, "rpc_unavailable");
1504
+ }
1505
+ consider(ix.programIdIndex, ix.accounts, data);
1506
+ }
1507
+ }
1508
+ const checked = transfers.filter((t) => t.tag === IX_TRANSFER_CHECKED);
1509
+ if (checked.length === 0) return at(false, "no_matching_transfer");
1510
+ if (checked.length > 1 || transfers.length > 1) return at(false, "multiple_matching_transfers");
1511
+ const [transfer] = checked;
1512
+ if (transfer.data.length < 10 || transfer.accounts.length < 4)
1513
+ return at(false, "no_matching_transfer");
1514
+ const [source, mint, destination, authority] = transfer.accounts;
1515
+ const trust = Object.prototype.hasOwnProperty.call(trusted, asset) ? trusted[asset] : void 0;
1516
+ if (!trust) return at(false, "untrusted_mint");
1517
+ if (!trust.tokenPrograms.includes(transfer.program))
1518
+ return at(false, "untrusted_token_program");
1519
+ if (mint !== asset) return at(false, "mint_mismatch");
1520
+ if (transfer.data[9] !== trust.decimals) return at(false, "decimals_mismatch");
1521
+ const [expectedDestination] = await kit.getProgramDerivedAddress({
1522
+ programAddress: kit.address(ASSOCIATED_TOKEN_PROGRAM),
1523
+ seeds: [
1524
+ kit.getAddressEncoder().encode(kit.address(input.payTo)),
1525
+ kit.getAddressEncoder().encode(kit.address(transfer.program)),
1526
+ kit.getAddressEncoder().encode(kit.address(asset))
1527
+ ]
1528
+ });
1529
+ if (destination !== expectedDestination) return at(false, "payee_mismatch");
1530
+ if (readU64(transfer.data, 1) !== BigInt(input.amountAtomic))
1531
+ return at(false, "amount_mismatch");
1532
+ if (authority !== input.payer) return at(false, "payer_mismatch");
1533
+ const [expectedSource] = await kit.getProgramDerivedAddress({
1534
+ programAddress: kit.address(ASSOCIATED_TOKEN_PROGRAM),
1535
+ seeds: [
1536
+ kit.getAddressEncoder().encode(kit.address(input.payer)),
1537
+ kit.getAddressEncoder().encode(kit.address(transfer.program)),
1538
+ kit.getAddressEncoder().encode(kit.address(asset))
1539
+ ]
1540
+ });
1541
+ if (source !== expectedSource) return at(false, "payer_mismatch");
1542
+ return {
1543
+ ...at(true, null),
1544
+ instructions: message.instructions.map((ix) => ({
1545
+ program: keys[ix.programAddressIndex],
1546
+ data: new Uint8Array(ix.data ?? [])
1547
+ }))
1548
+ };
1549
+ }
1550
+ return Object.freeze(Object.assign(verify, { cluster }));
1551
+ }
1552
+ function decodeWire(kit, base64) {
1553
+ const bytes = kit.getBase64Encoder().encode(base64);
1554
+ const decoded = kit.getTransactionDecoder().decode(bytes);
1555
+ const signatures = {};
1556
+ for (const [addr, sig] of Object.entries(decoded.signatures)) {
1557
+ signatures[addr] = sig ? new Uint8Array(sig) : null;
1558
+ }
1559
+ return { messageBytes: new Uint8Array(decoded.messageBytes), signatures };
1560
+ }
1149
1561
  export {
1150
1562
  CreditLedger,
1563
+ DEFAULT_RPC_TIMEOUT_MS,
1151
1564
  DEFAULT_SOLANA_USD_MINTS,
1152
1565
  DEFAULT_TEMPO_USD_ASSETS,
1566
+ DEFAULT_TRUSTED_USD_MINTS,
1567
+ InMemoryReceiptReferenceIndex,
1568
+ LANDING_WINDOW_SECONDS,
1569
+ MEMO_PROGRAM_ID,
1570
+ MEMO_V1_PROGRAM_ID,
1153
1571
  MPP_FULFILLMENT_MAX_BODY_BYTES,
1154
1572
  MPP_FULFILLMENT_RECORD_SCHEMA,
1155
1573
  MppFlushTimeoutError,
1156
1574
  MppGateError,
1157
1575
  MppPaidNoCompletionError,
1576
+ RECEIPT_REFERENCE_NOT_EVALUATED,
1158
1577
  ReplayRejectedError,
1159
1578
  SpentProofLedger,
1579
+ assertEchoPreSign,
1580
+ createEchoContext,
1160
1581
  createMppFetch,
1582
+ createReceiptChainVerifier,
1161
1583
  createSolanaMppFetch,
1162
1584
  creditPaidNoCompletion,
1585
+ evaluatePurchaseBinding,
1163
1586
  fulfillPaidProof,
1164
1587
  quoteFromChallenge,
1165
1588
  readCredits,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twzrd1111/clawrouter-mpp",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "MPP (Machine Payments Protocol) payment rails for ClawRouter: Tempo and Solana charge, with spend limits and paid-failure handling",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",