Skip to content

feat(ruby): add x402 exact (client+server)#127

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

feat(ruby): add x402 exact (client+server)#127
EfeDurmaz16 wants to merge 27 commits into
solana-foundation:mainfrom
EfeDurmaz16:pr/ruby-x402-port

Conversation

@EfeDurmaz16

@EfeDurmaz16 EfeDurmaz16 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Ports the x402 exact scheme (client + server) to Ruby under ruby/lib/x402/, following the rust/crates/x402/ canonical spine and mirroring the Go layout from #124.

Scope

  • Client and server modules at ruby/lib/x402/{client,exact,server}.rb
  • Harness binaries at ruby/bin/x402-interop-{client,server}
  • L8 settlement: broadcast then getSignatureStatuses confirm then put_if_absent("x402-svm-exact:consumed:<base58_signature>"), no claim-first, no release path; mirrors MPP server/charge.rs:535-556
  • Fee-payer-in-instruction-accounts sweep with ATA-create funding-payer carve-out (account-position 0 on AssociatedTokenAccount::Create / CreateIdempotent only)
  • PAYMENT-RESPONSE canonical shape on successful settlement (497d8f4)
  • Sign-then-verify ordering: client signature validated before facilitator co-sign

Files changed

  • SDK: ruby/lib/x402/{client,exact,server}.rb, ruby/bin/x402-interop-{client,server}
  • Tests: ruby/test/x402_interop_{client,server}_test.rb
  • Harness: harness/src/implementations.ts registers ruby-x402-client / ruby-x402-server with intents: ["x402-exact"]

Security highlights

  • Fee-payer ATA-drain attack suite (4 shapes plus positive control) in ruby/test/x402_interop_server_test.rb:278-343, all asserting the canonical reject token invalid_exact_svm_payload_transaction_fee_payer_in_instruction_accounts
  • Lighthouse passthrough is program-ID only, no invented discriminator allowlist (spine parity)
  • Destination ATA independently re-derived from (payTo, mint, tokenProgram)
  • short_vec UTF-8 encoding bug fix: [byte].pack("C") on ASCII-8BIT buffer
  • Non-ASCII memo byte-comparison (not UTF-8 normalized), 256-byte cap

Test evidence

  • bundle exec rake test: 211 runs, 725 assertions, 0 failures, 0 errors, 0 skips (+3 new L8 regressions)
  • bundle exec standardrb: clean
  • ruby -c on all lib, bin, test files: clean
  • pnpm typecheck (harness/): clean for changed files
  • Codex r8: 0 P1, confidence 4/5 (notes/codex-review/pr-127-r8.md); earlier rounds documented at notes/codex-review/pr-127-r{5,5-fix,7}.md
  • Interop matrix: 9-pair enumeration (3 client × 3 server) including rust-x402 ↔ ruby-x402-server

Closes / supersedes

None.

Reviewer notes

  • ATA-create allowlist and fee-payer carve-out documented as INTENTIONAL_DIVERGENCE matching the Go and Lua port pattern (see notes/lighthouse-allowlist-tracking.md)
  • Post-rename Cargo manifest path fix for the rust-x402 adapter (one level shallower under harness/) is included

@EfeDurmaz16 EfeDurmaz16 changed the title feat(ruby): port x402 exact (client+server) — from x402-sdk #20 feat(ruby): add x402 exact (client+server) May 25, 2026
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
Aligns the Ruby x402 client+server adapter env vars with the rest of the
x402 family (ts-x402, rust-x402) so that all x402-exact adapters opt in
via X402_INTEROP_CLIENTS / X402_INTEROP_SERVERS. Resolves PR solana-foundation#127 r8 P3
finding.

No CI workflow currently opts the ruby-x402-* adapters in via the old
MPP_INTEROP_* namespace, so this is a no-op for green CI and only
affects local runs that explicitly request the adapters.
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request May 26, 2026
PR solana-foundation#127 r8 P2 flagged that the cross-server-portability scenario was
wired as `ts-x402 -> rust-x402` while the TS reference client emits a
stub payload (`{ challengeId, resource }`) that does NOT deserialize
into the Rust spine's typed `PaymentProof::{transaction|signature}`
enum. Replaying that header to the Rust spine therefore produces
`payment_invalid` (parse error) rather than the canonical
`challenge_verification_failed` the scenario asserts.

Narrow the pair list to `[ts-x402, ts-x402]` so the assertion exercises
the full classifier path end-to-end, and document that the rust spine's
own portability semantics are covered by the rust/crates/x402 integration
tests. A follow-up can re-enable the cross-spine pair once the TS fixture
emits a typed PaymentProof payload.
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request May 26, 2026
…tring resource)

PR solana-foundation#127 r9 P1: the Ruby x402 client was rejecting offers emitted by the
TS reference fixture because

1. The fixture serialises offers with `maxAmountRequired` rather than
   the canonical Rust-spine `amount` field. Rust accepts either via
   string_field fallback at
   rust/crates/x402/src/protocol/schemes/exact/types.rs:337-339; Ruby
   was only checking `requirement["amount"]`.
2. The fixture's `PAYMENT-REQUIRED` envelope carries a bare `resource`
   URL string while Rust models the same field as a typed ResourceInfo
   object. Ruby's resource_from_envelope only kept Hash forms and
   silently dropped the string form, so the client lost the route
   context needed for downstream binding.

Update `selected_requirement?` to accept either amount field, and
normalise the resource field so consumers always see a
`{ "url" => <string> }` hash regardless of which spine issued the
challenge. Add two interop regression tests pinning both behaviours
against the TS fixture's wire shape.
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
Aligns the Ruby x402 client+server adapter env vars with the rest of the
x402 family (ts-x402, rust-x402) so that all x402-exact adapters opt in
via X402_INTEROP_CLIENTS / X402_INTEROP_SERVERS. Resolves PR solana-foundation#127 r8 P3
finding.

No CI workflow currently opts the ruby-x402-* adapters in via the old
MPP_INTEROP_* namespace, so this is a no-op for green CI and only
affects local runs that explicitly request the adapters.
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request May 26, 2026
PR solana-foundation#127 r8 P2 flagged that the cross-server-portability scenario was
wired as `ts-x402 -> rust-x402` while the TS reference client emits a
stub payload (`{ challengeId, resource }`) that does NOT deserialize
into the Rust spine's typed `PaymentProof::{transaction|signature}`
enum. Replaying that header to the Rust spine therefore produces
`payment_invalid` (parse error) rather than the canonical
`challenge_verification_failed` the scenario asserts.

Narrow the pair list to `[ts-x402, ts-x402]` so the assertion exercises
the full classifier path end-to-end, and document that the rust spine's
own portability semantics are covered by the rust/crates/x402 integration
tests. A follow-up can re-enable the cross-spine pair once the TS fixture
emits a typed PaymentProof payload.
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request May 26, 2026
…tring resource)

PR solana-foundation#127 r9 P1: the Ruby x402 client was rejecting offers emitted by the
TS reference fixture because

1. The fixture serialises offers with `maxAmountRequired` rather than
   the canonical Rust-spine `amount` field. Rust accepts either via
   string_field fallback at
   rust/crates/x402/src/protocol/schemes/exact/types.rs:337-339; Ruby
   was only checking `requirement["amount"]`.
2. The fixture's `PAYMENT-REQUIRED` envelope carries a bare `resource`
   URL string while Rust models the same field as a typed ResourceInfo
   object. Ruby's resource_from_envelope only kept Hash forms and
   silently dropped the string form, so the client lost the route
   context needed for downstream binding.

Update `selected_requirement?` to accept either amount field, and
normalise the resource field so consumers always see a
`{ "url" => <string> }` hash regardless of which spine issued the
challenge. Add two interop regression tests pinning both behaviours
against the TS fixture's wire shape.
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.
Comment thread ruby/lib/x402/exact.rb Outdated
Comment on lines +26 to +32
ED25519_P = (2**255) - 19
ED25519_D = (-121_665 * 121_666.pow(ED25519_P - 2, ED25519_P)) % ED25519_P
ED25519_I = 2.pow((ED25519_P - 1) / 4, ED25519_P)
ED25519_L = (2**252) + 277_423_177_773_723_535_358_519_377_908_836_484_93
ED25519_BASE_X = 151_122_213_495_354_007_725_011_514_095_885_315_114_540_126_930_418_572_060_461_132_839_498_477_622_02
ED25519_BASE_Y = 463_168_356_949_264_781_694_283_940_034_751_631_413_079_938_662_562_256_157_830_336_031_652_518_559_60
PROGRAM_DERIVED_ADDRESS_MARKER = "ProgramDerivedAddress"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this? 🤔

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed alongside the broader x402 cleanup. The Exact module is now scoped to interop only (file moved to ruby/lib/x402/interop/exact.rb in 431d55e) and the Ruby x402 client surface that originally referenced this code is gone. The remaining helpers in Exact are server-side verification primitives plus one test fixture (build_exact_payment_signature) used by x402_interop_server_test.rb to construct fake client payloads for server-verification coverage. Unused client-side helpers (build_exact_payment_signature_from_rpc, public_key_base58, latest_blockhash) were dropped in 9ed6a85. Tip: 9ed6a85.

EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request May 26, 2026
Addresses maintainer feedback on PR solana-foundation#127: stop reimplementing primitives
that already live in the Ruby gem and stop reinventing Ed25519 in pure
Ruby. Both points were called out by lgalabru in the inline review on
ruby/lib/x402/exact.rb constants + Ed25519 block.

What moved to the shared core (Mpp::Methods::Solana::*):

- Base58 alphabet, encode, decode: removed from x402, delegated to
  Mpp::Methods::Solana::Base58.
- Program IDs (TOKEN, TOKEN_2022, SYSTEM, ATA, MEMO, COMPUTE_BUDGET) and
  the devnet USDC + PYUSD mint addresses: sourced from
  Mpp::Methods::Solana::Mints (single canonical table).
- ATA derivation + PDA find_program_address + on-curve check: delegated
  to Mpp::Methods::Solana::AssociatedToken + PublicKey.
- getLatestBlockhash RPC call: delegated to Mpp::Methods::Solana::Rpc
  (also gains an HTTPSuccess guard so non-2xx responses raise the
  canonical Mpp::Error with `getLatestBlockhash HTTP <code>`).
- Solana short_vec / compact-u16 helpers: lifted to
  Mpp::Methods::Solana::Transaction as module functions, mirrors Rust
  spine rust/crates/x402/src/protocol/schemes/exact/types.rs.

What got deleted outright:

- ~170 lines of pure-Ruby Ed25519 curve math in x402/exact.rb
  (ED25519_P / _D / _I / _L / _BASE_X / _BASE_Y, scalar_mult, point_add,
  encode_point, decode_point, mod_sqrt, prune_scalar, public_key_from_seed,
  sign_ed25519, the pure-Ruby verify_ed25519). Replaced with calls into
  the `ed25519` runtime gem already pinned in solana-pay-kit.gemspec.
  Ed25519PrivateKey is now a 6-line adapter wrapping Ed25519::SigningKey.
- The duplicate BASE58_ALPHABET, COMPUTE_BUDGET_PROGRAM, MEMO_PROGRAM,
  ASSOCIATED_TOKEN_PROGRAM, SYSTEM_PROGRAM, TOKEN_2022_PROGRAM constants
  that were redeclared in x402/exact.rb and x402/server.rb.

What stays x402-local (justified):

- LIGHTHOUSE_PROGRAM (x402-protocol-specific, not in MPP).
- DEFAULT_COMPUTE_UNIT_LIMIT and price caps (x402 transaction shape).
- verify_exact_instructions! and the structural x402 validators.
- STABLECOIN_MINTS CAIP-2 view in x402/client.rb now projects from the
  shared Mints::MINTS table instead of redeclaring addresses.

Net effect: ruby/lib/x402/exact.rb drops 776 -> 605 lines and zero
constants or crypto math are duplicated between mpp and x402.

Behavior + tests:

- bundle exec rake test: 214 runs, 737 assertions, 0 failures, 0 errors.
- bundle exec standardrb: clean.
- One x402 client test (`test_latest_blockhash_rejects_http_failure`)
  now asserts the canonical Mpp::Error shape instead of the legacy
  RuntimeError; the net-http stub helper was widened to intercept the
  instance-level Net::HTTP#start path used by Mpp::Methods::Solana::Rpc.
- The pre-existing with_rpc_http helper in support_test wraps canned
  Struct responses with code "200" + is_a?(Net::HTTPSuccess) so the new
  HTTP-status guard in Mpp::Methods::Solana::Rpc#call treats them as 2xx.
@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

@lgalabru refactored to mirror your solana-pay-core / mpp / x402 / pay-kit layout, x402 now consumes PayCore directly and the 214 existing MPP tests still pass through backward-compat aliases, ready for another look when you have a sec.

Comment thread ruby/lib/mpp/methods/solana/mints.rb Outdated
end
end
# Backward-compat alias. Canonical home: `PayCore::Solana::Mints`.
Mints = ::PayCore::Solana::Mints

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to bother about backward compatibility and maintain this modules.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped the alias shims and rewrote callers to use PayCore::Solana::* and PayCore::* directly in 562a492. The Mpp:: namespace now only holds MPP-specific types (Challenge, Credential, Receipt, Verifier, VerificationResult, ChargeMethod, Headers MPP-flavoured wrapper). Followed up in 6b3c6f6 (harness + lua doc) and 9ed6a85 (trim now-unused x402 client-only exact helpers). Tip: 9ed6a85.

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 and others added 11 commits May 26, 2026 18:52
…eferences (solana-foundation#122)

Per maintainer guidance in solana-foundation#122, this is a transversal cleanup PR:

Part A — remove internal kitchen references
- Drop M1/M2/M3 milestone framing from swift/README.md, swift/Examples/README.md
- Reword 'M1 baseline / M2-followup' coverage gate comments in python/pyproject.toml
  and .github/workflows/python.yml as plain coverage gate descriptions
- Remove 'M1 closure / L6 audit row' tag from lua/mpp/protocol/core/error_codes.lua

Part B — rename tests/interop to harness
- git mv tests/interop harness
- Update all path references repo-wide (.github/workflows/*, READMEs,
  .gitignore, docs, composer.json, .php-cs-fixer.dist.php, skill files)
- Fix relative paths inside the harness now that depth dropped by one
  (rust-client/Cargo.toml, php-server, ruby-server, go.mod replace lines,
  src/implementations.ts, test/compute-budget-caps.test.ts REPO_ROOT)
- Update Go module identifiers harness/{go-client,go-server} to match path
- Refresh internal comments/docs that still mentioned tests/interop

Part C — skill / README polish
- Skill references and intent docs now point at harness/* paths

Closes solana-foundation#122.
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.
…dation#20

Port the Ruby x402 exact adapter from solana-foundation/x402-sdk PR solana-foundation#20
(tip 45e618f, Codex Round 4, 0 real P1, Confidence 4/5) into
ruby/lib/x402/, following Ludo's rust/crates/x402/ pattern.

Behavioral parity with rust/crates/x402 spine:
- Program IDs, mints, CAIP-2 network IDs match types.rs verbatim
- MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 5_000_000, MAX_MEMO_BYTES = 256
- Lighthouse passthrough by program-ID only (no discriminator allowlist)
- Compute-unit limit unbounded (parity-tracked)
- Sign-then-verify ordering, resource binding, fee-payer attack guard
- strict_decode64 for headers, short_vec UTF-8 fix, memo byte comparison
- Namespace: X402SDK::Interop -> X402::Interop (top-level rename only)

Tests: 42 server runs / 135 assertions; 26 client runs / 54 assertions;
0 failures across both suites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cross-spine wiring on top of pr/transversal-cleanup (solana-foundation#131) and
pr/x402-harness-intent (solana-foundation#132). 0 new P1; ruby-x402-client and
ruby-x402-server adapters register cleanly with intents:
[x402-exact]. Matrix enumerates 9 pairs; allowedPair gate still
blocks ruby pairs from running, tracked as P2 follow-up.
Rename `verify_fee_payer_not_in_instruction_accounts!` to
`reject_fee_payer_in_instruction_accounts!` and add an explicit carve-out
for the `AssociatedTokenAccount::Create` / `CreateIdempotent` funding
payer slot (account index 0). Every other position in every other
instruction now rejects when the fee payer appears in the accounts list,
closing the inherited drain vector where a malicious client could attach
an extra SPL TransferChecked or SystemProgram::Transfer that names the
managed signer before the facilitator co-signs.

Attack regression coverage in `ruby/test/x402_interop_server_test.rb`:

- DRAIN (SPL): extra TransferChecked names fee payer → reject
- DRAIN (SOL): SystemProgram::Transfer from fee payer → reject
- SLOT: ATA-create with fee payer at the wallet slot (not slot 0) → reject
- Positive control: ATA-create funded by fee payer at slot 0 → accept
Confirms P1: 0 (inherited fee-payer ATA drain closed via
reject_fee_payer_in_instruction_accounts! sweep + ATA-create slot-0
carve-out). Remaining P2 findings are pre-existing follow-ups documented
in the original r5 review (harness adapter paths, server resource path
parity, PAYMENT-RESPONSE header) — out of scope for this fix.
Add tracker-note references at the two Ruby exact-verifier spots where the
port intentionally diverges from the Rust/TypeScript spine:

  1. Optional-instruction allowlist permits AssociatedTokenAccount::Create /
     CreateIdempotent in slots 3-4 alongside Memo + Lighthouse so a buyer
     can fund their own destination ATA in-band.
  2. Fee-payer-in-instruction-accounts sweep with an ATA-create-payer-slot
     carve-out (spine has no such sweep; the carve-out preserves the in-band
     destination-ATA-create flow while keeping the DRAIN attack surface
     covered).

Both divergences match the Go and Lua ports. Convergence with the Rust
spine is a protocol-wide decision tracked at
notes/lighthouse-allowlist-tracking.md.

Comment-only change. 208 tests still pass, standardrb clean.
P1-1 — Ruby x402 server replay ordering (L8):
- Refactor settle_exact_payment to follow broadcast -> confirm ->
  put_if_absent on the confirmed signature, mirroring MPP
  `server/charge.rs:535-556` and the x402 SDK pull-mode contract
  recorded in skills/x402-sdk-implementation/references/pr-readiness.md.
- Drop pre-broadcast `duplicate?` reserve and the release-on-failure path
  (claim-first creates a release race; the on-chain signature is the
  global uniqueness primitive).
- Replay key is scheme-namespaced as
  `x402-svm-exact:consumed:<base58_signature>` so x402 schemes do not
  bleed into each other or into MPP's `solana-charge:consumed:<sig>`.
- Add `await_confirmation` polling `getSignatureStatuses` until
  confirmed/finalized, with discriminated failure on explicit RPC `err`
  and a bounded timeout.
- Add `signature_confirmer` injection on `Server::State` so tests can
  drive ordering/failure scenarios without standing up an RPC.
- New tests cover: broadcast -> confirm -> put_if_absent ordering,
  canonical `signature_consumed` token on duplicate, no-record-on-
  broadcast-failure (retry allowed), and no-record-on-confirmation-
  failure.

P1-2 — Harness adapter Cargo manifest path:
- Post-`tests/interop` -> `harness` rename, the rust-x402 client/server
  adapter commands still pointed at `../../rust/Cargo.toml`, which no
  longer resolves from the harness CWD. Fix to `../rust/Cargo.toml`,
  matching the MPP rust adapter, so the rust<->ruby x402-exact matrix
  can spawn the rust spine.
- The ruby x402 sh -c adapters were broken by the same rename
  (`cd ../../ruby` -> `cd ../ruby`).
The Ruby interop server returned only the fixture settlement header
on a successful 200 response. The Rust spine (rust/crates/x402/src/
bin/interop_server.rs L221-231) and the TS fixture (harness/src/
fixtures/typescript/exact-server.ts L322-331) both emit the canonical
x402 v2 PAYMENT-RESPONSE header alongside the fixture settlement
header. Without it, x402 v2 clients cannot consume the Ruby server
as protocol-ready.

Header value is raw (non-base64) JSON carrying the canonical
PaymentResponse fields: { success, network, transaction }. Mirrors
the Rust and TS serializations exactly. The fixture
x-fixture-settlement header is preserved so existing harness
assertions keep working.

Adds a regression assertion in the existing
test_protected_route_returns_settlement_success that the
PAYMENT-RESPONSE header is present and decodes to the canonical
three-field shape.
… env vars

Hardcoded /protected and x-fixture-settlement prevented cross-server
scenarios from driving the route and header name. State now reads
X402_INTEROP_RESOURCE_PATH and X402_INTEROP_SETTLEMENT_HEADER with the
prior defaults, response_for routes on state.resource_path, and the
settlement response emits state.settlement_header. The interop client
binary also reads X402_INTEROP_SETTLEMENT_HEADER when extracting the
settlement value. Regression test asserts overrides flow through the
challenge URI, route dispatch, and response header.
Aligns the Ruby x402 client+server adapter env vars with the rest of the
x402 family (ts-x402, rust-x402) so that all x402-exact adapters opt in
via X402_INTEROP_CLIENTS / X402_INTEROP_SERVERS. Resolves PR solana-foundation#127 r8 P3
finding.

No CI workflow currently opts the ruby-x402-* adapters in via the old
MPP_INTEROP_* namespace, so this is a no-op for green CI and only
affects local runs that explicitly request the adapters.
PR solana-foundation#127 r8 P2 flagged that the cross-server-portability scenario was
wired as `ts-x402 -> rust-x402` while the TS reference client emits a
stub payload (`{ challengeId, resource }`) that does NOT deserialize
into the Rust spine's typed `PaymentProof::{transaction|signature}`
enum. Replaying that header to the Rust spine therefore produces
`payment_invalid` (parse error) rather than the canonical
`challenge_verification_failed` the scenario asserts.

Narrow the pair list to `[ts-x402, ts-x402]` so the assertion exercises
the full classifier path end-to-end, and document that the rust spine's
own portability semantics are covered by the rust/crates/x402 integration
tests. A follow-up can re-enable the cross-spine pair once the TS fixture
emits a typed PaymentProof payload.
…tring resource)

PR solana-foundation#127 r9 P1: the Ruby x402 client was rejecting offers emitted by the
TS reference fixture because

1. The fixture serialises offers with `maxAmountRequired` rather than
   the canonical Rust-spine `amount` field. Rust accepts either via
   string_field fallback at
   rust/crates/x402/src/protocol/schemes/exact/types.rs:337-339; Ruby
   was only checking `requirement["amount"]`.
2. The fixture's `PAYMENT-REQUIRED` envelope carries a bare `resource`
   URL string while Rust models the same field as a typed ResourceInfo
   object. Ruby's resource_from_envelope only kept Hash forms and
   silently dropped the string form, so the client lost the route
   context needed for downstream binding.

Update `selected_requirement?` to accept either amount field, and
normalise the resource field so consumers always see a
`{ "url" => <string> }` hash regardless of which spine issued the
challenge. Add two interop regression tests pinning both behaviours
against the TS fixture's wire shape.
Addresses maintainer feedback on PR solana-foundation#127: stop reimplementing primitives
that already live in the Ruby gem and stop reinventing Ed25519 in pure
Ruby. Both points were called out by lgalabru in the inline review on
ruby/lib/x402/exact.rb constants + Ed25519 block.

What moved to the shared core (Mpp::Methods::Solana::*):

- Base58 alphabet, encode, decode: removed from x402, delegated to
  Mpp::Methods::Solana::Base58.
- Program IDs (TOKEN, TOKEN_2022, SYSTEM, ATA, MEMO, COMPUTE_BUDGET) and
  the devnet USDC + PYUSD mint addresses: sourced from
  Mpp::Methods::Solana::Mints (single canonical table).
- ATA derivation + PDA find_program_address + on-curve check: delegated
  to Mpp::Methods::Solana::AssociatedToken + PublicKey.
- getLatestBlockhash RPC call: delegated to Mpp::Methods::Solana::Rpc
  (also gains an HTTPSuccess guard so non-2xx responses raise the
  canonical Mpp::Error with `getLatestBlockhash HTTP <code>`).
- Solana short_vec / compact-u16 helpers: lifted to
  Mpp::Methods::Solana::Transaction as module functions, mirrors Rust
  spine rust/crates/x402/src/protocol/schemes/exact/types.rs.

What got deleted outright:

- ~170 lines of pure-Ruby Ed25519 curve math in x402/exact.rb
  (ED25519_P / _D / _I / _L / _BASE_X / _BASE_Y, scalar_mult, point_add,
  encode_point, decode_point, mod_sqrt, prune_scalar, public_key_from_seed,
  sign_ed25519, the pure-Ruby verify_ed25519). Replaced with calls into
  the `ed25519` runtime gem already pinned in solana-pay-kit.gemspec.
  Ed25519PrivateKey is now a 6-line adapter wrapping Ed25519::SigningKey.
- The duplicate BASE58_ALPHABET, COMPUTE_BUDGET_PROGRAM, MEMO_PROGRAM,
  ASSOCIATED_TOKEN_PROGRAM, SYSTEM_PROGRAM, TOKEN_2022_PROGRAM constants
  that were redeclared in x402/exact.rb and x402/server.rb.

What stays x402-local (justified):

- LIGHTHOUSE_PROGRAM (x402-protocol-specific, not in MPP).
- DEFAULT_COMPUTE_UNIT_LIMIT and price caps (x402 transaction shape).
- verify_exact_instructions! and the structural x402 validators.
- STABLECOIN_MINTS CAIP-2 view in x402/client.rb now projects from the
  shared Mints::MINTS table instead of redeclaring addresses.

Net effect: ruby/lib/x402/exact.rb drops 776 -> 605 lines and zero
constants or crypto math are duplicated between mpp and x402.

Behavior + tests:

- bundle exec rake test: 214 runs, 737 assertions, 0 failures, 0 errors.
- bundle exec standardrb: clean.
- One x402 client test (`test_latest_blockhash_rejects_http_failure`)
  now asserts the canonical Mpp::Error shape instead of the legacy
  RuntimeError; the net-http stub helper was widened to intercept the
  instance-level Net::HTTP#start path used by Mpp::Methods::Solana::Rpc.
- The pre-existing with_rpc_http helper in support_test wraps canned
  Struct responses with code "200" + is_a?(Net::HTTPSuccess) so the new
  HTTP-status guard in Mpp::Methods::Solana::Rpc#call treats them as 2xx.
Mirrors the Rust spine umbrella layout (solana-pay-core / solana-mpp /
solana-x402 / solana-pay-kit). All shared Solana primitives + JCS RFC
8785 + RFC 7235 auth-param parser + RFC 3339 + base64url + canonical
L6 error codes live under PayCore::*. Both solana-mpp (Mpp::*) and
solana-x402 (X402::*) consume PayCore directly; no cross-layer
references.

  +------------------------------------------------------------+
  |                       solana-pay-kit                       |
  +---------------------------+--------------------------------+
  |        solana-mpp         |          solana-x402           |
  +---------------------------+--------------------------------+
  |                       solana-pay-core                      |
  +------------------------------------------------------------+

New files under ruby/lib/pay_core/:

- base64_url.rb        PayCore::Base64Url
- json.rb              PayCore::Json (RFC 8785 JCS)
- headers.rb           PayCore::Headers (generic RFC 7235 auth-param parser)
- rfc3339_parser.rb    PayCore::Rfc3339Parser
- error_codes.rb       PayCore::ErrorCodes (canonical L6 codes + classifier)
- solana/base58.rb     PayCore::Solana::Base58
- solana/programs.rb   PayCore::Solana::Programs (NEW: SYSTEM / TOKEN / TOKEN_2022 /
                       ASSOCIATED_TOKEN / MEMO / COMPUTE_BUDGET / LIGHTHOUSE)
- solana/caip2.rb      PayCore::Solana::Caip2 (NEW: MAINNET / DEVNET / TESTNET)
- solana/mints.rb      PayCore::Solana::Mints
- solana/public_key.rb PayCore::Solana::PublicKey (PDA derivation + on-curve)
- solana/ata.rb        PayCore::Solana::ATA (NEW name; was AssociatedToken)
- solana/account.rb    PayCore::Solana::Account
- solana/transaction.rb PayCore::Solana::Transaction (wire codec + short_vec helpers)
- solana/rpc.rb        PayCore::Solana::Rpc

Backward-compat alias layer (no public-API churn for MPP consumers):

- Mpp::Methods::Solana::Base58           = PayCore::Solana::Base58
- Mpp::Methods::Solana::Mints            = PayCore::Solana::Mints
- Mpp::Methods::Solana::PublicKey        = PayCore::Solana::PublicKey
- Mpp::Methods::Solana::Account          = PayCore::Solana::Account
- Mpp::Methods::Solana::AssociatedToken  = PayCore::Solana::ATA
- Mpp::Methods::Solana::Rpc              < PayCore::Solana::Rpc (overrides
                                           error class to raise Mpp::Error)
- Mpp::Methods::Solana::Transaction      < PayCore::Solana::Transaction
                                           (overrides sign_with error class to
                                            raise Mpp::VerificationError)
- Mpp::Methods::Solana::{Message, Instruction, AddressLookup, Cursor}
                                         = PayCore::Solana::*
- Mpp::Core::Base64Url                   = PayCore::Base64Url
- Mpp::Core::Json                        = PayCore::Json
- Mpp::Core::Rfc3339Parser               = PayCore::Rfc3339Parser
- Mpp::Core::Headers                     delegates to PayCore::Headers for
                                         generic auth-param parsing; keeps
                                         MPP-specific parse_www_authenticate /
                                         format_receipt / parse_receipt because
                                         they construct Mpp::Core::Challenge /
                                         Mpp::Core::Receipt
- Mpp::ErrorCodes                        = PayCore::ErrorCodes

solana-x402 (X402::Interop::*) now consumes PayCore directly; no
Mpp:: references remain in ruby/lib/x402/*.rb.

Umbrella ruby/lib/pay_kit.rb re-exports PayCore + Mpp + X402 under
PayKit::Core / PayKit::Mpp / PayKit::X402. Existing
`require "mpp"` and direct `require "x402/..."` paths still work.

Test results:

- bundle exec rake test: 229 runs, 770 assertions, 0 failures, 0 errors.
  214 baseline MPP tests preserved unchanged via aliases;
  15 new PayCore tests assert (a) PayCore::* are the canonical homes,
  (b) Mpp::* aliases resolve via assert_same,
  (c) Mpp::Methods::Solana::Rpc / Transaction subclass PayCore variants,
  (d) X402::Interop::Server::DEFAULT_NETWORK reads from
      PayCore::Solana::Caip2::DEVNET (no string literal duplicate).
- bundle exec standardrb: clean.

Test for `latest_blockhash_rejects_http_failure` updated to expect
PayCore::Solana::Rpc::RpcError (was previously expected to be
Mpp::Error, which only fires through the MPP charge path via the
subclass override).
Codex r1 P2 nit: the docstring on `PayCore::Solana::Transaction` said
"higher layers may catch and re-raise without subclassing", but the
in-tree extension point is exactly the private `signing_error_class`
hook overridden by `Mpp::Methods::Solana::Transaction`. Update the
comment to match the actual contract so reviewers do not expect a
catch-and-rethrow shape that subclasses do not use.
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.
Comment thread ruby/lib/x402/interop/server.rb Outdated
require "x402/exact"

module X402
module Interop

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the interop code doing here? 🤔

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the interop fixture out of the production-looking ruby/lib/x402/ mainline and into ruby/lib/x402/interop/ in 431d55e. server.rb and exact.rb now live at ruby/lib/x402/interop/server.rb and ruby/lib/x402/interop/exact.rb; the file path makes the fixture-only status obvious. The module top-level comment in interop/server.rb explicitly labels it fixture-only and notes that the production x402 server surface is intentionally out of scope for this PR. ruby/lib/x402.rb only requires the interop modules and carries the same disclaimer.

Comment thread ruby/lib/x402/client.rb Outdated

module X402
module Interop
module Client

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have a ruby client?
We should only support ruby server imo.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped the Ruby x402 client entirely in 431d55e: ruby/lib/x402/client.rb, ruby/bin/x402-interop-client, and ruby/test/x402_interop_client_test.rb are gone. Removed the matching ruby-x402-client harness adapter in 6b3c6f6. Trimmed the now-unused client-side helpers (build_exact_payment_signature_from_rpc, public_key_base58, latest_blockhash) from X402::Interop::Exact in 9ed6a85. The harness exercises Ruby in the server role only; client-side payloads are produced by the TS/Rust/Go/Python adapters.

Per maintainer feedback on solana-foundation#127: do not maintain backward-compat shims
for the Mpp::Methods::Solana::* and Mpp::Core::* layers. Every shared
primitive now lives only in PayCore; MPP source files reach into
PayCore::Solana::* and PayCore::* directly.

Deleted:
 - ruby/lib/mpp/methods/solana/{base58,mints,public_key,account,
   associated_token,rpc,transaction}.rb (alias and subclass shims)
 - ruby/lib/mpp/core/{base64_url,json,headers,rfc3339_parser}.rb
   (alias and delegating shims)
 - ruby/lib/mpp/error_codes.rb (alias shim)
 - ruby/test/pay_core_test.rb (obsolete alias-resolution suite)

Kept under Mpp:::
 - Mpp::Headers (MPP-specific Payment header formatter/parser, wraps
   PayCore::Headers for generic auth-param parsing)
 - Mpp::Core::{Challenge,ChallengeEcho,Credential,Receipt}
 - Mpp::Methods::Solana::{Verifier,VerificationResult,ChargeMethod}
 - Mpp::{Error,VerificationError,Challenge,Settlement,Server,...}

The Rpc and Transaction error subclasses (Mpp::Error, Mpp::VerificationError)
are no longer raised by the wire layer; PayCore::Solana::Rpc::RpcError and
PayCore::Solana::Transaction::SigningError surface directly and are caught
at the MPP boundary in Mpp::Internal::Handler#handle.
Comment thread ruby/lib/x402/interop/exact.rb Outdated
require "pay_core/solana/rpc"
require "pay_core/solana/transaction"

module X402

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect the x402 module to look more or less like this:

Image

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And similar for the mpp module.

…:Interop

Per maintainer feedback on solana-foundation#127:
 - "Why do we have a ruby client? We should only support ruby server."
 - "What is the interop code doing here?"

Drop the Ruby x402 client surface entirely. The cross-language harness
exercises Ruby in the server role only; the client side is covered by
the TS/Rust/Go/Python adapters.

Move the remaining x402 interop fixture out of the production-looking
`ruby/lib/x402/` mainline and into `ruby/lib/x402/interop/` so the
fixture-only nature is obvious in the file path:

 - Delete `ruby/lib/x402/client.rb`,
   `ruby/bin/x402-interop-client`,
   `ruby/test/x402_interop_client_test.rb`.
 - Move `ruby/lib/x402/server.rb` -> `ruby/lib/x402/interop/server.rb`.
 - Move `ruby/lib/x402/exact.rb` -> `ruby/lib/x402/interop/exact.rb`.
 - Update `ruby/lib/x402.rb` to require only the interop modules and
   document that the production x402 server surface is out of scope for
   this PR.
 - Update `ruby/bin/x402-interop-server` and
   `ruby/test/x402_interop_server_test.rb` to the new require paths.

Tests after this commit: 186 runs, 680 assertions, 0 failures.
Codex r2 P1/P2/P3 follow-ups:

 - harness/ruby-server/server.rb still imported the deleted
   Mpp::Methods::Solana::Account alias; switch to
   ::PayCore::Solana::Account.
 - harness/src/implementations.ts still registered the now-deleted
   ruby-x402-client adapter; remove the entry so X402_INTEROP_CLIENTS
   cannot reselect a dead binary.
 - lua/mpp/solana/rpc.lua header referenced the deleted
   Mpp::Methods::Solana::Rpc and Mpp::Error wrapping discipline; point
   at PayCore::Solana::Rpc and PayCore::Solana::Rpc::RpcError instead.
Codex r2 round 2 flagged the remaining client-side helpers in
`X402::Interop::Exact` as a client surface inside the lib. They were
not called by the interop server, the harness, or the test suite. Drop
them so the only "client" code path remaining is the test fixture
`build_exact_payment_signature`, which exists solely to construct a
fake client-signed payload for server-verification tests.

Removed:
 - `build_exact_payment_signature_from_rpc` (client RPC + sign helper)
 - `public_key_base58` (client pubkey emit)
 - `latest_blockhash` (client RPC wrapper)
Restructure ruby/lib/x402/ to mirror the Rust spine at
rust/crates/x402/src/ instead of the previous interop-flavored
single namespace.

  lib/x402.rb                                  -> lib.rs
  lib/x402/constants.rb                        -> constants.rs
  lib/x402/error.rb                            -> error.rs
  lib/x402/protocol/schemes/exact/types.rb     -> protocol/schemes/exact/types.rs
  lib/x402/protocol/schemes/exact/verify.rb    -> protocol/schemes/exact/verify.rs
  lib/x402/server/exact.rb                     -> server/exact.rs
  bin/x402-interop-server                      -> bin/interop_server.rs

X402::Server::Exact is the production server entry point; the
former X402::Interop::Server::State becomes X402::Server::Exact::Config
(State alias retained for back-compat). The 11-rule verifier moves
into X402::Protocol::Schemes::Exact::Verifier with each rule citing
the spine verify.rs line range. The interop bin is a thin TCP
adapter; all harness env reads (X402_INTEROP_*) live in the bin,
not in the library.
Replace the env-keyed Config constructor with typed kwargs
(rpc_url:, pay_to:, facilitator_secret_key:, amount:, ...) so
production callers can wire X402::Server::Exact::Config directly
without going through X402_INTEROP_* env vars.

The harness-specific env parsing moves into
Config.from_interop_env, used only by bin/x402-interop-server.
@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

Status + a scoping question before I keep going.

Phase 1 done (x402 module shape). ruby/lib/x402/ now mirrors the Rust spine:

ruby/lib/x402/
  constants.rb
  error.rb
  protocol/schemes/exact/{types,verify}.rb
  server/exact.rb
ruby/bin/x402-interop-server   # thin process-boundary wrapper

X402::Interop is deleted from lib/. Interop only lives at the bin/harness boundary now. 186 tests, 0 failures. Head: 5f61e04.

mpp module: proposing immediate follow-up PR after #127 merges, same shape:

ruby/lib/mpp/
  constants.rb
  error.rb
  protocol/{core,intents}/...
  server/{charge,session}.rb

OK with you?

Dual-gate syntax proposal. A reusable intent-set builder that works in both Sinatra and Rack, with shared pricing declared once:

PRICE = SolanaPayKit.accepts_payment do
  pay_to       ENV.fetch("PAY_TO")
  facilitator  ENV.fetch("FACILITATOR_URL")
  network      "solana-devnet"
  currency     "USDC"
  amount       "0.10"

  x402.exact
  mpp.charge description: "Premium report"
end

class App < Sinatra::Base
  helpers SolanaPayKit::Sinatra
  get("/report") { require_payment!(PRICE); json ok: true, paid_by: payment.scheme }
end

Same object plugs into Rack:

use SolanaPayKit::Rack::Payments, protect: { "/report" => PRICE }

Underneath it composes X402::Server::Exact + Mpp::Server::Charge directly, no protocol re-implementation. 402 body lists both accepts entries; wire headers stay protocol-specific (x402 X-PAYMENT, MPP Authorization: Payment). Public surface: SolanaPayKit.accepts_payment, SolanaPayKit::Sinatra, SolanaPayKit::Rack::Payments. Engines remain explicit underneath.

Scope question: ship this in #127, or as a separate feat(ruby): pay-kit dual gate PR after #127 lands?

My instinct is separate PR, so the structural cleanup lands first and the new API surface gets its own review surface.

Let me know your preference on naming, scope, and sequencing.

return Methods::Solana::VerificationResult.failure("Amount mismatch: credential has #{decoded.amount} but endpoint expects #{expected.amount}", code: ::PayCore::ErrorCodes::CODE_CHARGE_REQUEST_MISMATCH) unless decoded.amount == expected.amount
return Methods::Solana::VerificationResult.failure("Currency mismatch: credential has #{decoded.currency} but endpoint expects #{expected.currency}", code: ::PayCore::ErrorCodes::CODE_CHARGE_REQUEST_MISMATCH) unless decoded.currency == expected.currency
return Methods::Solana::VerificationResult.failure("Recipient mismatch", code: ::PayCore::ErrorCodes::CODE_CHARGE_REQUEST_MISMATCH) unless decoded.recipient == expected.recipient
return Methods::Solana::VerificationResult.failure("Method details mismatch", code: ::PayCore::ErrorCodes::CODE_CHARGE_REQUEST_MISMATCH) unless comparable_method_details(decoded.method_details) == comparable_method_details(expected.method_details)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can move mpp/internal to mpp/core

@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #138, which supersedes this PR with the full pay-kit v2 surface (x402 spine cleanup carried forward + mpp/internal -> mpp/core rename addressing your inline + PayKit unified gate API on top). Branch pr/ruby-x402-port is preserved for cherry-pick reference.

EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request May 26, 2026
Reshapes ruby/lib/mpp/ to match rust/crates/mpp/src/, addressing Ludo's
"similar for the mpp module" review on PR solana-foundation#127:

  ruby/lib/mpp/
    error.rb, expires.rb, store.rb, version.rb     stays (top-level utility)
    challenge.rb, settlement.rb                     stays (public 402/200 wrappers)
    sinatra.rb                                      stays (framework shim)
    protocol/
      core/
        challenge.rb           was mpp/core/challenge.rb
        credential.rb          was mpp/core/credential.rb
        receipt.rb             was mpp/core/receipt.rb
        headers.rb             was mpp/headers.rb (top-level)
        challenge_store.rb     was mpp/core/challenge_store.rb (was mpp/internal/)
      intents/
        charge.rb              was mpp/intent/charge_request.rb
      solana.rb                was mpp/methods/solana.rb
      solana/
        verifier.rb            was mpp/methods/solana/verifier.rb
        verification_result.rb was mpp/methods/solana/verification_result.rb
    server/
      charge.rb                merged mpp/server.rb (Server::Instance) and
                               mpp/core/handler.rb. Exposes Mpp::Server::Charge
                               (was Server::Instance) with nested
                               Mpp::Server::Charge::Handler (was Mpp::Core::Handler).
      middleware.rb            stays
      decorator.rb             stays

Module renames (no backward-compat shims):

  Mpp::Methods::Solana          -> Mpp::Protocol::Solana
  Mpp::Headers                  -> Mpp::Protocol::Core::Headers
  Mpp::Intent::ChargeRequest    -> Mpp::Protocol::Intents::ChargeRequest
  Mpp::Core::Challenge          -> Mpp::Protocol::Core::Challenge
  Mpp::Core::Credential         -> Mpp::Protocol::Core::Credential
  Mpp::Core::Receipt            -> Mpp::Protocol::Core::Receipt
  Mpp::Core::ChallengeStore     -> Mpp::Protocol::Core::ChallengeStore
  Mpp::Core::Handler            -> Mpp::Server::Charge::Handler
  Mpp::Server::Instance         -> Mpp::Server::Charge

Mpp.create unchanged (public factory). 259 tests, 0 failures.
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.

2 participants