test(harness): full x402 exact interop matrix (client/server cross-pairs + parity locks)#3
Open
EfeDurmaz16 wants to merge 10 commits into
Open
test(harness): full x402 exact interop matrix (client/server cross-pairs + parity locks)#3EfeDurmaz16 wants to merge 10 commits into
EfeDurmaz16 wants to merge 10 commits into
Conversation
…irs + parity locks) Add three-tier x402-exact test architecture on top of solana-foundation#132: 1. Wire compat (no RPC, default `pnpm test`): - `harness/test/x402-exact.compat.test.ts` - Drives every registered x402-exact adapter (gated by COMPAT_INCLUDE_IDS) against canonical fixtures and an attack suite. Catches wire-format drift before the live matrix runs. 2. Parity-lock fixtures (`harness/fixtures/x402-exact/`): - canonical-challenge.json — 402 envelope every client must parse. - canonical-payment-signature.json — credential every server must parse (accept or reject with a known token). - canonical-reject-tokens.json — union of high-level reject tokens and the invalid_exact_svm_payload_* family mirrored from rust/crates/x402/src/protocol/schemes/exact/verify.rs. - attack-scenarios.json — 9 tampered credential scenarios + replay. 3. Live full matrix (`harness/test/x402-exact.live.matrix.test.ts`): - Env-gated (X402_INTEROP_MATRIX=1 + funded keypair). Enumerates every allowedPair from the policy in implementations.ts and runs the happy path. Widens automatically as new adapters register. Also expand `harness/test/x402-exact.e2e.test.ts` with an explicit self-pair group so per-language regressions stand out in vitest output, and update `harness/README.md` with the three-tier documentation and extension recipe.
…lback
Address review findings on the x402-exact matrix:
- Drop blanket `payment_invalid` fallback in attack-scenario assertion;
only wire-only adapters (WIRE_ONLY_ADAPTER_IDS) may emit the generic
token. Full verifiers must emit a specific reject token per scenario.
- Rework extractRejectToken: the Rust spine wraps verifier failures as
`{ error: "payment_invalid", message: "<verifier-token>: ..." }`, so
the most-specific token is in `message`, not `error`. Search every
field for a known taxonomy token (svm-payload tokens before
high-level) and return that; previously the test masked specific
tokens behind the high-level error.
- Replay test now requires the first submission to be accepted; a
server that rejects every credential previously passed trivially.
- Reject-token taxonomy is now strict-checked against the rust spine
source (rust/crates/x402/src/protocol/schemes/exact/verify.rs): the
fixture set must equal the set of `invalid_exact_svm_payload_*`
literals in the spine. Token add/remove/rename in the spine fails
the test with a pointed diff.
- Add canonical-payment-signature-rust.json with the Rust-spine
PaymentProof::Transaction shape (vs the existing TS-wire stub).
- Reframe TS-wire fixture descriptions to honestly document the
PaymentRequiredEnvelope `resource: ResourceInfo` and
`payload: PaymentProof` differences vs the Rust spine.
- Replace `it.fails` skip in the live matrix with a hard `it` failure
so a broken scenario registry fails CI loudly.
- Remove generic `payment_invalid` from per-scenario expectedRejectTokens
in attack-scenarios.json. Wire-only adapters still get the fallback
via WIRE_ONLY_ADAPTER_IDS in the test runner; full verifiers must now
emit a specific token (no silent regression to generic rejection).
- Document each scenario's true scope: wire-binding checks (rejected by
the TS reference's classifyCredential / rust spine's requirement
binding) vs SVM transaction structural checks (live matrix only).
- Tone down canonical-payment-signature-rust.json description: the
placeholder transaction fails bincode-deserialization BEFORE
verify.rs runs, so the fixture asserts envelope parsing + structured
402 emission, not `invalid_exact_svm_payload_*` tokens.
- Add `once("error")` rejection on the in-process fixture server's
listen call so EPERM/EADDRINUSE fails the test cleanly instead of
hanging 60s on the adapter timeout.
- X402_COMPAT_INCLUDE_RUST=1 opts the rust-x402 adapter into the compat suite (off by default to keep `pnpm test` cargo-free). - Replay assertion gated by WIRE_ONLY_ADAPTER_IDS + opt-in X402_COMPAT_REPLAY_TRUST list: adapters whose verifier requires a real signed transaction (rust spine) skip the stub-credential replay test cleanly with a documented skip message; live matrix covers replay against them with a real PaymentProof::Transaction. - README documents both opt-in flags.
…olicy - Default full-verifier behavior: server adapters not in WIRE_ONLY_ADAPTER_IDS that accept the TS-wire stub credential are now flagged as a verifier bypass. Opt-in via X402_COMPAT_STUB_ACCEPT (CSV) for adapters whose verifier accepts the stub by design. - Drop payment_invalid fallback for the replay second-submit assertion: once first=200 the second submission MUST be classified as signature_consumed by every adapter (no generic-rejection regression). - Add explicit canonical-payment-signature-rust.json shape lock: every field rust spine's PaymentSignatureEnvelope requires must be present, payload must deserialize as PaymentProof::Transaction xor PaymentProof::Signature, base64 round-trip stable. Fixture can no longer drift undetected. - Reject-token taxonomy match is now longest-first so suffixed tokens (e.g. ..._compute_price_instruction_too_high) match before their prefix (..._compute_price_instruction). - Extract allowedX402Pair / baseLang / isRustSpine to src/x402-pair-policy.ts so the e2e and live-matrix tests share one source of truth and cannot drift apart silently.
…pat keypair requirement - Add console.warn for live-matrix skip-due-to-missing-env so CI matrix misconfiguration is visible in job logs (per spec the behavior remains skip-not-fail, since the matrix is opt-in by env). - Document that X402_COMPAT_INCLUDE_RUST=1 requires real ed25519 keypairs (rust spine validates via MemorySigner::from_bytes); the README and inline comment make this contract explicit.
The TS-wire canonical credential carries `payload.challengeId/resource`, which the rust spine rejects at PaymentProof deserialization with the generic `payment_invalid` token — defeating the per-scenario specific-token assertions that make the compat suite robust. Rather than ship a half-functional opt-in, drop it: the compat suite is now honestly TS-only, and the live matrix (tier 3) is the canonical place for rust spine coverage. README documents the rationale.
Two Codex r8 P2 findings on the x402 harness matrix:
1) TS x402 fixture server gated its cross-server portability rejection
behind `issued.size > 0`, so a freshly started server (or one that
had not yet issued any 402) would accept any challengeId. Drop the
size guard so the membership check fires unconditionally. The
happy-path flow (GET /protected -> 402 with challengeId -> POST
with challengeId) is unaffected because the served challengeId is
added to `issued` on the 402 path before the client returns.
2) `cross-server-scenarios.test.ts:extractCanonicalCode` searched
`error` before `message`. The Rust x402 interop server wraps
verifier failures as `{ error: "payment_invalid", message:
"<specific-verifier-token>" }`, so the first-match strategy
resolved to the generic `payment_invalid` and silently discarded
the verifier-specific token that the canonical taxonomy needs to
classify. Combine both fields into one string before classifying
so the richer signal survives.
The cross-server portability scenario previously listed a single TS->Rust crossServerPair. Add the TS->TS control pair so the assertion exercises the canonical challenge_verification_failed reject token on the TS reference server itself (two independent server instances issue disjoint challenge id sets), not only on the Rust spine's proof-layer reject path. Document why the reverse Rust->TS direction is gated to the live matrix: the Rust spine adapter does not echo the captured credential to the harness by design, so credential-capture replay flows can only originate from the TS client; the canonical Rust->TS portability is asserted end-to-end via the live matrix where a real signed transaction is exchanged.
4f85dee to
c25cf77
Compare
EfeDurmaz16
added a commit
that referenced
this pull request
May 28, 2026
Mega-port of issue solana-foundation#137 — Go SDK design — applying lessons from Ruby PR solana-foundation#142, Lua PR solana-foundation#141, and PHP PR solana-foundation#145. paykit/ types.go Scheme, Stablecoin, Network, Address, Denom, Price, Operator, X402Config, MPPConfig, Config, Payment. Network.DefaultRPCURL/MintsLabel/CAIP2 (caveats #1, #2 from Ruby PR solana-foundation#142). errors.go Sentinel errors (ErrPaymentRequired, ErrInvalidProof, ErrChallengeExpired, ErrMixedDenoms, ErrSchemeIncompatible, ErrDemoSignerOnMainnet, ErrInvalidConfig) + PaymentError + GateError. price.go ParseUSD/EUR/GBP + MustParse* boot-time variants; shopspring/decimal under the hood, never float64. gate.go Gate value + Total/Payout/HasFees/Validate. Validate enforces mixed-denom, sum(FeeWithin)<=Amount, x402+fees-incompatible rules. signer.go Signer interface (Pubkey/Sign/IsDemo/SecretKey). mints.go Cross-language ResolveMint + TokenProgramFor (caveat #1: localnet falls back to mainnet mint row). client.go New() resolves zero-value defaults, wires registered adapter builders, runs preflight. DefaultSigner hook avoids the paykit -> signer -> paykit import cycle. middleware.go Client.Require(Gate) + Client.RequireFunc(GateFunc) return func(http.Handler) http.Handler. Context- attached *Payment via private ctxKey{} per the log/slog convention. PaymentFrom / IsPaid / IsPaidFor accessors. preflight.go MPP HMAC secret auto-resolution (caveat solana-foundation#4): env -> ./.env -> generate + persist mode 0600. paykit/signer/ signer.go Local Ed25519 factories: Demo / Generate / FromBytes / FromJSON / FromHex / FromBase58 / FromFile / FromEnv + MustXxx variants. Demo pubkey matches Ruby/Lua/PHP: ALtYSsZuYyKrNSe6GnVCzxj1T2RPMTPzXMe51xhbmXEq. Registers Demo via paykit.DefaultSigner in init(). paykit/schemes/mpp/ mpp.go Wraps the existing server.Mpp charge handler in the Adapter contract. acceptsEntry advertises methodDetails.network as the short slug so pay --sandbox --mpp curl matches the active wallet (the PHP fix from PR solana-foundation#145). paykit/schemes/x402/ x402.go x402-exact adapter: 402 envelope with recentBlockhash in extra (caveat solana-foundation#5), base64 + Solana versioned-tx decode, partial-sign as facilitator, sendRawTx via gagliardetto/solana-go RpcClient. Replay-store reservation in memory (paykit.Store interface pluggable later). cmd/harness-server/ main.go Cross-language harness adapter binary. Reads X402_INTEROP_* or MPP_INTEROP_* env (or PAY_KIT_INTEROP_PROTOCOL hint), boots paykit.Client, emits the ready JSON, serves /paid. Mirrors harness/ruby-server, lua-server, php-server. examples/paykit-server/ main.go Dual-protocol localnet demo. Boots a paykit.Client with the demo signer, gates /paid behind a /bin/zsh.10 USDC charge. Tooling: - go/Justfile: install / build / test / fmt / lint / audit / test-cover / check / serve-example targets, mirroring Ruby + Lua + PHP. - .github/workflows/go.yml: paykit + paykit/signer added to the package list in the test step; new interop-go-paykit job that builds rust-x402 + the harness binary and runs the dual-protocol matrix against the Go server (typescript client -> go-paykit for MPP charge, rust-x402 client -> go-paykit for x402-exact). - harness/src/implementations.ts: registers the go-paykit server with intents [charge, x402-exact]. Manual DX verified end-to-end against the locally built solana-foundation/pay@feat/internals: pay --sandbox --x402 curl http://127.0.0.1:4567/paid -> 200 signature 3Bzkj2P8si6tsgVpyGpVJGF6BD5WhYS3fewsMQV6B1f7LWJTX1k3oHDmUd5TCYJ6PzAGTZpq9KTN7Lx1S3fhxL3G pay --sandbox --mpp curl http://127.0.0.1:4567/paid -> 200 signature 2ZMspVR99ipbpMUX3CMsmxsPb5hLbHg6h78vYxYDJ9MrfHfGqDihoo1aFkBBTGhfxGJ2Jrq1vjrxagBjBPvM4DJj go test ./paykit/... green. Adapter packages compile clean; further unit coverage + the live surfnet auto-bootstrap (caveat #3) land in follow-up commits in this same branch.
EfeDurmaz16
added a commit
that referenced
this pull request
May 28, 2026
…undation#6 solana-foundation#7 Two fixes after rereading the issue body + Efe's comment carefully: (1) Rename paykit/schemes -> paykit/protocols. Cross-language convention across Ruby (lib/pay_kit/protocols), PHP (src/Protocols), and Lua (pay_kit/protocols) uses 'protocols'; the initial sketch followed the schemes label from Ludo's draft but should track the established naming. - go/paykit/schemes/{mpp,x402} -> go/paykit/protocols/{mpp,x402} - All import sites updated (examples, cmd/harness-server, test fixtures). (2) Apply caveats that were partially or entirely missed: Caveat #3 -- live preflight with Surfnet cheatcode auto-bootstrap. paykit/preflight.go is now a real check, not a stub: - checkFeePayerSOL: getBalance via the resolved RPC, compares against minFeePayerLamports (1_000_000). On localnet + demo signer + balance below the floor, fires surfnet_setAccount with autofundLamports (10 SOL). Elsewhere raises *PreflightError with the pubkey + actual lamports + remediation hint. - checkRecipientATA: derives the ATA via utils.FindAssociatedTokenAddressWithProgram, calls getAccountInfo, fires surfnet_setTokenAccount on localnet + demo when the account is missing, raises *PreflightError elsewhere. - RPC failures (network unreachable, RPC errors) are logged via slog.Warn and returned as nil so the runtime resurfaces the issue on the first request -- matches the explicit contract in Ruby PR solana-foundation#142. Exposed PreflightRPCInterface + SetPreflightRPCFactoryForTests + RunPreflightForTests + PreflightEnabledForTests so external test packages can drive the autofix branches via a fakeRPC double (mirrors PHP's FakeRpcGateway + Ruby's FakeRpc + Lua's test_helper). paykit/preflight_test.go covers: - localnet+demo auto-funds fee-payer when balance is 0 - localnet+demo auto-provisions ATA when getAccountInfo returns nil - devnet+non-demo signer raises *PreflightError with stage=fee-payer - RPC failures defer to runtime (preflight returns nil) - FeePayer=false skips the SOL balance check - PAY_KIT_DISABLE_PREFLIGHT=1 env kill switch - Preflight=&false config override Caveat solana-foundation#6 -- framework-host quirks. doc.go documents the Go-specific contract: net/http accepts mixed-case header writes (no Rack-3 lowercase enforcement needed), there is no PHP CLI dev server WWW-Authenticate auto-401 quirk, and middleware short-circuits via direct http.ResponseWriter writes (no Sinatra halt analogue). Caveat solana-foundation#7 -- coverage gate. CI go.yml ratchets to 85% combined across all listed packages (legacy mpp + paykit umbrella). Local combined coverage is 87.0%. The preflight live-RPC paths are exercised through the fake; the on-chain settle paths are exercised by the harness step rather than by unit tests. go test ./paykit/... clean. Manual DX still settles: pay --sandbox --x402 curl http://127.0.0.1:4567/paid -> 200 pay --sandbox --mpp curl http://127.0.0.1:4567/paid -> 200
EfeDurmaz16
added a commit
that referenced
this pull request
May 28, 2026
Mega-port of issue solana-foundation#137 — Go SDK design — applying lessons from Ruby PR solana-foundation#142, Lua PR solana-foundation#141, and PHP PR solana-foundation#145. paykit/ types.go Scheme, Stablecoin, Network, Address, Denom, Price, Operator, X402Config, MPPConfig, Config, Payment. Network.DefaultRPCURL/MintsLabel/CAIP2 (caveats #1, #2 from Ruby PR solana-foundation#142). errors.go Sentinel errors (ErrPaymentRequired, ErrInvalidProof, ErrChallengeExpired, ErrMixedDenoms, ErrSchemeIncompatible, ErrDemoSignerOnMainnet, ErrInvalidConfig) + PaymentError + GateError. price.go ParseUSD/EUR/GBP + MustParse* boot-time variants; shopspring/decimal under the hood, never float64. gate.go Gate value + Total/Payout/HasFees/Validate. Validate enforces mixed-denom, sum(FeeWithin)<=Amount, x402+fees-incompatible rules. signer.go Signer interface (Pubkey/Sign/IsDemo/SecretKey). mints.go Cross-language ResolveMint + TokenProgramFor (caveat #1: localnet falls back to mainnet mint row). client.go New() resolves zero-value defaults, wires registered adapter builders, runs preflight. DefaultSigner hook avoids the paykit -> signer -> paykit import cycle. middleware.go Client.Require(Gate) + Client.RequireFunc(GateFunc) return func(http.Handler) http.Handler. Context- attached *Payment via private ctxKey{} per the log/slog convention. PaymentFrom / IsPaid / IsPaidFor accessors. preflight.go MPP HMAC secret auto-resolution (caveat solana-foundation#4): env -> ./.env -> generate + persist mode 0600. paykit/signer/ signer.go Local Ed25519 factories: Demo / Generate / FromBytes / FromJSON / FromHex / FromBase58 / FromFile / FromEnv + MustXxx variants. Demo pubkey matches Ruby/Lua/PHP: ALtYSsZuYyKrNSe6GnVCzxj1T2RPMTPzXMe51xhbmXEq. Registers Demo via paykit.DefaultSigner in init(). paykit/schemes/mpp/ mpp.go Wraps the existing server.Mpp charge handler in the Adapter contract. acceptsEntry advertises methodDetails.network as the short slug so pay --sandbox --mpp curl matches the active wallet (the PHP fix from PR solana-foundation#145). paykit/schemes/x402/ x402.go x402-exact adapter: 402 envelope with recentBlockhash in extra (caveat solana-foundation#5), base64 + Solana versioned-tx decode, partial-sign as facilitator, sendRawTx via gagliardetto/solana-go RpcClient. Replay-store reservation in memory (paykit.Store interface pluggable later). cmd/harness-server/ main.go Cross-language harness adapter binary. Reads X402_INTEROP_* or MPP_INTEROP_* env (or PAY_KIT_INTEROP_PROTOCOL hint), boots paykit.Client, emits the ready JSON, serves /paid. Mirrors harness/ruby-server, lua-server, php-server. examples/paykit-server/ main.go Dual-protocol localnet demo. Boots a paykit.Client with the demo signer, gates /paid behind a /bin/zsh.10 USDC charge. Tooling: - go/Justfile: install / build / test / fmt / lint / audit / test-cover / check / serve-example targets, mirroring Ruby + Lua + PHP. - .github/workflows/go.yml: paykit + paykit/signer added to the package list in the test step; new interop-go-paykit job that builds rust-x402 + the harness binary and runs the dual-protocol matrix against the Go server (typescript client -> go-paykit for MPP charge, rust-x402 client -> go-paykit for x402-exact). - harness/src/implementations.ts: registers the go-paykit server with intents [charge, x402-exact]. Manual DX verified end-to-end against the locally built solana-foundation/pay@feat/internals: pay --sandbox --x402 curl http://127.0.0.1:4567/paid -> 200 signature 3Bzkj2P8si6tsgVpyGpVJGF6BD5WhYS3fewsMQV6B1f7LWJTX1k3oHDmUd5TCYJ6PzAGTZpq9KTN7Lx1S3fhxL3G pay --sandbox --mpp curl http://127.0.0.1:4567/paid -> 200 signature 2ZMspVR99ipbpMUX3CMsmxsPb5hLbHg6h78vYxYDJ9MrfHfGqDihoo1aFkBBTGhfxGJ2Jrq1vjrxagBjBPvM4DJj go test ./paykit/... green. Adapter packages compile clean; further unit coverage + the live surfnet auto-bootstrap (caveat #3) land in follow-up commits in this same branch.
EfeDurmaz16
added a commit
that referenced
this pull request
May 28, 2026
…undation#6 solana-foundation#7 Two fixes after rereading the issue body + Efe's comment carefully: (1) Rename paykit/schemes -> paykit/protocols. Cross-language convention across Ruby (lib/pay_kit/protocols), PHP (src/Protocols), and Lua (pay_kit/protocols) uses 'protocols'; the initial sketch followed the schemes label from Ludo's draft but should track the established naming. - go/paykit/schemes/{mpp,x402} -> go/paykit/protocols/{mpp,x402} - All import sites updated (examples, cmd/harness-server, test fixtures). (2) Apply caveats that were partially or entirely missed: Caveat #3 -- live preflight with Surfnet cheatcode auto-bootstrap. paykit/preflight.go is now a real check, not a stub: - checkFeePayerSOL: getBalance via the resolved RPC, compares against minFeePayerLamports (1_000_000). On localnet + demo signer + balance below the floor, fires surfnet_setAccount with autofundLamports (10 SOL). Elsewhere raises *PreflightError with the pubkey + actual lamports + remediation hint. - checkRecipientATA: derives the ATA via utils.FindAssociatedTokenAddressWithProgram, calls getAccountInfo, fires surfnet_setTokenAccount on localnet + demo when the account is missing, raises *PreflightError elsewhere. - RPC failures (network unreachable, RPC errors) are logged via slog.Warn and returned as nil so the runtime resurfaces the issue on the first request -- matches the explicit contract in Ruby PR solana-foundation#142. Exposed PreflightRPCInterface + SetPreflightRPCFactoryForTests + RunPreflightForTests + PreflightEnabledForTests so external test packages can drive the autofix branches via a fakeRPC double (mirrors PHP's FakeRpcGateway + Ruby's FakeRpc + Lua's test_helper). paykit/preflight_test.go covers: - localnet+demo auto-funds fee-payer when balance is 0 - localnet+demo auto-provisions ATA when getAccountInfo returns nil - devnet+non-demo signer raises *PreflightError with stage=fee-payer - RPC failures defer to runtime (preflight returns nil) - FeePayer=false skips the SOL balance check - PAY_KIT_DISABLE_PREFLIGHT=1 env kill switch - Preflight=&false config override Caveat solana-foundation#6 -- framework-host quirks. doc.go documents the Go-specific contract: net/http accepts mixed-case header writes (no Rack-3 lowercase enforcement needed), there is no PHP CLI dev server WWW-Authenticate auto-401 quirk, and middleware short-circuits via direct http.ResponseWriter writes (no Sinatra halt analogue). Caveat solana-foundation#7 -- coverage gate. CI go.yml ratchets to 85% combined across all listed packages (legacy mpp + paykit umbrella). Local combined coverage is 87.0%. The preflight live-RPC paths are exercised through the fake; the on-chain settle paths are exercised by the harness step rather than by unit tests. go test ./paykit/... clean. Manual DX still settles: pay --sandbox --x402 curl http://127.0.0.1:4567/paid -> 200 pay --sandbox --mpp curl http://127.0.0.1:4567/paid -> 200
The Python interop matrix built the rust adapters with an unscoped `cargo build --bin interop_client --bin interop_server`. Both the solana-mpp and solana-x402 crates expose binaries with those names, so the workspace build hit an output-filename collision at target/debug/interop_client and the x402 binary won the colliding path. The rust client adapter then executed the x402 binary, which aborts with "X402_INTEROP_TARGET_URL is required" instead of reading MPP_INTEROP_TARGET_URL, failing every rust-client-to-python-server charge scenario. Scope the build to -p solana-mpp to match php.yml and ci.yml, which already pin the package.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hardens the x402
exactinterop harness with a three-tier test architecture stacked on solana-foundation#132.Tier 1 — Wire compat (default
pnpm test, no RPC, no cargo)harness/test/x402-exact.compat.test.tsdrives every registered fast in-process x402-exact adapter (gated byCOMPAT_INCLUDE_IDS) against canonical fixtures and an attack suite. Catches wire-format drift before any live RPC matrix runs. 17 tests.Tier 2 — Cross-spine + self-pair matrix (env-gated)
harness/test/x402-exact.e2e.test.ts(expanded from solana-foundation#132) now has an explicit per-language self-pair group so per-language regressions stand out in vitest output. Still gated byX402_INTEROP_MATRIX=1.Tier 3 — Live full matrix (env-gated)
harness/test/x402-exact.live.matrix.test.tsenumerates EVERYallowedPair(client × server) from the shared pair policy inharness/src/x402-pair-policy.ts. Widens automatically as new x402-exact adapters register — no test edit required.Parity-lock fixtures (
harness/fixtures/x402-exact/)canonical-challenge.json— TS-wire 402 envelope every client must parse.canonical-payment-signature.json— TS-wire credential every server must parse.canonical-payment-signature-rust.json— Rust-spinePaymentSignatureEnvelopeshape lock (mirrorsrust/crates/x402/src/protocol/schemes/exact/types.rs::PaymentSignatureEnvelopewithpayload: PaymentProof::Transaction).canonical-reject-tokens.json— taxonomy of every high-level +invalid_exact_svm_payload_*reject token. Strictly equality-checked againstrust/crates/x402/src/protocol/schemes/exact/verify.rs(test fails if the rust spine adds/removes/renames a token).attack-scenarios.json— 9 tampered credential scenarios + replay.Robustness gates
payment_invalidfallback). Wire-only adapters (WIRE_ONLY_ADAPTER_IDS) keep the fallback.X402_COMPAT_STUB_ACCEPT)...._compute_price_instruction_too_highis not greedily credited as..._compute_price_instruction.extractRejectTokensearches every response field for a known taxonomy token (the rust spine wraps verifier errors as{ error: "payment_invalid", message: "<specific-token>: ..." }— the most-specific token is inmessage, noterror).Extending with a new language
{id, intents: ["x402-exact"], enabled}inharness/src/implementations.ts.allowedX402Pairpolicy.COMPAT_INCLUDE_IDSwhen adapter has fast startup and shares the TS-wire credential shape. Otherwise rely on the live matrix.Env-gating contract
X402_INTEROP_MATRIX=1X402_INTEROP_RPC_URL/_MINT/_PAY_TO/_CLIENT_SECRET_KEY/_FACILITATOR_SECRET_KEYX402_COMPAT_STUB_ACCEPT=<id,...>X402_COMPAT_REPLAY_TRUST=<id,...>Missing live-matrix envs while
X402_INTEROP_MATRIX=1is set produces a loudconsole.warnto stderr in addition to the skip (spec'd behavior).Test plan
pnpm installcleanpnpm exec vitest run test/x402-exact.compat.test.ts— 17/17 passpnpm exec vitest run test/x402-exact.e2e.test.ts test/x402-exact.live.matrix.test.ts— 2 skip (env-gated, expected)X402_INTEROP_MATRIX=1 pnpm exec vitest run test/x402-exact.live.matrix.test.ts— enumerates pairs and skips with explicitmissing required env vars: ...reasoncompute-budget-caps.test.tspredates this PR — verified by stashing changes)0 P1; shipStacked on solana-foundation#132. Base =
pr/x402-harness-intent.