Skip to content

feat(swift): add x402 exact client#129

Closed
EfeDurmaz16 wants to merge 12 commits into
solana-foundation:mainfrom
EfeDurmaz16:pr/swift-x402-port
Closed

feat(swift): add x402 exact client#129
EfeDurmaz16 wants to merge 12 commits into
solana-foundation:mainfrom
EfeDurmaz16:pr/swift-x402-port

Conversation

@EfeDurmaz16

@EfeDurmaz16 EfeDurmaz16 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Ports the x402 exact client to Swift by adding an X402 library and x402-interop-client executable as sibling targets in the existing swift/Package.swift (alongside SolanaMpp from #104). Client-only, darwin-only.

Scope

  • Implementation at swift/Sources/X402/: Base58.swift, Challenge.swift, Constants.swift, Ed25519CompressedPoint.swift, JSONValue.swift, Rpc.swift, Signer.swift, SolanaTransaction.swift
  • Harness adapter swift/Sources/X402InteropClient/main.swift
  • extra.tokenProgram SPL allowlist via validatedTokenProgram() rejecting any program outside {SPL Token, Token-2022}
  • extra.decimals validated as 0..255 integer (rejects NaN/Inf/negative/fractional)
  • Spine-pinned compute-unit policy: client never injects a SetComputeUnitLimit instruction; price-only cap respected
  • Optional NSNull handling for the absent fixture settlement header

Files changed

  • SDK: swift/Package.swift, swift/Sources/X402/**, swift/Sources/X402InteropClient/main.swift
  • Tests: swift/Tests/X402Tests/** (20 XCTest cases)
  • Harness: harness/src/implementations.ts registers swift-x402-client darwin-gated (process.platform === "darwin") with intents: ["x402-exact"]

Security highlights

  • MemorySolanaSigner pubkey verify uses CryptoKit derivation and throws on mismatch
  • Ed25519 non-canonical fixture (32-byte y = p, y = p + 1 plus count == 32 assert) prevents accepting malformed compressed points
  • x402Version validation rejects unknown protocol revisions
  • Per-network stablecoin symbol-to-mint resolution with cross-network fallback but no silent mainnet widening

Test evidence

  • swift test --package-path swift on darwin (Swift 6.2.3): 76/76 pass (56 SolanaMpp + 20 X402)
  • pnpm typecheck (harness/): clean for changed files
  • Codex r8: 0 P1, confidence 4/5 (notes/codex-review/pr-129-r8.md)
  • ubuntu-latest interop jobs that list swift-x402-client no-op cleanly via the darwin gate

Closes / supersedes

None.

Reviewer notes

  • Client-only; no Swift server runtime in this PR
  • SYSTEM_PROGRAM and LIGHTHOUSE_PROGRAM constants intentionally omitted (server-only)
  • resource url|uri permissive deser not applicable: Swift PaymentRequirement does not decode resource; routing happens via headers and CAIP-2 network

@EfeDurmaz16 EfeDurmaz16 changed the title feat(swift): port x402 exact client — from x402-sdk #26 feat(swift): add x402 exact client May 25, 2026
@EfeDurmaz16 EfeDurmaz16 force-pushed the pr/swift-x402-port branch from 7eb8c02 to 3219a6f Compare May 25, 2026 21:14
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.
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 EfeDurmaz16 force-pushed the pr/swift-x402-port branch from b29f8f8 to 8293b0e Compare May 26, 2026 13:04
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.
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.
Adds X402 library + x402-interop-client executable as sibling targets to
the existing SolanaMpp library in swift/Package.swift. Client-only,
darwin-only (CryptoKit + Apple Swift toolchain required). Source:
solana-foundation/x402-sdk PR solana-foundation#26 (tip 36c5e9c).

- swift/Package.swift — add X402 library, X402InteropClient executable,
  and X402Tests test target alongside SolanaMpp
- swift/Sources/X402/ — implementation (renamed X402SwiftExact -> X402)
- swift/Sources/X402InteropClient/ — interop harness adapter
- swift/Tests/X402Tests/ — 20 XCTest cases (all passing on darwin)
- notes/codex-review-swift-x402-r4.md — Codex r4 evidence carry-over
- tests/interop/src/implementations.ts — swift-x402-client entry, gated
  on process.platform === "darwin"

Behavioral parity verified against rust/crates/x402/ reference spine:
CAIP-2 network identifiers, SPL/Token-2022 program IDs, stablecoin mint
addresses, EXACT_SCHEME, MAX_MEMO_BYTES, per-network stablecoin
resolution with cross-network fallback, fail-closed network selection,
extra.tokenProgram SPL allowlist enforcement, decimals validation
(0..255 integer), x402Version validation, and the canonical
X402_INTEROP_* env-var contract including X402_INTEROP_PREFER_CURRENCIES.
PaymentRequirement Codable previously read only `amount`, silently
dropping every spine-shaped challenge that uses the canonical
`maxAmountRequired` wire field (TS fixture, Rust spine output,
Go/Kotlin/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.
Compute budget values (limit=20_000, price=1 microLamport) already
match the Rust spine canonical at
rust/crates/x402/src/client/exact/payment.rs:55-57. Document the
cross-reference at the call site and pin the serialized
SetComputeUnitLimit and SetComputeUnitPrice byte sequences in a
regression test so any future drift surfaces in CI rather than at
the facilitator.
The TypeScript adapter at `typescript/` is a fixture stub that only
exercises challenge framing; it does not settle on-chain. The Rust
spine at `rust/crates/x402/` is the canonical reference. Update the
Swift X402 in-source docs that previously labelled the TS adapter as
"reference" so future contributors and review tooling cannot mistake
the fixture stub for the authoritative implementation.
Replace CryptoKit with the swift-crypto `Crypto` module so the X402
client builds and runs on Linux as well as Apple platforms. The
Curve25519.Signing and SHA256 APIs are API-compatible across CryptoKit
and SwiftCrypto, so no logic changes are required.

Drop the Darwin-only runtime gate from the `swift-x402-client` interop
adapter registration so the same harness entry works on ubuntu CI
runners that have a Swift toolchain installed. CI runners without
Swift must still leave the adapter out of MPP_INTEROP_CLIENTS.
Replace the per-line TS constants.ts:NN cross-references with the
canonical rust/crates/x402/src/protocol/schemes/exact/types.rs line
ranges so the source of truth is unambiguous. TS is a fixture stub
and is non-authoritative; the mint table values themselves are
unchanged.

Also refresh notes/codex-review-swift-x402-r4.md to reflect the
SwiftCrypto migration (no longer Darwin-only/CryptoKit/XCTest).
@EfeDurmaz16 EfeDurmaz16 force-pushed the pr/swift-x402-port branch from 0a962ce to 1970c81 Compare May 26, 2026 15:52
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

Copy link
Copy Markdown
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.

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