Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ harness/go-client/go-client
mpp-sdk-self-learning/
.build/
go/coverage.out
notes/codex-review/
49 changes: 49 additions & 0 deletions harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,55 @@ Use these environment variables to filter the active matrix:
- `MPP_INTEROP_INTENTS=charge`
- `MPP_INTEROP_SCENARIOS=charge-basic,charge-split-ata,charge-network-mismatch,charge-cross-route-replay`

### x402 exact intent

A second intent, `x402-exact`, exercises the canonical x402 `exact` scheme
against the Rust spine in `rust/crates/x402/src/bin/interop_{client,server}.rs`.
The TypeScript reference adapters live at
`src/fixtures/typescript/exact-{client,server}.ts` and share the same
harness contract as the Rust spine: identical `X402_INTEROP_*` env vars,
identical `PAYMENT-REQUIRED` / `PAYMENT-SIGNATURE` headers, identical
ready / result JSON shapes. The TS reference fixture carries a stub
credential payload (challenge id + resource) and is paired against the
TS reference server in the default matrix; the Rust spine is paired
against itself. As language adapters that carry a real Solana
PaymentProof land, they expand the matrix by registering under
`intents: ["x402-exact"]` in `implementations.ts`.

Env vars consumed by both roles:

- `X402_INTEROP_RPC_URL`, `X402_INTEROP_NETWORK`, `X402_INTEROP_MINT`
- `X402_INTEROP_PAY_TO`, `X402_INTEROP_PRICE`
- `X402_INTEROP_FACILITATOR_SECRET_KEY`

Server-only:

- `X402_INTEROP_EXTRA_OFFERED_MINTS` (CSV of additional mint addresses)

Client-only:

- `X402_INTEROP_TARGET_URL`
- `X402_INTEROP_CLIENT_SECRET_KEY`
- `X402_INTEROP_PREFER_CURRENCIES` (CSV of preferred currencies)

Run the x402 matrix slice:

```bash
X402_INTEROP_MATRIX=1 \
X402_INTEROP_RPC_URL=http://127.0.0.1:8899 \
X402_INTEROP_MINT=... X402_INTEROP_PAY_TO=... \
X402_INTEROP_CLIENT_SECRET_KEY='[...]' \
X402_INTEROP_FACILITATOR_SECRET_KEY='[...]' \
pnpm test x402-exact.e2e.test.ts
```

Cross-server portability and idempotent-resubmit scenarios are gated
separately:

```bash
X402_INTEROP_CROSS_SERVER=1 pnpm test cross-server-scenarios.test.ts
```

The current scenario set covers only the `charge` intent. It includes a basic
payment, a split payment that requires the server fee payer to create the split
recipient ATA, a negative network-mismatch payment, and a cross-route replay
Expand Down
21 changes: 15 additions & 6 deletions harness/src/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { CanonicalErrorCode } from "./canonical-codes";
import { chargeScenarios } from "./intents/charge";
import { x402ExactScenarios } from "./intents/x402-exact";

export type { CanonicalErrorCode };

export type AdapterKind = "client" | "server";

export type InteropIntent = "charge";
export type InteropIntent = "charge" | "x402-exact";

export type InteropScenarioSplit = {
recipientKey: string;
Expand Down Expand Up @@ -136,8 +137,10 @@ export type AdapterMessage = ReadyMessage | ClientRunResult;

export { chargeCanonicalJsonVectors } from "./intents/charge";

export const interopScenarios: readonly InteropScenario[] =
chargeScenarios;
export const interopScenarios: readonly InteropScenario[] = [
...chargeScenarios,
...x402ExactScenarios,
];

export const interopScenario: InteropScenario = {
...(interopScenarios[0] as InteropScenario),
Expand Down Expand Up @@ -191,11 +194,18 @@ function selectScenarioIds(rawSelection: string | undefined): string[] {
return selected;
}

// The legacy MPP charge runner predates the x402-exact intent. To keep
// the existing CI matrix's default behaviour (charge-only) stable while
// still surfacing the new intent through `selectInteropIntents("x402-exact")`,
// the empty-selection default is restricted to "charge". Callers that
// want the full intent set should pass the explicit list.
const DEFAULT_INTENTS: readonly InteropIntent[] = ["charge"];

export function selectInteropIntents(
rawSelection: string | undefined,
): InteropIntent[] {
if (!rawSelection || rawSelection.trim() === "") {
return [...supportedInteropIntents];
return [...DEFAULT_INTENTS];
}

const selected = rawSelection
Expand All @@ -209,8 +219,7 @@ export function selectInteropIntents(
if (unsupported.length > 0) {
throw new Error(
`Unsupported MPP_INTEROP_INTENTS value(s): ${unsupported.join(", ")}. ` +
`Supported intents: ${supportedInteropIntents.join(", ")}. ` +
"Session and subscription scenarios are not implemented in this harness yet.",
`Supported intents: ${supportedInteropIntents.join(", ")}.`,
);
}

Expand Down
225 changes: 225 additions & 0 deletions harness/src/fixtures/typescript/exact-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// TypeScript reference x402 `exact` interop client.
//
// Shares the same `X402_INTEROP_*` env-var contract and ready/result
// JSON protocol as the Rust spine (`rust/crates/x402/src/bin/
// interop_client.rs`). Sends an unpaid GET, parses the base64
// `PAYMENT-REQUIRED` envelope, selects an offer (`preferredCurrencies`
// first) and resubmits with `PAYMENT-SIGNATURE`. Prints one result
// JSON line to stdout.
//
// Scope: the fixture carries a stub credential payload (challenge id +
// resource) so the harness wiring, negative-code classification, and
// cross-server portability + idempotent-resubmit flows can run without
// a full Solana signer. Real SVM PaymentProof construction (signed
// VersionedTransaction or settled signature) lives in the Rust spine
// and the TS SDK port; this client only pairs against the TS reference
// server in the default matrix (see `test/x402-exact.e2e.test.ts`).

import {
PAYMENT_REQUIRED_HEADER,
PAYMENT_SIGNATURE_HEADER,
readX402ClientEnvironment,
} from "./exact-shared";

type PaymentRequirement = {
scheme: string;
network: string;
resource?: string;
payTo: string;
asset: string;
maxAmountRequired: string;
extra?: { decimals?: number; tokenProgram?: string };
};

type PaymentRequiredEnvelope = {
x402Version: number;
accepts: PaymentRequirement[];
resource?: string;
};

const STABLECOIN_MINTS: Record<string, Record<string, string>> = {
USDC: {
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp":
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1":
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
},
PYUSD: {
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp":
"2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo",
},
};

function resolveMint(currency: string, network: string): string {
const upper = currency.toUpperCase();
const byNetwork = STABLECOIN_MINTS[upper];
if (byNetwork && byNetwork[network]) {
return byNetwork[network];
}
return currency;
}

function pickOffer(
envelope: PaymentRequiredEnvelope,
preferred: string[],
network: string,
): PaymentRequirement | undefined {
const supported = envelope.accepts.filter(
offer => offer.scheme === "exact" && offer.network === network,
);
if (supported.length === 0) {
return undefined;
}
if (preferred.length === 0) {
return supported[0];
}
for (const wanted of preferred) {
const wantedMint = resolveMint(wanted, network);
const match = supported.find(offer => offer.asset === wantedMint);
if (match) return match;
}
return supported[0];
}

function decodePaymentRequired(headerValue: string | null): PaymentRequiredEnvelope | null {
if (!headerValue) return null;
try {
const raw = Buffer.from(headerValue, "base64").toString("utf8");
return JSON.parse(raw) as PaymentRequiredEnvelope;
} catch {
return null;
}
}

async function readResponseBody(response: Response): Promise<unknown> {
const raw = await response.text();
try {
return JSON.parse(raw);
} catch {
return raw;
}
}

async function main() {
const env = readX402ClientEnvironment();

const firstResponse = await fetch(env.targetUrl);
const envelope = decodePaymentRequired(
firstResponse.headers.get(PAYMENT_REQUIRED_HEADER),
);

if (!envelope) {
console.log(
JSON.stringify({
type: "result",
implementation: "typescript",
role: "client",
ok: false,
status: firstResponse.status,
responseHeaders: Object.fromEntries(firstResponse.headers.entries()),
responseBody: await readResponseBody(firstResponse),
settlement: null,
error: "missing or unparseable PAYMENT-REQUIRED header",
}),
);
return;
}

const offer = pickOffer(envelope, env.preferredCurrencies, env.network);
if (!offer) {
console.log(
JSON.stringify({
type: "result",
implementation: "typescript",
role: "client",
ok: false,
status: firstResponse.status,
responseHeaders: Object.fromEntries(firstResponse.headers.entries()),
responseBody: await readResponseBody(firstResponse),
settlement: null,
error: `no offer matched network ${env.network}`,
}),
);
return;
}

// Credential payload mirrors the canonical x402 `exact` shape: an
// adapter-specific id plus the offer the client is committing to.
// A live SDK would also embed a signed Solana transaction here; the
// matrix runner uses the rust spine for the actual on-chain
// settlement assertions. The TS fixture's role is wire-level
// protocol compliance.
// Use the server-issued challenge id if present (TS reference server
// emits one in the `x-challenge-id` header on the 402). This lets the
// server verify the credential was issued against its own 402 — the
// cross-server portability scenario relies on this distinction.
const issuedChallengeId = firstResponse.headers.get("x-challenge-id");
const credentialId =
issuedChallengeId ??
`ts-x402-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
// Mirrors the Rust spine's PaymentPayload wire shape:
// { x402Version, accepted: { scheme, network, asset, payTo, amount, extra? },
// payload: { ... scheme-specific blob ... }, resource?: string }
// The `payload` field is required by Rust's parser. For the wire-only
// TS adapter the payload carries the credential id plus the route the
// client is committing to; a full SDK fixture would carry a signed
// Solana transaction here.
const credential = {
x402Version: envelope.x402Version,
accepted: {
scheme: offer.scheme,
network: offer.network,
asset: offer.asset,
payTo: offer.payTo,
amount: offer.maxAmountRequired,
extra: offer.extra ?? null,
},
payload: {
challengeId: credentialId,
resource: offer.resource ?? envelope.resource,
},
resource: offer.resource ?? envelope.resource,
};
const credentialHeader = Buffer.from(JSON.stringify(credential), "utf8").toString(
"base64",
);

const paidResponse = await fetch(env.targetUrl, {
headers: { [PAYMENT_SIGNATURE_HEADER]: credentialHeader },
});

const responseHeaders = Object.fromEntries(paidResponse.headers.entries());
// Echo the credential the client sent so the harness can replay it in
// cross-server portability + idempotent-resubmit scenarios. The credential
// is a request header so it is never reflected in the response on its own.
responseHeaders[`${PAYMENT_SIGNATURE_HEADER}-sent`] = credentialHeader;

console.log(
JSON.stringify({
type: "result",
implementation: "typescript",
role: "client",
ok: paidResponse.ok,
status: paidResponse.status,
responseHeaders,
responseBody: await readResponseBody(paidResponse),
settlement: paidResponse.headers.get(env.settlementHeader),
}),
);
}

void main().catch(error => {
console.log(
JSON.stringify({
type: "result",
implementation: "typescript",
role: "client",
ok: false,
status: 0,
responseHeaders: {},
responseBody: null,
settlement: null,
error: error instanceof Error ? error.message : String(error),
}),
);
});
Loading
Loading