@twzrd1111/clawrouter-mpp 0.4.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 +26 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +59 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
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
|
+
|
|
6
28
|
## Upgrading to 0.4.0 from 0.3.0
|
|
7
29
|
|
|
8
30
|
This release includes the standalone Solana receipt verifier (#16), fulfillment wiring (#18),
|
|
@@ -126,8 +148,10 @@ the verifier and writes its result into the row:
|
|
|
126
148
|
- `receipt_reference_chain_verified` (`true`, `false`, or `null`), `receipt_reference_finalized_slot`,
|
|
127
149
|
`receipt_reference_mismatch` (why `false`), and `receipt_reference_unverified_reason` (why `null`;
|
|
128
150
|
`verifier_error` when the verifier threw).
|
|
129
|
-
- `receipt_reference_purchase_bound` and its reason.
|
|
130
|
-
|
|
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.
|
|
131
155
|
- `receipt_reference_local_acceptance` (`accepted`, `refused`, or `null`) and
|
|
132
156
|
`receipt_reference_refusal_reason` (`reference_reused`, `stale_by_local_clock`): this sink's
|
|
133
157
|
policy, which never changes a chain field.
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,18 @@ 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 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;
|
|
6
18
|
/** A blockhash stays valid for about 150 slots, roughly 60-90 s. */
|
|
7
19
|
declare const LANDING_WINDOW_SECONDS = 90;
|
|
8
20
|
declare const CLUSTER_IDS: {
|
|
@@ -112,6 +124,8 @@ type ReceiptChainVerifyInput = {
|
|
|
112
124
|
signedAtUnix: number | null;
|
|
113
125
|
/** Buyer-generated id of the logical purchase (not the seller's challenge id). */
|
|
114
126
|
purchaseId: string;
|
|
127
|
+
/** Buyer-generated Echo expectation, retained only after exact pre-sign reflection. */
|
|
128
|
+
expectedExternalId?: string | null;
|
|
115
129
|
};
|
|
116
130
|
/** A built verifier. `cluster` lets a rail refuse a verifier configured for another cluster. */
|
|
117
131
|
type ReceiptChainVerify = ((input: ReceiptChainVerifyInput) => Promise<ReceiptChainVerification>) & {
|
|
@@ -272,6 +286,15 @@ declare function createMppFetch(account: Account, base: typeof fetch | undefined
|
|
|
272
286
|
|
|
273
287
|
declare const DEFAULT_SOLANA_USD_MINTS: ReadonlySet<string>;
|
|
274
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";
|
|
275
298
|
type SolanaMppFetchOptions = {
|
|
276
299
|
signer: KeyPairSigner;
|
|
277
300
|
rpcUrl: string;
|
|
@@ -287,6 +310,8 @@ type SolanaMppFetchOptions = {
|
|
|
287
310
|
* omitted (the row's receipt_reference_* verification fields stay null). Never delays the response.
|
|
288
311
|
*/
|
|
289
312
|
receiptVerification?: SolanaReceiptVerificationOptions;
|
|
313
|
+
/** Opt in to buyer nonce reflection. Omit to preserve existing seller externalId behavior. */
|
|
314
|
+
echo?: EchoPolicy;
|
|
290
315
|
};
|
|
291
316
|
type SolanaReceiptVerificationOptions = {
|
|
292
317
|
/** From createReceiptChainVerifier, built for the same cluster as `network`. */
|
|
@@ -389,5 +414,5 @@ declare function fulfillPaidProof<T>(opts: {
|
|
|
389
414
|
upstream: () => Promise<T>;
|
|
390
415
|
}): Promise<T>;
|
|
391
416
|
|
|
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 };
|
|
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
|
@@ -912,7 +912,7 @@ function createMppFetch(account, base = fetch, opts) {
|
|
|
912
912
|
}
|
|
913
913
|
|
|
914
914
|
// src/mpp-solana-fetch.ts
|
|
915
|
-
import { randomUUID } from "crypto";
|
|
915
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
916
916
|
import {
|
|
917
917
|
assertSpendPolicyAllows as assertSpendPolicyAllows2,
|
|
918
918
|
SpendPolicyError as SpendPolicyError3
|
|
@@ -922,6 +922,23 @@ var DEFAULT_SOLANA_USD_MINTS = /* @__PURE__ */ new Set([
|
|
|
922
922
|
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
923
923
|
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
|
924
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
|
+
}
|
|
925
942
|
var CAIP2 = {
|
|
926
943
|
mainnet: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
927
944
|
devnet: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
@@ -997,6 +1014,8 @@ async function createSolanaMppFetch(opts) {
|
|
|
997
1014
|
opts.spend,
|
|
998
1015
|
async () => {
|
|
999
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);
|
|
1000
1019
|
const requestForRecord = request.clone();
|
|
1001
1020
|
const probe = await base(request.clone());
|
|
1002
1021
|
if (probe.status !== 402) return probe;
|
|
@@ -1024,6 +1043,7 @@ async function createSolanaMppFetch(opts) {
|
|
|
1024
1043
|
);
|
|
1025
1044
|
const payment = paymentFactsFromMppChallenge(challenge);
|
|
1026
1045
|
markPaymentChallenge(payment);
|
|
1046
|
+
const expectedExternalId = echoContext && assertEchoPreSign(challenge.request.externalId, echoContext, opts.echo) === "bound" ? echoContext.expectedExternalId : null;
|
|
1027
1047
|
const key = JSON.stringify([
|
|
1028
1048
|
challenge.id,
|
|
1029
1049
|
challenge.realm,
|
|
@@ -1112,7 +1132,8 @@ async function createSolanaMppFetch(opts) {
|
|
|
1112
1132
|
signedTransaction,
|
|
1113
1133
|
signedAtUnix,
|
|
1114
1134
|
purchaseId: randomUUID(),
|
|
1115
|
-
hasSplits: Array.isArray(splits) && splits.length > 0
|
|
1135
|
+
hasSplits: Array.isArray(splits) && splits.length > 0,
|
|
1136
|
+
expectedExternalId
|
|
1116
1137
|
};
|
|
1117
1138
|
} catch (err) {
|
|
1118
1139
|
if (signed) {
|
|
@@ -1159,7 +1180,9 @@ async function createSolanaMppFetch(opts) {
|
|
|
1159
1180
|
payer: opts.signer.address,
|
|
1160
1181
|
signedTransaction: result.signedTransaction,
|
|
1161
1182
|
signedAtUnix: result.signedAtUnix,
|
|
1162
|
-
purchaseId: result.purchaseId
|
|
1183
|
+
purchaseId: result.purchaseId,
|
|
1184
|
+
// Binding belongs to this call, not merely a cached credential.
|
|
1185
|
+
expectedExternalId: result.expectedExternalId === echoContext?.expectedExternalId ? result.expectedExternalId : null
|
|
1163
1186
|
}),
|
|
1164
1187
|
verification.waitMs,
|
|
1165
1188
|
verification.pollMs
|
|
@@ -1248,6 +1271,23 @@ async function fulfillPaidProof(opts) {
|
|
|
1248
1271
|
|
|
1249
1272
|
// src/receipt-chain-verify.ts
|
|
1250
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
|
+
}
|
|
1251
1291
|
var TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
1252
1292
|
var TOKEN_2022_PROGRAM = "TokenzQdBNbLqP7VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
|
|
1253
1293
|
var ASSOCIATED_TOKEN_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
@@ -1342,12 +1382,10 @@ function createReceiptChainVerifier(config) {
|
|
|
1342
1382
|
async function verify(input) {
|
|
1343
1383
|
const chain = await chainFacts(input);
|
|
1344
1384
|
const verified = chain.receipt_reference_chain_verified;
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
receipt_reference_purchase_bound: null,
|
|
1350
|
-
receipt_reference_purchase_bound_reason: "chain_unverified"
|
|
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
|
|
1351
1389
|
};
|
|
1352
1390
|
let refusal = null;
|
|
1353
1391
|
if (chain.blockTime !== null && input.signedAtUnix !== null && Number.isFinite(input.signedAtUnix) && input.signedAtUnix - chain.blockTime > maxAge) {
|
|
@@ -1501,7 +1539,13 @@ function createReceiptChainVerifier(config) {
|
|
|
1501
1539
|
]
|
|
1502
1540
|
});
|
|
1503
1541
|
if (source !== expectedSource) return at(false, "payer_mismatch");
|
|
1504
|
-
return
|
|
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
|
+
};
|
|
1505
1549
|
}
|
|
1506
1550
|
return Object.freeze(Object.assign(verify, { cluster }));
|
|
1507
1551
|
}
|
|
@@ -1522,6 +1566,8 @@ export {
|
|
|
1522
1566
|
DEFAULT_TRUSTED_USD_MINTS,
|
|
1523
1567
|
InMemoryReceiptReferenceIndex,
|
|
1524
1568
|
LANDING_WINDOW_SECONDS,
|
|
1569
|
+
MEMO_PROGRAM_ID,
|
|
1570
|
+
MEMO_V1_PROGRAM_ID,
|
|
1525
1571
|
MPP_FULFILLMENT_MAX_BODY_BYTES,
|
|
1526
1572
|
MPP_FULFILLMENT_RECORD_SCHEMA,
|
|
1527
1573
|
MppFlushTimeoutError,
|
|
@@ -1530,10 +1576,13 @@ export {
|
|
|
1530
1576
|
RECEIPT_REFERENCE_NOT_EVALUATED,
|
|
1531
1577
|
ReplayRejectedError,
|
|
1532
1578
|
SpentProofLedger,
|
|
1579
|
+
assertEchoPreSign,
|
|
1580
|
+
createEchoContext,
|
|
1533
1581
|
createMppFetch,
|
|
1534
1582
|
createReceiptChainVerifier,
|
|
1535
1583
|
createSolanaMppFetch,
|
|
1536
1584
|
creditPaidNoCompletion,
|
|
1585
|
+
evaluatePurchaseBinding,
|
|
1537
1586
|
fulfillPaidProof,
|
|
1538
1587
|
quoteFromChallenge,
|
|
1539
1588
|
readCredits,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twzrd1111/clawrouter-mpp",
|
|
3
|
-
"version": "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",
|