Skip to content

test(harness): full x402 exact interop matrix (client/server cross-pairs + parity locks)#3

Open
EfeDurmaz16 wants to merge 10 commits into
pr/x402-harness-intentfrom
pr/x402-harness-matrix
Open

test(harness): full x402 exact interop matrix (client/server cross-pairs + parity locks)#3
EfeDurmaz16 wants to merge 10 commits into
pr/x402-harness-intentfrom
pr/x402-harness-matrix

Conversation

@EfeDurmaz16

Copy link
Copy Markdown
Owner

Summary

Hardens the x402 exact interop 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.ts drives every registered fast in-process x402-exact adapter (gated by COMPAT_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 by X402_INTEROP_MATRIX=1.

Tier 3 — Live full matrix (env-gated)

harness/test/x402-exact.live.matrix.test.ts enumerates EVERY allowedPair (client × server) from the shared pair policy in harness/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-spine PaymentSignatureEnvelope shape lock (mirrors rust/crates/x402/src/protocol/schemes/exact/types.rs::PaymentSignatureEnvelope with payload: PaymentProof::Transaction).
  • canonical-reject-tokens.json — taxonomy of every high-level + invalid_exact_svm_payload_* reject token. Strictly equality-checked against rust/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

  • Full verifiers MUST emit a specific reject token (no payment_invalid fallback). Wire-only adapters (WIRE_ONLY_ADAPTER_IDS) keep the fallback.
  • Full verifiers that accept the TS-wire stub credential are flagged as a verifier bypass (opt out via X402_COMPAT_STUB_ACCEPT).
  • Reject-token taxonomy match is longest-first so ..._compute_price_instruction_too_high is not greedily credited as ..._compute_price_instruction.
  • Replay assertion requires first submission to be accepted; a server that rejects every credential cannot trivially pass.
  • extractRejectToken searches 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 in message, not error).

Extending with a new language

  1. Register {id, intents: ["x402-exact"], enabled} in harness/src/implementations.ts.
  2. Self-pair tests in tier 2 + live matrix in tier 3 pick it up automatically via the shared allowedX402Pair policy.
  3. Compat (tier 1): add to COMPAT_INCLUDE_IDS when adapter has fast startup and shares the TS-wire credential shape. Otherwise rely on the live matrix.

Env-gating contract

Env Purpose Default
X402_INTEROP_MATRIX=1 Enable tiers 2 + 3 (live RPC) off
X402_INTEROP_RPC_URL / _MINT / _PAY_TO / _CLIENT_SECRET_KEY / _FACILITATOR_SECRET_KEY Live matrix env required when matrix=1
X402_COMPAT_STUB_ACCEPT=<id,...> Allow listed full verifiers to accept TS-wire stub empty
X402_COMPAT_REPLAY_TRUST=<id,...> Allow listed full verifiers to run the replay assertion empty (wire-only only)

Missing live-matrix envs while X402_INTEROP_MATRIX=1 is set produces a loud console.warn to stderr in addition to the skip (spec'd behavior).

Test plan

  • pnpm install clean
  • pnpm exec vitest run test/x402-exact.compat.test.ts — 17/17 pass
  • pnpm 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 explicit missing required env vars: ... reason
  • No regression in the rest of the harness suite (one pre-existing failure in compute-budget-caps.test.ts predates this PR — verified by stashing changes)
  • Codex review final pass: 0 P1; ship

Stacked on solana-foundation#132. Base = pr/x402-harness-intent.

…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.
@EfeDurmaz16 EfeDurmaz16 force-pushed the pr/x402-harness-matrix branch from 4f85dee to c25cf77 Compare May 26, 2026 09:39
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant