@twzrd1111/clawrouter-mpp 0.3.0 → 0.4.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 +64 -1
- package/dist/index.d.ts +149 -5
- package/dist/index.js +379 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
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.4.0 from 0.3.0
|
|
7
|
+
|
|
8
|
+
This release includes the standalone Solana receipt verifier (#16), fulfillment wiring (#18),
|
|
9
|
+
and the per-RPC deadline (#19). Verification is opt-in and does not verify Tempo receipts.
|
|
10
|
+
Purchase binding remains `null/no_buyer_nonce`; Echo and a durable reference index are not supplied
|
|
11
|
+
by this release. The reference index is caller-supplied and its operations have no built-in timeout.
|
|
12
|
+
|
|
13
|
+
- Fulfillment rows now use `twzrd.mpp.fulfillment.v3`. v3 adds eight `receipt_reference_*`
|
|
14
|
+
verification fields (below); every other field is unchanged. Readers must accept the new schema.
|
|
15
|
+
The fields are null unless the Solana rail was given `receiptVerification`, so Tempo rows and
|
|
16
|
+
unconfigured Solana rows carry them as null ("not evaluated", never "clean").
|
|
17
|
+
- `createSolanaMppFetch({ receiptVerification: { verify } })` retains the transaction the buyer
|
|
18
|
+
signed and chain-verifies the seller's receipt reference before writing the row.
|
|
19
|
+
- Each `getTransaction` call made by the verifier has a deadline: `rpcTimeoutMs`, default 10000.
|
|
20
|
+
When it passes, the row records `null/rpc_unavailable` and the `signal` passed to the fetcher
|
|
21
|
+
aborts, so a hung RPC cannot hold the row. Pass that `signal` to `fetch()`.
|
|
22
|
+
|
|
6
23
|
## Upgrading from 0.2.0
|
|
7
24
|
|
|
8
25
|
- Fulfillment rows now use `twzrd.mpp.fulfillment.v2`. Readers must accept the new schema to retain
|
|
@@ -74,12 +91,58 @@ intent match the challenge; it is not an on-chain verification. `seller_reported
|
|
|
74
91
|
compares labels only and does not verify model identity. A failed local write does not change an
|
|
75
92
|
already-paid response; `payFetch.flush()` surfaces persistence failures to the shutdown caller.
|
|
76
93
|
|
|
77
|
-
Rows use schema `twzrd.mpp.fulfillment.
|
|
94
|
+
Rows use schema `twzrd.mpp.fulfillment.v3`. `response_bytes` and `body_sha256` (lowercase hex
|
|
78
95
|
SHA-256) describe the complete response body as the recorder read it: a JSON body read through its
|
|
79
96
|
clone, or an SSE body tapped to its end. Both are `null` when the recorder did not read the body,
|
|
80
97
|
it exceeded 1 MiB, or the stream was cancelled or errored before its end; a partial body is never
|
|
81
98
|
hashed. The digest is of bytes the seller sent and is not a seller attestation.
|
|
82
99
|
|
|
100
|
+
### Receipt reference chain verification (Solana, opt-in)
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { createReceiptChainVerifier, createSolanaMppFetch } from "@twzrd1111/clawrouter-mpp";
|
|
104
|
+
|
|
105
|
+
const verify = createReceiptChainVerifier({
|
|
106
|
+
cluster: "mainnet", // must match the rail's network; a mismatch throws at construction
|
|
107
|
+
getTransaction, // your finalized getTransaction (base64, maxSupportedTransactionVersion 0)
|
|
108
|
+
index, // a durable ReceiptReferenceIndex with an atomic bind
|
|
109
|
+
rpcTimeoutMs: 10_000, // per getTransaction call (the default); pass the given signal to fetch()
|
|
110
|
+
});
|
|
111
|
+
const payFetch = await createSolanaMppFetch({
|
|
112
|
+
signer,
|
|
113
|
+
rpcUrl,
|
|
114
|
+
network: "mainnet",
|
|
115
|
+
spend,
|
|
116
|
+
integration,
|
|
117
|
+
receiptVerification: { verify, finalityWaitMs: 30_000 },
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The rail keeps the base64 wire transaction from the SDK's `signed` event, the buyer clock when it
|
|
122
|
+
fired, and a buyer-generated purchase id (every send of one credential is one purchase). After a
|
|
123
|
+
paid response, the recorder passes the seller's `valid_format` receipt reference with those facts to
|
|
124
|
+
the verifier and writes its result into the row:
|
|
125
|
+
|
|
126
|
+
- `receipt_reference_chain_verified` (`true`, `false`, or `null`), `receipt_reference_finalized_slot`,
|
|
127
|
+
`receipt_reference_mismatch` (why `false`), and `receipt_reference_unverified_reason` (why `null`;
|
|
128
|
+
`verifier_error` when the verifier threw).
|
|
129
|
+
- `receipt_reference_purchase_bound` and its reason. It stays `null/no_buyer_nonce` until a seller
|
|
130
|
+
echoes a buyer nonce; `chain_verified: true` does not mean the payment belongs to this purchase alone.
|
|
131
|
+
- `receipt_reference_local_acceptance` (`accepted`, `refused`, or `null`) and
|
|
132
|
+
`receipt_reference_refusal_reason` (`reference_reused`, `stale_by_local_clock`): this sink's
|
|
133
|
+
policy, which never changes a chain field.
|
|
134
|
+
|
|
135
|
+
Verification never delays or changes the response. It delays only the row and `flush()`.
|
|
136
|
+
`finalityWaitMs` (default 0) re-checks a reference that is not finalized yet every `finalityPollMs`
|
|
137
|
+
(default 2000); with the default, an unfinalized reference is recorded as `null/not_finalized`.
|
|
138
|
+
`finalityWaitMs` is not an RPC timeout: each `getTransaction` call has its own deadline,
|
|
139
|
+
`rpcTimeoutMs` on the verifier (default 10000). When it passes, the fetcher's `signal` aborts and
|
|
140
|
+
the reference is recorded as `null/rpc_unavailable`. A row therefore waits at most about
|
|
141
|
+
`finalityWaitMs + rpcTimeoutMs` on the chain. The reference index has no deadline, so a durable
|
|
142
|
+
index must bound its own calls; `flush({ timeoutMs })` bounds only how long the caller waits.
|
|
143
|
+
No RPC URL ships with the package. `InMemoryReceiptReferenceIndex` forgets on restart and is for
|
|
144
|
+
tests. See `docs/receipt-chain-verify.md` for each check.
|
|
145
|
+
|
|
83
146
|
This candidate requires `@twzrd1111/clawrouter >=0.12.309`, including deferred streaming headers.
|
|
84
147
|
The fork is the release target. Upstream PRs #545 and #546 are independent of this release.
|
|
85
148
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,124 @@ import { PayFetch, SpendControl, SpendRecord } from '@twzrd1111/clawrouter';
|
|
|
3
3
|
export { PayFetch } from '@twzrd1111/clawrouter';
|
|
4
4
|
import { KeyPairSigner } from '@solana/kit';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** A blockhash stays valid for about 150 slots, roughly 60-90 s. */
|
|
7
|
+
declare const LANDING_WINDOW_SECONDS = 90;
|
|
8
|
+
declare const CLUSTER_IDS: {
|
|
9
|
+
readonly mainnet: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
10
|
+
readonly devnet: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
|
|
11
|
+
};
|
|
12
|
+
type SolanaCluster = keyof typeof CLUSTER_IDS;
|
|
13
|
+
type TrustedMint = {
|
|
14
|
+
decimals: number;
|
|
15
|
+
tokenPrograms: readonly string[];
|
|
16
|
+
};
|
|
17
|
+
/** Classic SPL Token USD mints with 6 decimals: USDC mainnet, USDC devnet, USDT mainnet. */
|
|
18
|
+
declare const DEFAULT_TRUSTED_USD_MINTS: Readonly<Record<string, TrustedMint>>;
|
|
19
|
+
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";
|
|
20
|
+
type ChainUnverifiedReason = "rpc_unavailable" | "not_finalized" | "no_fetcher" | "non_solana" | "no_signed_transaction" | "splits_unsupported" | "unnormalized_terms" | "unsupported_message_format"
|
|
21
|
+
/** The verifier itself threw; recorded by the fulfillment hook, never returned by `verify`. */
|
|
22
|
+
| "verifier_error";
|
|
23
|
+
type ReceiptChainVerification = {
|
|
24
|
+
receipt_reference_chain_verified: boolean | null;
|
|
25
|
+
receipt_reference_finalized_slot: number | null;
|
|
26
|
+
receipt_reference_mismatch: ChainMismatch | null;
|
|
27
|
+
receipt_reference_unverified_reason: ChainUnverifiedReason | null;
|
|
28
|
+
receipt_reference_purchase_bound: boolean | null;
|
|
29
|
+
receipt_reference_purchase_bound_reason: "nonce_mismatch" | "no_buyer_nonce" | "chain_unverified" | null;
|
|
30
|
+
receipt_reference_local_acceptance: "accepted" | "refused" | null;
|
|
31
|
+
receipt_reference_refusal_reason: "reference_reused" | "stale_by_local_clock" | null;
|
|
32
|
+
};
|
|
33
|
+
/** Compiled inner instruction as returned by getTransaction with `encoding: "base64"`. */
|
|
34
|
+
type RpcInnerInstruction = {
|
|
35
|
+
programIdIndex: number;
|
|
36
|
+
accounts: number[];
|
|
37
|
+
data: string;
|
|
38
|
+
};
|
|
39
|
+
/** The subset of a `getTransaction` (`encoding: "base64"`, finalized) result this verifier reads. */
|
|
40
|
+
type RpcTransaction = {
|
|
41
|
+
slot: number;
|
|
42
|
+
blockTime: number | null;
|
|
43
|
+
meta: {
|
|
44
|
+
err: unknown;
|
|
45
|
+
innerInstructions?: {
|
|
46
|
+
index: number;
|
|
47
|
+
instructions: RpcInnerInstruction[];
|
|
48
|
+
}[] | null;
|
|
49
|
+
} | null;
|
|
50
|
+
transaction: [string, "base64"] | readonly [string, string];
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Must call getTransaction(signature, {commitment: "finalized", encoding: "base64",
|
|
54
|
+
* maxSupportedTransactionVersion: 0}) on the configured cluster and return its result (null when
|
|
55
|
+
* not found or not finalized). Throw on transport failure. `signal` aborts when the verifier's
|
|
56
|
+
* `rpcTimeoutMs` deadline passes; pass it to fetch() so the request is cancelled, not abandoned.
|
|
57
|
+
*/
|
|
58
|
+
type GetFinalizedTransaction = (signature: string, options?: {
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
}) => Promise<RpcTransaction | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Durable, atomic reference -> purchase_id index. `bind` returns true when this purchase owns the
|
|
63
|
+
* reference (newly bound or already bound to the same purchase) and false when another purchase
|
|
64
|
+
* holds it; exactly one concurrent binder may win. `owner` reads without binding.
|
|
65
|
+
*/
|
|
66
|
+
type ReceiptReferenceIndex = {
|
|
67
|
+
bind(reference: string, purchaseId: string): Promise<boolean>;
|
|
68
|
+
owner(reference: string): Promise<string | undefined>;
|
|
69
|
+
};
|
|
70
|
+
/** In-memory index for tests and single-process demos. It forgets on restart, so it is not durable. */
|
|
71
|
+
declare class InMemoryReceiptReferenceIndex implements ReceiptReferenceIndex {
|
|
72
|
+
#private;
|
|
73
|
+
bind(reference: string, purchaseId: string): Promise<boolean>;
|
|
74
|
+
owner(reference: string): Promise<string | undefined>;
|
|
75
|
+
}
|
|
76
|
+
type ReceiptChainVerifierConfig = {
|
|
77
|
+
cluster: SolanaCluster;
|
|
78
|
+
getTransaction?: GetFinalizedTransaction;
|
|
79
|
+
index: ReceiptReferenceIndex;
|
|
80
|
+
/** Trusted mint -> decimals and token programs. Defaults to DEFAULT_TRUSTED_USD_MINTS. */
|
|
81
|
+
trustedMints?: Readonly<Record<string, TrustedMint>>;
|
|
82
|
+
/** Default 600. Must exceed maxClockSkewSeconds + LANDING_WINDOW_SECONDS. */
|
|
83
|
+
maxAgeSeconds?: number;
|
|
84
|
+
/** Default 60. */
|
|
85
|
+
maxClockSkewSeconds?: number;
|
|
86
|
+
/**
|
|
87
|
+
* Deadline for one getTransaction call, in ms. Default 10000. When it passes, the fetcher's
|
|
88
|
+
* signal aborts and the reference is recorded as null/rpc_unavailable. The reference index is
|
|
89
|
+
* not covered: a durable index must bound its own calls.
|
|
90
|
+
*/
|
|
91
|
+
rpcTimeoutMs?: number;
|
|
92
|
+
};
|
|
93
|
+
/** Default deadline for one getTransaction call. */
|
|
94
|
+
declare const DEFAULT_RPC_TIMEOUT_MS = 10000;
|
|
95
|
+
type ReceiptChainVerifyInput = {
|
|
96
|
+
/** Seller's receipt reference: the settled transaction signature. */
|
|
97
|
+
reference: string;
|
|
98
|
+
/** MPP payment method; only "solana" is verified. */
|
|
99
|
+
method: string;
|
|
100
|
+
/** Challenge terms as the seller sent them. */
|
|
101
|
+
network: string | null;
|
|
102
|
+
asset: string | null;
|
|
103
|
+
payTo: string | null;
|
|
104
|
+
amountAtomic: string | null;
|
|
105
|
+
/** True when the challenge carried split recipients. */
|
|
106
|
+
hasSplits?: boolean;
|
|
107
|
+
/** The buyer's own address. */
|
|
108
|
+
payer: string;
|
|
109
|
+
/** Base64 wire transaction from the rail's `signed` event, or null when not retained. */
|
|
110
|
+
signedTransaction: string | null;
|
|
111
|
+
/** Buyer clock (unix seconds) when the signed event fired. */
|
|
112
|
+
signedAtUnix: number | null;
|
|
113
|
+
/** Buyer-generated id of the logical purchase (not the seller's challenge id). */
|
|
114
|
+
purchaseId: string;
|
|
115
|
+
};
|
|
116
|
+
/** A built verifier. `cluster` lets a rail refuse a verifier configured for another cluster. */
|
|
117
|
+
type ReceiptChainVerify = ((input: ReceiptChainVerifyInput) => Promise<ReceiptChainVerification>) & {
|
|
118
|
+
readonly cluster: SolanaCluster;
|
|
119
|
+
};
|
|
120
|
+
/** Build a verifier. Throws when the age policy cannot hold an honest payment. */
|
|
121
|
+
declare function createReceiptChainVerifier(config: ReceiptChainVerifierConfig): ReceiptChainVerify;
|
|
122
|
+
|
|
123
|
+
declare const MPP_FULFILLMENT_RECORD_SCHEMA = "twzrd.mpp.fulfillment.v3";
|
|
7
124
|
declare const MPP_FULFILLMENT_MAX_BODY_BYTES: number;
|
|
8
125
|
type MppPaymentFacts = {
|
|
9
126
|
challengeId: string;
|
|
@@ -14,7 +131,14 @@ type MppPaymentFacts = {
|
|
|
14
131
|
asset: string | null;
|
|
15
132
|
amountAtomic: string | null;
|
|
16
133
|
};
|
|
17
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Chain verification of the seller's receipt reference. Every field is null when no verifier ran
|
|
136
|
+
* (none configured, a non-Solana rail, or no valid-format receipt): null means "not evaluated",
|
|
137
|
+
* never "clean". See docs/receipt-chain-verify.md for each field.
|
|
138
|
+
*/
|
|
139
|
+
type MppReceiptReferenceVerification = ReceiptChainVerification;
|
|
140
|
+
declare const RECEIPT_REFERENCE_NOT_EVALUATED: Readonly<MppReceiptReferenceVerification>;
|
|
141
|
+
type MppFulfillmentRecord = MppReceiptReferenceVerification & {
|
|
18
142
|
schema: typeof MPP_FULFILLMENT_RECORD_SCHEMA;
|
|
19
143
|
at_unix_ms: number;
|
|
20
144
|
resource_origin: string | null;
|
|
@@ -29,7 +153,10 @@ type MppFulfillmentRecord = {
|
|
|
29
153
|
receipt_state: "valid_format" | "missing" | "invalid_format" | "unavailable";
|
|
30
154
|
receipt_status: string | null;
|
|
31
155
|
receipt_method: string | null;
|
|
32
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* Seller-supplied MPP receipt reference (EVM hash or Solana signature). Chain-verified only when
|
|
158
|
+
* the rail was given a verifier; see the receipt_reference_* verification fields.
|
|
159
|
+
*/
|
|
33
160
|
receipt_reference: string | null;
|
|
34
161
|
receipt_timestamp: string | null;
|
|
35
162
|
requested_model: string | null;
|
|
@@ -155,6 +282,23 @@ type SolanaMppFetchOptions = {
|
|
|
155
282
|
integration: MppIntegration;
|
|
156
283
|
/** Injectable transport; defaults to fetch. The SDK's RPC transport is separate. */
|
|
157
284
|
fetch?: typeof fetch;
|
|
285
|
+
/**
|
|
286
|
+
* Chain-verify each seller receipt reference before its fulfillment row is written. Off when
|
|
287
|
+
* omitted (the row's receipt_reference_* verification fields stay null). Never delays the response.
|
|
288
|
+
*/
|
|
289
|
+
receiptVerification?: SolanaReceiptVerificationOptions;
|
|
290
|
+
};
|
|
291
|
+
type SolanaReceiptVerificationOptions = {
|
|
292
|
+
/** From createReceiptChainVerifier, built for the same cluster as `network`. */
|
|
293
|
+
verify: ReceiptChainVerify;
|
|
294
|
+
/**
|
|
295
|
+
* How long to keep re-checking a reference that is not finalized yet, in ms. Default 0: one check,
|
|
296
|
+
* and a not-yet-finalized reference is recorded as null/not_finalized. This delays only the row
|
|
297
|
+
* (and flush()), never the response.
|
|
298
|
+
*/
|
|
299
|
+
finalityWaitMs?: number;
|
|
300
|
+
/** Delay between finality re-checks, in ms. Default 2000. */
|
|
301
|
+
finalityPollMs?: number;
|
|
158
302
|
};
|
|
159
303
|
declare function createSolanaMppFetch(opts: SolanaMppFetchOptions): Promise<MppPayFetch>;
|
|
160
304
|
|
|
@@ -245,5 +389,5 @@ declare function fulfillPaidProof<T>(opts: {
|
|
|
245
389
|
upstream: () => Promise<T>;
|
|
246
390
|
}): Promise<T>;
|
|
247
391
|
|
|
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 };
|
|
392
|
+
export { CreditLedger, DEFAULT_RPC_TIMEOUT_MS, DEFAULT_SOLANA_USD_MINTS, DEFAULT_TEMPO_USD_ASSETS, DEFAULT_TRUSTED_USD_MINTS, InMemoryReceiptReferenceIndex, LANDING_WINDOW_SECONDS, MPP_FULFILLMENT_MAX_BODY_BYTES, MPP_FULFILLMENT_RECORD_SCHEMA, MppFlushTimeoutError, MppGateError, MppPaidNoCompletionError, RECEIPT_REFERENCE_NOT_EVALUATED, ReplayRejectedError, SpentProofLedger, createMppFetch, createReceiptChainVerifier, createSolanaMppFetch, creditPaidNoCompletion, fulfillPaidProof, quoteFromChallenge, readCredits, readPaidCallEvidence };
|
|
393
|
+
export type { ChainMismatch, ChainUnverifiedReason, CreditEntry, GetFinalizedTransaction, MppBodyDigest, MppFetchOptions, MppFlushOptions, MppFulfillmentRecord, MppFulfillmentSink, MppGateChallenge, MppGateDecision, MppGateEvaluator, MppIntegration, MppPayFetch, MppPaymentFacts, MppReceiptReferenceVerification, 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.
|
|
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:
|
|
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 { randomUUID } from "crypto";
|
|
884
916
|
import {
|
|
885
917
|
assertSpendPolicyAllows as assertSpendPolicyAllows2,
|
|
886
918
|
SpendPolicyError as SpendPolicyError3
|
|
@@ -895,12 +927,46 @@ var CAIP2 = {
|
|
|
895
927
|
devnet: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
896
928
|
localnet: "solana:localnet"
|
|
897
929
|
};
|
|
930
|
+
var MAX_TIMER_MS2 = 2147483647;
|
|
931
|
+
function verificationSettings(options, network) {
|
|
932
|
+
if (options === void 0) return void 0;
|
|
933
|
+
if (typeof options?.verify !== "function") {
|
|
934
|
+
throw new TypeError("receiptVerification.verify must come from createReceiptChainVerifier");
|
|
935
|
+
}
|
|
936
|
+
if (options.verify.cluster !== network) {
|
|
937
|
+
throw new TypeError(
|
|
938
|
+
`receiptVerification is built for ${String(options.verify.cluster)}, but the rail network is ${network}`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
const waitMs = options.finalityWaitMs ?? 0;
|
|
942
|
+
const pollMs = options.finalityPollMs ?? 2e3;
|
|
943
|
+
for (const [name, value, min] of [
|
|
944
|
+
["finalityWaitMs", waitMs, 0],
|
|
945
|
+
["finalityPollMs", pollMs, 1]
|
|
946
|
+
]) {
|
|
947
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > MAX_TIMER_MS2)
|
|
948
|
+
throw new TypeError(
|
|
949
|
+
`receiptVerification.${name} must be a number from ${min} to ${MAX_TIMER_MS2}`
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
return { verify: options.verify, waitMs, pollMs };
|
|
953
|
+
}
|
|
954
|
+
async function verifyUntilFinal(verify, waitMs, pollMs) {
|
|
955
|
+
const deadline = Date.now() + waitMs;
|
|
956
|
+
for (; ; ) {
|
|
957
|
+
const result = await verify();
|
|
958
|
+
if (result.receipt_reference_unverified_reason !== "not_finalized") return result;
|
|
959
|
+
if (Date.now() + pollMs > deadline) return result;
|
|
960
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
898
963
|
var solanaChargeDeps = {
|
|
899
964
|
load: () => loadOptional("@solana/mpp", SOLANA_MPP_RANGE, () => import("@solana/mpp/client"))
|
|
900
965
|
};
|
|
901
966
|
async function createSolanaMppFetch(opts) {
|
|
902
967
|
assertMppIntegration(opts?.integration, "Solana");
|
|
903
968
|
const integration = Object.freeze({ ...opts.integration });
|
|
969
|
+
const verification = verificationSettings(opts.receiptVerification, opts.network);
|
|
904
970
|
const { charge, selectSolanaChargeChallengeFromResponse, resolveStablecoinMint } = await solanaChargeDeps.load();
|
|
905
971
|
const base = opts.fetch ?? fetch;
|
|
906
972
|
const usd = /* @__PURE__ */ new Set([...DEFAULT_SOLANA_USD_MINTS, ...opts.usdMints ?? []]);
|
|
@@ -997,6 +1063,8 @@ async function createSolanaMppFetch(opts) {
|
|
|
997
1063
|
let sessionSpendRecorded = false;
|
|
998
1064
|
let record;
|
|
999
1065
|
let payload = "";
|
|
1066
|
+
let signedTransaction = null;
|
|
1067
|
+
let signedAtUnix = null;
|
|
1000
1068
|
try {
|
|
1001
1069
|
const method = charge({
|
|
1002
1070
|
signer: opts.signer,
|
|
@@ -1007,6 +1075,8 @@ async function createSolanaMppFetch(opts) {
|
|
|
1007
1075
|
if (event.type !== "signed") return;
|
|
1008
1076
|
signed = true;
|
|
1009
1077
|
payload = event.transaction;
|
|
1078
|
+
signedTransaction = event.transaction;
|
|
1079
|
+
signedAtUnix = Math.floor(Date.now() / 1e3);
|
|
1010
1080
|
if (!sessionSpendRecorded) {
|
|
1011
1081
|
budgetContext?.recordSigned(budgetQuoteMicros);
|
|
1012
1082
|
sessionSpendRecorded = true;
|
|
@@ -1032,12 +1102,17 @@ async function createSolanaMppFetch(opts) {
|
|
|
1032
1102
|
});
|
|
1033
1103
|
}
|
|
1034
1104
|
}
|
|
1105
|
+
const splits = challenge.request.methodDetails.splits;
|
|
1035
1106
|
return {
|
|
1036
1107
|
credential,
|
|
1037
1108
|
payload,
|
|
1038
1109
|
record,
|
|
1039
1110
|
gate: gateDecision,
|
|
1040
|
-
payment
|
|
1111
|
+
payment,
|
|
1112
|
+
signedTransaction,
|
|
1113
|
+
signedAtUnix,
|
|
1114
|
+
purchaseId: randomUUID(),
|
|
1115
|
+
hasSplits: Array.isArray(splits) && splits.length > 0
|
|
1041
1116
|
};
|
|
1042
1117
|
} catch (err) {
|
|
1043
1118
|
if (signed) {
|
|
@@ -1071,8 +1146,32 @@ async function createSolanaMppFetch(opts) {
|
|
|
1071
1146
|
const response = await base(new Request(request, { headers }));
|
|
1072
1147
|
const paid = currentPaidCall();
|
|
1073
1148
|
if (response.status !== 402 && paid?.signed && paid.payment) {
|
|
1149
|
+
const terms = paid.payment;
|
|
1150
|
+
const verifyReceiptReference = verification ? (reference) => verifyUntilFinal(
|
|
1151
|
+
() => verification.verify({
|
|
1152
|
+
reference,
|
|
1153
|
+
method: terms.method,
|
|
1154
|
+
network: terms.network,
|
|
1155
|
+
asset: terms.asset,
|
|
1156
|
+
payTo: terms.payTo,
|
|
1157
|
+
amountAtomic: terms.amountAtomic,
|
|
1158
|
+
hasSplits: result.hasSplits,
|
|
1159
|
+
payer: opts.signer.address,
|
|
1160
|
+
signedTransaction: result.signedTransaction,
|
|
1161
|
+
signedAtUnix: result.signedAtUnix,
|
|
1162
|
+
purchaseId: result.purchaseId
|
|
1163
|
+
}),
|
|
1164
|
+
verification.waitMs,
|
|
1165
|
+
verification.pollMs
|
|
1166
|
+
) : void 0;
|
|
1074
1167
|
return recordMppFulfillment(
|
|
1075
|
-
{
|
|
1168
|
+
{
|
|
1169
|
+
input: requestForRecord,
|
|
1170
|
+
response,
|
|
1171
|
+
payment: terms,
|
|
1172
|
+
gate: paid.gate,
|
|
1173
|
+
verifyReceiptReference
|
|
1174
|
+
},
|
|
1076
1175
|
integration.onFulfillment,
|
|
1077
1176
|
trackFulfillment
|
|
1078
1177
|
);
|
|
@@ -1146,18 +1245,293 @@ async function fulfillPaidProof(opts) {
|
|
|
1146
1245
|
if (!won) throw new ReplayRejectedError(opts.networkId, opts.txSignature, opts.challengeId);
|
|
1147
1246
|
return opts.upstream();
|
|
1148
1247
|
}
|
|
1248
|
+
|
|
1249
|
+
// src/receipt-chain-verify.ts
|
|
1250
|
+
var SOLANA_KIT_RANGE = "~6.10.0";
|
|
1251
|
+
var TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
1252
|
+
var TOKEN_2022_PROGRAM = "TokenzQdBNbLqP7VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
|
|
1253
|
+
var ASSOCIATED_TOKEN_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
1254
|
+
var KNOWN_TOKEN_PROGRAMS = /* @__PURE__ */ new Set([TOKEN_PROGRAM, TOKEN_2022_PROGRAM]);
|
|
1255
|
+
var IX_TRANSFER = 3;
|
|
1256
|
+
var IX_TRANSFER_CHECKED = 12;
|
|
1257
|
+
var LANDING_WINDOW_SECONDS = 90;
|
|
1258
|
+
var CLUSTER_IDS = {
|
|
1259
|
+
mainnet: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
1260
|
+
devnet: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"
|
|
1261
|
+
};
|
|
1262
|
+
var SYMBOL_MINTS = {
|
|
1263
|
+
mainnet: { USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" },
|
|
1264
|
+
devnet: { USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" }
|
|
1265
|
+
};
|
|
1266
|
+
var DEFAULT_TRUSTED_USD_MINTS = Object.freeze({
|
|
1267
|
+
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: { decimals: 6, tokenPrograms: [TOKEN_PROGRAM] },
|
|
1268
|
+
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU": { decimals: 6, tokenPrograms: [TOKEN_PROGRAM] },
|
|
1269
|
+
Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: { decimals: 6, tokenPrograms: [TOKEN_PROGRAM] }
|
|
1270
|
+
});
|
|
1271
|
+
var InMemoryReceiptReferenceIndex = class {
|
|
1272
|
+
#owners = /* @__PURE__ */ new Map();
|
|
1273
|
+
async bind(reference, purchaseId) {
|
|
1274
|
+
const current = this.#owners.get(reference);
|
|
1275
|
+
if (current === void 0) {
|
|
1276
|
+
this.#owners.set(reference, purchaseId);
|
|
1277
|
+
return true;
|
|
1278
|
+
}
|
|
1279
|
+
return current === purchaseId;
|
|
1280
|
+
}
|
|
1281
|
+
async owner(reference) {
|
|
1282
|
+
return this.#owners.get(reference);
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
var DEFAULT_RPC_TIMEOUT_MS = 1e4;
|
|
1286
|
+
var MAX_TIMER_MS3 = 2147483647;
|
|
1287
|
+
var loadKit = () => loadOptional("@solana/kit", SOLANA_KIT_RANGE, () => import("@solana/kit"));
|
|
1288
|
+
function bytesEqual(a, b) {
|
|
1289
|
+
if (a.length !== b.length) return false;
|
|
1290
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
1291
|
+
return true;
|
|
1292
|
+
}
|
|
1293
|
+
function readU64(data, offset) {
|
|
1294
|
+
let value = 0n;
|
|
1295
|
+
for (let i = 7; i >= 0; i--) value = value << 8n | BigInt(data[offset + i]);
|
|
1296
|
+
return value;
|
|
1297
|
+
}
|
|
1298
|
+
function clusterOf(network) {
|
|
1299
|
+
if (network === "mainnet" || network === "mainnet-beta" || network === CLUSTER_IDS.mainnet)
|
|
1300
|
+
return "mainnet";
|
|
1301
|
+
if (network === "devnet" || network === CLUSTER_IDS.devnet) return "devnet";
|
|
1302
|
+
return void 0;
|
|
1303
|
+
}
|
|
1304
|
+
function createReceiptChainVerifier(config) {
|
|
1305
|
+
const maxAge = config.maxAgeSeconds ?? 600;
|
|
1306
|
+
const skew = config.maxClockSkewSeconds ?? 60;
|
|
1307
|
+
if (!Number.isFinite(maxAge) || !Number.isFinite(skew) || skew < 0) {
|
|
1308
|
+
throw new TypeError("maxAgeSeconds and maxClockSkewSeconds must be finite; skew must be >= 0");
|
|
1309
|
+
}
|
|
1310
|
+
if (maxAge <= skew + LANDING_WINDOW_SECONDS) {
|
|
1311
|
+
throw new RangeError(
|
|
1312
|
+
`maxAgeSeconds (${maxAge}) must exceed maxClockSkewSeconds (${skew}) + the ${LANDING_WINDOW_SECONDS} s landing window`
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
if (!config.index) throw new TypeError("a ReceiptReferenceIndex is required");
|
|
1316
|
+
const rpcTimeoutMs = config.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS;
|
|
1317
|
+
if (typeof rpcTimeoutMs !== "number" || !Number.isFinite(rpcTimeoutMs) || rpcTimeoutMs < 1 || rpcTimeoutMs > MAX_TIMER_MS3) {
|
|
1318
|
+
throw new TypeError(`rpcTimeoutMs must be a number from 1 to ${MAX_TIMER_MS3}`);
|
|
1319
|
+
}
|
|
1320
|
+
const trusted = config.trustedMints ?? DEFAULT_TRUSTED_USD_MINTS;
|
|
1321
|
+
const cluster = config.cluster;
|
|
1322
|
+
if (!(cluster in CLUSTER_IDS)) throw new TypeError(`unknown cluster ${String(cluster)}`);
|
|
1323
|
+
async function fetchWithDeadline(getTransaction, reference) {
|
|
1324
|
+
const controller = new AbortController();
|
|
1325
|
+
let timer;
|
|
1326
|
+
const expired = new Promise((_, reject) => {
|
|
1327
|
+
timer = setTimeout(() => {
|
|
1328
|
+
const reason = new Error(`getTransaction exceeded rpcTimeoutMs (${rpcTimeoutMs} ms)`);
|
|
1329
|
+
controller.abort(reason);
|
|
1330
|
+
reject(reason);
|
|
1331
|
+
}, rpcTimeoutMs);
|
|
1332
|
+
});
|
|
1333
|
+
try {
|
|
1334
|
+
return await Promise.race([
|
|
1335
|
+
getTransaction(reference, { signal: controller.signal }),
|
|
1336
|
+
expired
|
|
1337
|
+
]);
|
|
1338
|
+
} finally {
|
|
1339
|
+
clearTimeout(timer);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
async function verify(input) {
|
|
1343
|
+
const chain = await chainFacts(input);
|
|
1344
|
+
const verified = chain.receipt_reference_chain_verified;
|
|
1345
|
+
const purchase = verified === true ? {
|
|
1346
|
+
receipt_reference_purchase_bound: null,
|
|
1347
|
+
receipt_reference_purchase_bound_reason: "no_buyer_nonce"
|
|
1348
|
+
} : {
|
|
1349
|
+
receipt_reference_purchase_bound: null,
|
|
1350
|
+
receipt_reference_purchase_bound_reason: "chain_unverified"
|
|
1351
|
+
};
|
|
1352
|
+
let refusal = null;
|
|
1353
|
+
if (chain.blockTime !== null && input.signedAtUnix !== null && Number.isFinite(input.signedAtUnix) && input.signedAtUnix - chain.blockTime > maxAge) {
|
|
1354
|
+
refusal = "stale_by_local_clock";
|
|
1355
|
+
}
|
|
1356
|
+
if (verified === true && refusal === null) {
|
|
1357
|
+
const owns = await config.index.bind(input.reference, input.purchaseId);
|
|
1358
|
+
if (!owns) refusal = "reference_reused";
|
|
1359
|
+
} else {
|
|
1360
|
+
const owner = await config.index.owner(input.reference);
|
|
1361
|
+
if (owner !== void 0 && owner !== input.purchaseId) refusal = "reference_reused";
|
|
1362
|
+
}
|
|
1363
|
+
const acceptance = refusal ? "refused" : verified === true ? "accepted" : null;
|
|
1364
|
+
return {
|
|
1365
|
+
receipt_reference_chain_verified: verified,
|
|
1366
|
+
receipt_reference_finalized_slot: chain.receipt_reference_finalized_slot,
|
|
1367
|
+
receipt_reference_mismatch: chain.receipt_reference_mismatch,
|
|
1368
|
+
receipt_reference_unverified_reason: chain.receipt_reference_unverified_reason,
|
|
1369
|
+
...purchase,
|
|
1370
|
+
receipt_reference_local_acceptance: acceptance,
|
|
1371
|
+
receipt_reference_refusal_reason: refusal
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
function result(verified, code, slot = null, blockTime = null) {
|
|
1375
|
+
return {
|
|
1376
|
+
receipt_reference_chain_verified: verified,
|
|
1377
|
+
receipt_reference_finalized_slot: slot,
|
|
1378
|
+
receipt_reference_mismatch: verified === false ? code : null,
|
|
1379
|
+
receipt_reference_unverified_reason: verified === null ? code : null,
|
|
1380
|
+
blockTime
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
async function chainFacts(input) {
|
|
1384
|
+
if (input.method !== "solana") return result(null, "non_solana");
|
|
1385
|
+
const kit = await loadKit();
|
|
1386
|
+
const base58 = kit.getBase58Encoder();
|
|
1387
|
+
let referenceBytes;
|
|
1388
|
+
try {
|
|
1389
|
+
referenceBytes = new Uint8Array(base58.encode(input.reference));
|
|
1390
|
+
} catch {
|
|
1391
|
+
return result(false, "bad_reference_format");
|
|
1392
|
+
}
|
|
1393
|
+
if (referenceBytes.length !== 64) return result(false, "bad_reference_format");
|
|
1394
|
+
const challengeCluster = input.network ? clusterOf(input.network) : void 0;
|
|
1395
|
+
if (challengeCluster !== cluster) return result(false, "wrong_network");
|
|
1396
|
+
if (input.hasSplits) return result(null, "splits_unsupported");
|
|
1397
|
+
if (!input.signedTransaction) return result(null, "no_signed_transaction");
|
|
1398
|
+
const symbols = SYMBOL_MINTS[cluster];
|
|
1399
|
+
const asset = input.asset ? Object.prototype.hasOwnProperty.call(symbols, input.asset) ? symbols[input.asset] : input.asset : null;
|
|
1400
|
+
if (!asset || !kit.isAddress(asset)) return result(null, "unnormalized_terms");
|
|
1401
|
+
if (!input.payTo || !kit.isAddress(input.payTo)) return result(null, "unnormalized_terms");
|
|
1402
|
+
if (!kit.isAddress(input.payer)) return result(null, "unnormalized_terms");
|
|
1403
|
+
if (!input.amountAtomic || !/^\d+$/.test(input.amountAtomic))
|
|
1404
|
+
return result(null, "unnormalized_terms");
|
|
1405
|
+
let retained;
|
|
1406
|
+
try {
|
|
1407
|
+
retained = decodeWire(kit, input.signedTransaction);
|
|
1408
|
+
} catch {
|
|
1409
|
+
return result(null, "no_signed_transaction");
|
|
1410
|
+
}
|
|
1411
|
+
if (!config.getTransaction) return result(null, "no_fetcher");
|
|
1412
|
+
let tx;
|
|
1413
|
+
try {
|
|
1414
|
+
tx = await fetchWithDeadline(config.getTransaction, input.reference);
|
|
1415
|
+
} catch {
|
|
1416
|
+
return result(null, "rpc_unavailable");
|
|
1417
|
+
}
|
|
1418
|
+
if (!tx) return result(null, "not_finalized");
|
|
1419
|
+
const slot = typeof tx.slot === "number" ? tx.slot : null;
|
|
1420
|
+
const blockTime = typeof tx.blockTime === "number" ? tx.blockTime : null;
|
|
1421
|
+
const at = (verified, code) => result(verified, code, slot, blockTime);
|
|
1422
|
+
if (!tx.meta) return at(null, "rpc_unavailable");
|
|
1423
|
+
if (tx.meta.err !== null && tx.meta.err !== void 0) return at(false, "tx_failed");
|
|
1424
|
+
let onChain;
|
|
1425
|
+
try {
|
|
1426
|
+
onChain = decodeWire(kit, tx.transaction[0]);
|
|
1427
|
+
} catch {
|
|
1428
|
+
return at(null, "rpc_unavailable");
|
|
1429
|
+
}
|
|
1430
|
+
const firstSignature = Object.values(onChain.signatures)[0];
|
|
1431
|
+
if (!firstSignature || !bytesEqual(firstSignature, referenceBytes))
|
|
1432
|
+
return at(false, "signature_mismatch");
|
|
1433
|
+
if (!bytesEqual(onChain.messageBytes, retained.messageBytes))
|
|
1434
|
+
return at(false, "message_mismatch");
|
|
1435
|
+
const buyerRetained = retained.signatures[input.payer];
|
|
1436
|
+
const buyerOnChain = onChain.signatures[input.payer];
|
|
1437
|
+
if (!buyerRetained || !buyerOnChain || !bytesEqual(buyerRetained, buyerOnChain)) {
|
|
1438
|
+
return at(false, "buyer_signature_missing");
|
|
1439
|
+
}
|
|
1440
|
+
const message = kit.getCompiledTransactionMessageDecoder().decode(onChain.messageBytes);
|
|
1441
|
+
if (message.version !== "legacy" && message.version !== 0) {
|
|
1442
|
+
return at(null, "unsupported_message_format");
|
|
1443
|
+
}
|
|
1444
|
+
const lookups = message.addressTableLookups;
|
|
1445
|
+
if (lookups && lookups.length > 0) return at(null, "unsupported_message_format");
|
|
1446
|
+
const keys = message.staticAccounts.map(String);
|
|
1447
|
+
const transfers = [];
|
|
1448
|
+
const consider = (programIndex, accountIndexes, data) => {
|
|
1449
|
+
const program = keys[programIndex];
|
|
1450
|
+
if (!program || !KNOWN_TOKEN_PROGRAMS.has(program) || data.length === 0) return;
|
|
1451
|
+
if (data[0] !== IX_TRANSFER && data[0] !== IX_TRANSFER_CHECKED) return;
|
|
1452
|
+
transfers.push({ program, tag: data[0], accounts: accountIndexes.map((i) => keys[i]), data });
|
|
1453
|
+
};
|
|
1454
|
+
for (const ix of message.instructions) {
|
|
1455
|
+
consider(ix.programAddressIndex, ix.accountIndices ?? [], new Uint8Array(ix.data ?? []));
|
|
1456
|
+
}
|
|
1457
|
+
const base58Bytes = kit.getBase58Encoder();
|
|
1458
|
+
if (!Array.isArray(tx.meta.innerInstructions)) return at(null, "rpc_unavailable");
|
|
1459
|
+
for (const group of tx.meta.innerInstructions) {
|
|
1460
|
+
for (const ix of group.instructions) {
|
|
1461
|
+
let data;
|
|
1462
|
+
try {
|
|
1463
|
+
data = new Uint8Array(base58Bytes.encode(ix.data));
|
|
1464
|
+
} catch {
|
|
1465
|
+
return at(null, "rpc_unavailable");
|
|
1466
|
+
}
|
|
1467
|
+
consider(ix.programIdIndex, ix.accounts, data);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
const checked = transfers.filter((t) => t.tag === IX_TRANSFER_CHECKED);
|
|
1471
|
+
if (checked.length === 0) return at(false, "no_matching_transfer");
|
|
1472
|
+
if (checked.length > 1 || transfers.length > 1) return at(false, "multiple_matching_transfers");
|
|
1473
|
+
const [transfer] = checked;
|
|
1474
|
+
if (transfer.data.length < 10 || transfer.accounts.length < 4)
|
|
1475
|
+
return at(false, "no_matching_transfer");
|
|
1476
|
+
const [source, mint, destination, authority] = transfer.accounts;
|
|
1477
|
+
const trust = Object.prototype.hasOwnProperty.call(trusted, asset) ? trusted[asset] : void 0;
|
|
1478
|
+
if (!trust) return at(false, "untrusted_mint");
|
|
1479
|
+
if (!trust.tokenPrograms.includes(transfer.program))
|
|
1480
|
+
return at(false, "untrusted_token_program");
|
|
1481
|
+
if (mint !== asset) return at(false, "mint_mismatch");
|
|
1482
|
+
if (transfer.data[9] !== trust.decimals) return at(false, "decimals_mismatch");
|
|
1483
|
+
const [expectedDestination] = await kit.getProgramDerivedAddress({
|
|
1484
|
+
programAddress: kit.address(ASSOCIATED_TOKEN_PROGRAM),
|
|
1485
|
+
seeds: [
|
|
1486
|
+
kit.getAddressEncoder().encode(kit.address(input.payTo)),
|
|
1487
|
+
kit.getAddressEncoder().encode(kit.address(transfer.program)),
|
|
1488
|
+
kit.getAddressEncoder().encode(kit.address(asset))
|
|
1489
|
+
]
|
|
1490
|
+
});
|
|
1491
|
+
if (destination !== expectedDestination) return at(false, "payee_mismatch");
|
|
1492
|
+
if (readU64(transfer.data, 1) !== BigInt(input.amountAtomic))
|
|
1493
|
+
return at(false, "amount_mismatch");
|
|
1494
|
+
if (authority !== input.payer) return at(false, "payer_mismatch");
|
|
1495
|
+
const [expectedSource] = await kit.getProgramDerivedAddress({
|
|
1496
|
+
programAddress: kit.address(ASSOCIATED_TOKEN_PROGRAM),
|
|
1497
|
+
seeds: [
|
|
1498
|
+
kit.getAddressEncoder().encode(kit.address(input.payer)),
|
|
1499
|
+
kit.getAddressEncoder().encode(kit.address(transfer.program)),
|
|
1500
|
+
kit.getAddressEncoder().encode(kit.address(asset))
|
|
1501
|
+
]
|
|
1502
|
+
});
|
|
1503
|
+
if (source !== expectedSource) return at(false, "payer_mismatch");
|
|
1504
|
+
return at(true, null);
|
|
1505
|
+
}
|
|
1506
|
+
return Object.freeze(Object.assign(verify, { cluster }));
|
|
1507
|
+
}
|
|
1508
|
+
function decodeWire(kit, base64) {
|
|
1509
|
+
const bytes = kit.getBase64Encoder().encode(base64);
|
|
1510
|
+
const decoded = kit.getTransactionDecoder().decode(bytes);
|
|
1511
|
+
const signatures = {};
|
|
1512
|
+
for (const [addr, sig] of Object.entries(decoded.signatures)) {
|
|
1513
|
+
signatures[addr] = sig ? new Uint8Array(sig) : null;
|
|
1514
|
+
}
|
|
1515
|
+
return { messageBytes: new Uint8Array(decoded.messageBytes), signatures };
|
|
1516
|
+
}
|
|
1149
1517
|
export {
|
|
1150
1518
|
CreditLedger,
|
|
1519
|
+
DEFAULT_RPC_TIMEOUT_MS,
|
|
1151
1520
|
DEFAULT_SOLANA_USD_MINTS,
|
|
1152
1521
|
DEFAULT_TEMPO_USD_ASSETS,
|
|
1522
|
+
DEFAULT_TRUSTED_USD_MINTS,
|
|
1523
|
+
InMemoryReceiptReferenceIndex,
|
|
1524
|
+
LANDING_WINDOW_SECONDS,
|
|
1153
1525
|
MPP_FULFILLMENT_MAX_BODY_BYTES,
|
|
1154
1526
|
MPP_FULFILLMENT_RECORD_SCHEMA,
|
|
1155
1527
|
MppFlushTimeoutError,
|
|
1156
1528
|
MppGateError,
|
|
1157
1529
|
MppPaidNoCompletionError,
|
|
1530
|
+
RECEIPT_REFERENCE_NOT_EVALUATED,
|
|
1158
1531
|
ReplayRejectedError,
|
|
1159
1532
|
SpentProofLedger,
|
|
1160
1533
|
createMppFetch,
|
|
1534
|
+
createReceiptChainVerifier,
|
|
1161
1535
|
createSolanaMppFetch,
|
|
1162
1536
|
creditPaidNoCompletion,
|
|
1163
1537
|
fulfillPaidProof,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twzrd1111/clawrouter-mpp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|