feat(kotlin): add x402 exact client#126
Closed
EfeDurmaz16 wants to merge 12 commits into
Closed
Conversation
Adds the canonical x402 `exact` intent to the cross-language interop harness, plus TypeScript reference client and server fixtures and matrix wiring that registers the Rust spine adapters already shipped under `rust/crates/x402/src/bin/`. Language adapters can now target the harness contract (X402_INTEROP_* env vars, ready/result JSON shapes) to validate against the Rust spine cell. The TS reference fixture carries a stub credential payload (challenge id + resource) so the harness wiring, negative-code classification, cross-server portability, and idempotent-resubmit flows can run without a full Solana signer. Pair restriction in the matrix gates TS↔TS and Rust↔Rust by default; full TS↔Rust on-chain settlement parity lands with a follow-up SDK port. The legacy MPP charge runner hard-skips the new intent so default `pnpm test` behaviour is unchanged.
…n#27 Ports the Kotlin x402 exact client adapter from solana-foundation/x402-sdk PR solana-foundation#27 (tip cab2f21) into a new top-level `kotlin/` module under mpp-sdk, following Ludo's `rust/crates/x402/` pattern. Client-only — no Kotlin server runtime in this milestone. Layout: `kotlin/{build.gradle.kts,settings.gradle.kts,src/main,src/test}` with package namespace rewritten from `org.x402.sdk.interop` to `org.solana.x402.exact`. Wires `tests/interop/src/implementations.ts` with the `kotlin-x402-client` entry (gated, opt-in via MPP_INTEROP_CLIENTS). Codex Round 4 evidence (notes/codex-review-kotlin-x402-r4.md): - 0 real P1, Confidence 4/5 - 27 JUnit tests pass (`gradle --project-dir kotlin test`) - carries payTo!=payer self-transfer guard, currencyMatches runCatching wrap, stablecoin mainnet-leak fix, cross-set account-key dedup, real Long.MAX_VALUE guard, ALLOWED_TOKEN_PROGRAMS triple validation, RFC 8032 §7.1 TEST 1 regression
235507f to
343419a
Compare
EfeDurmaz16
added a commit
to EfeDurmaz16/mpp-sdk
that referenced
this pull request
May 25, 2026
Three fixes so the x402-exact matrix actually executes once the language adapter PRs (solana-foundation#124, solana-foundation#126, solana-foundation#127, solana-foundation#128, solana-foundation#129, solana-foundation#130) rebase on top of this branch: 1. Pair filter is data-driven. Previously only ts-x402 self-pair and rust-x402 self-pair were accepted. Now the filter walks the registered adapters and accepts any pair where: both sides are the TS reference (stub-payload), both sides are the Rust spine, the two sides share a base language id (same-language self-pair, e.g. go-x402-client <-> go-x402-server), or one side is the Rust spine (cross-spine pair in either direction). TS reference is locked to its self-pair only because its stub payload would fail real signature verification on any other server. 2. rust-x402 cargo --manifest-path corrected from ../../rust/Cargo.toml to ../rust/Cargo.toml. The path was stale after the tests/interop -> harness rename; the existing rust (charge) entries already used the correct relative path, the x402 entries did not. 3. Pair selector docstring rewritten to spell out the data-driven matrix policy so future language ports don't need to touch the test.
The Mainnet network identifier used the full 44-char base58 genesis hash which broke interop with every spine-compliant mainnet challenge. The Rust spine constant SOLANA_MAINNET (rust/crates/x402/src/protocol/schemes/exact/types.rs) uses the canonical 32-char truncated prefix; all other ports already match. Updates the affected unit test (it previously relied on the 44-char form to assert network-mismatch rejection) and adds a regression test pinning SolanaNetwork.Mainnet.caip2 to the spine constant.
ExactChallenge.accepts() previously read only `amount`, silently dropping every spine-shaped challenge that uses the canonical `maxAmountRequired` wire field (TS fixture, Rust spine output, Go/Python/PHP ports). Mirror the Rust spine fallback in rust/crates/x402/src/protocol/schemes/exact/types.rs by accepting either field, preferring `amount` when both are present for back-compat. Adds two regression tests covering the new wire field and the both-present precedence.
Codex r8 P2 finding. The Rust spine parses x402 exact amounts as u64 (rust/crates/x402/src/protocol/schemes/exact/verify.rs), so the full unsigned 64-bit range is valid on the wire. The Kotlin builder previously narrowed the amount to a signed Long and rejected anything above Long.MAX_VALUE, breaking spine parity for valid challenges that exercise the upper half of u64. Pass the parsed ULong directly into the instruction encoder, which already serialises the value as 8 little-endian bytes — no signed narrowing happens anywhere now. Replace the dead Long.MAX_VALUE rejection regression with a positive test that builds a transaction at u64::MAX and asserts the encoded transferChecked instruction contains 0xFF * 8 + decimals.
Codex r9 P1 finding. The Rust spine's `find_matching_requirement` (rust/crates/x402/src/server/exact.rs) round-trips the credential's `accepted` through the typed `PaymentRequirements` serialiser and structurally compares the result against the route's offered requirement. Echoing the raw offered object verbatim leaks deprecated wire aliases (`maxAmountRequired`, `currency`, `recipient`) into the credential and the structural equality match fails even when the underlying values agree. Mirror `PaymentRequirements::to_accepted_value` in rust/crates/x402/src/protocol/schemes/exact/types.rs by stripping the deprecated aliases before re-asserting canonical `scheme`, `network`, `asset`, `amount`, `payTo`, `maxTimeoutSeconds` and `extra`. Adds an `ExactPaymentClientTest` regression covering a requirement whose raw envelope carries both alias forms and confirming the emitted `accepted` only contains the canonical fields.
Codex r9 P1 finding. The Rust spine
(rust/crates/x402/src/client/exact/payment.rs) supports native SOL
challenges by emitting a System Program transfer for `asset: "SOL"`.
This Kotlin client is SPL-only — the builder decodes `asset` as a
base58 mint and emits transferChecked. A Rust-spine SOL offer would
crash deep inside Base58.decode("SOL") with an opaque error.
Filter native SOL offers out at selection time so the resolver either
picks a supported SPL candidate or returns null. The kotlin client
falls through cleanly when no SPL offer is available rather than
constructing an invalid transferChecked.
Adds two ExactChallengeTest regressions covering (a) SOL skipped in
favour of an SPL alternative and (b) selection returning null when
only a SOL offer is on the wire.
Codex r10 P1 finding. The Rust spine client at
rust/crates/x402/src/client/exact/payment.rs treats
`requirements.fee_payer_key` as optional and falls back to the signer
as the actual fee payer:
let actual_fee_payer = fee_payer_pubkey.unwrap_or(signer_pubkey);
The Kotlin client previously required `extra.feePayer` and rejected
every spine-compliant challenge that omitted it. Mirror the Rust
fallback: when no managed fee payer is supplied, the transfer
authority (payer) pays its own network fees. The drain-attack and
self-pay-loop guards still fire when a feePayer *is* present.
`SolanaExactPaymentRequest.feePayer` is now nullable.
`DefaultSolanaExactTransactionBuilder` substitutes the payer when the
field is absent, producing a v0 message that requires a single
signature (signer == feePayer).
Two regressions:
- `falls back to payer as fee payer when feePayer is absent` (client)
confirms the request flows through with feePayer = null.
- `builder uses signer as fee payer when challenge omits feePayer`
(builder) asserts num_required_signatures == 1 and signerIndex == 0.
EfeDurmaz16
added a commit
to EfeDurmaz16/mpp-sdk
that referenced
this pull request
May 26, 2026
Three fixes so the x402-exact matrix actually executes once the language adapter PRs (solana-foundation#124, solana-foundation#126, solana-foundation#127, solana-foundation#128, solana-foundation#129, solana-foundation#130) rebase on top of this branch: 1. Pair filter is data-driven. Previously only ts-x402 self-pair and rust-x402 self-pair were accepted. Now the filter walks the registered adapters and accepts any pair where: both sides are the TS reference (stub-payload), both sides are the Rust spine, the two sides share a base language id (same-language self-pair, e.g. go-x402-client <-> go-x402-server), or one side is the Rust spine (cross-spine pair in either direction). TS reference is locked to its self-pair only because its stub payload would fail real signature verification on any other server. 2. rust-x402 cargo --manifest-path corrected from ../../rust/Cargo.toml to ../rust/Cargo.toml. The path was stale after the tests/interop -> harness rename; the existing rust (charge) entries already used the correct relative path, the x402 entries did not. 3. Pair selector docstring rewritten to spell out the data-driven matrix policy so future language ports don't need to touch the test.
EfeDurmaz16
added a commit
to EfeDurmaz16/mpp-sdk
that referenced
this pull request
May 26, 2026
Three fixes so the x402-exact matrix actually executes once the language adapter PRs (solana-foundation#124, solana-foundation#126, solana-foundation#127, solana-foundation#128, solana-foundation#129, solana-foundation#130) rebase on top of this branch: 1. Pair filter is data-driven. Previously only ts-x402 self-pair and rust-x402 self-pair were accepted. Now the filter walks the registered adapters and accepts any pair where: both sides are the TS reference (stub-payload), both sides are the Rust spine, the two sides share a base language id (same-language self-pair, e.g. go-x402-client <-> go-x402-server), or one side is the Rust spine (cross-spine pair in either direction). TS reference is locked to its self-pair only because its stub payload would fail real signature verification on any other server. 2. rust-x402 cargo --manifest-path corrected from ../../rust/Cargo.toml to ../rust/Cargo.toml. The path was stale after the tests/interop -> harness rename; the existing rust (charge) entries already used the correct relative path, the x402 entries did not. 3. Pair selector docstring rewritten to spell out the data-driven matrix policy so future language ports don't need to touch the test.
EfeDurmaz16
added a commit
to EfeDurmaz16/mpp-sdk
that referenced
this pull request
May 26, 2026
Three fixes so the x402-exact matrix actually executes once the language adapter PRs (solana-foundation#124, solana-foundation#126, solana-foundation#127, solana-foundation#128, solana-foundation#129, solana-foundation#130) rebase on top of this branch: 1. Pair filter is data-driven. Previously only ts-x402 self-pair and rust-x402 self-pair were accepted. Now the filter walks the registered adapters and accepts any pair where: both sides are the TS reference (stub-payload), both sides are the Rust spine, the two sides share a base language id (same-language self-pair, e.g. go-x402-client <-> go-x402-server), or one side is the Rust spine (cross-spine pair in either direction). TS reference is locked to its self-pair only because its stub payload would fail real signature verification on any other server. 2. rust-x402 cargo --manifest-path corrected from ../../rust/Cargo.toml to ../rust/Cargo.toml. The path was stale after the tests/interop -> harness rename; the existing rust (charge) entries already used the correct relative path, the x402 entries did not. 3. Pair selector docstring rewritten to spell out the data-driven matrix policy so future language ports don't need to touch the test.
EfeDurmaz16
added a commit
to EfeDurmaz16/mpp-sdk
that referenced
this pull request
May 26, 2026
Three fixes so the x402-exact matrix actually executes once the language adapter PRs (solana-foundation#124, solana-foundation#126, solana-foundation#127, solana-foundation#128, solana-foundation#129, solana-foundation#130) rebase on top of this branch: 1. Pair filter is data-driven. Previously only ts-x402 self-pair and rust-x402 self-pair were accepted. Now the filter walks the registered adapters and accepts any pair where: both sides are the TS reference (stub-payload), both sides are the Rust spine, the two sides share a base language id (same-language self-pair, e.g. go-x402-client <-> go-x402-server), or one side is the Rust spine (cross-spine pair in either direction). TS reference is locked to its self-pair only because its stub payload would fail real signature verification on any other server. 2. rust-x402 cargo --manifest-path corrected from ../../rust/Cargo.toml to ../rust/Cargo.toml. The path was stale after the tests/interop -> harness rename; the existing rust (charge) entries already used the correct relative path, the x402 entries did not. 3. Pair selector docstring rewritten to spell out the data-driven matrix policy so future language ports don't need to touch the test.
Collaborator
Author
|
Closing per maintainer's focus-on-Ruby guidance. The branch stays available on the fork so the work can be cherry-picked once the Ruby pattern is set and we replay the same shape across languages. |
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
Ports the x402
exactclient to Kotlin as a new top-levelkotlin/module, following therust/crates/x402/canonical spine. Client-only by design (no Kotlin server runtime, no MWA dependency).Scope
kotlin/withbuild.gradle.ktsandsettings.gradle.ktskotlin/src/main/kotlin/org/solana/x402/exact/:ExactChallenge.kt,ExactPaymentClient.kt,InteropClient.kt,SolanaTransaction.ktSOLANA_MAINNET32-char canonical prefix (339c647)maxAmountRequiredfallback for the TS reference fixture (8fd19f0) so cross-fixture pairing is robust to the stub envelopeALLOWED_TOKEN_PROGRAMStriple-validation at the challenge parser, transaction builder, and RPC mint-owner check (same three layers asclient/exact/payment.rs)Files changed
kotlin/src/main/kotlin/org/solana/x402/exact/**kotlin/src/test/kotlin/org/solana/x402/exact/**(27 JUnit cases)harness/src/implementations.tsregisterskotlin-x402-clientwithintents: ["x402-exact"]Security highlights
payTo != payerself-transfer guard fails fast before any RPC or Base58 workcurrencyMatcheswrapped inrunCatchingfor safe parse-failure handlingcompileV0Messagecross-set account-key dedup with role promotionTest evidence
gradle --project-dir kotlin test --rerun-tasks: 27/27 passpnpm typecheck(harness/): clean for changed filesnotes/codex-review/pr-126-r8.md)kotlin-x402-client ↔ rust-x402-serverregisteredCloses / supersedes
None.
Reviewer notes