Skip to content

fix(x402): resolve missing SPL decimals from mint accounts - #324

Open
EfeDurmaz16 wants to merge 7 commits into
solana-foundation:mainfrom
EfeDurmaz16:fix/x402-mint-decimals-credit
Open

EfeDurmaz16 wants to merge 7 commits into
solana-foundation:mainfrom
EfeDurmaz16:fix/x402-mint-decimals-credit

Conversation

@EfeDurmaz16

@EfeDurmaz16 EfeDurmaz16 commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

When an x402 SPL offer omits extra.decimals, clients now read the mint instead of assuming six decimals. Explicit hints remain range-checked, and native SOL avoids the lookup.

Continues #297 by @latent-9, whose original cross-SDK implementation is carried forward onto current main with co-author credit. This PR also fixes the Python fallback to support the built-in SolanaRpc, isolates Swift HTTP mocks so tests can run in parallel, and preserves Kotlin trailing-lambda callers.

Validation:

  • Python: 1,280 tests passed; Ruff and targeted Pyright passed.
  • Swift: 205 tests passed with default parallel execution.
  • Rust: 235 x402 tests passed; formatting checked.
  • Go: solanatx, x402, and x402/client packages passed.
  • Kotlin: 318 tests passed, including compatibility coverage for existing trailing-lambda calls.

EfeDurmaz16 and others added 5 commits September 17, 2026 12:28
Carry forward the cross-SDK decimals work from solana-foundation#297 onto current main. Preserve explicit hints and fetch mint decimals when offers omit them.

Co-authored-by: latent-9 <296084221+latent-9@users.noreply.github.com>
Handle the string address and tuple response used by SolanaRpc while preserving solana-py support. Exercise the built-in RPC through a mock HTTP transport and verify the encoded decimals byte.
@EfeDurmaz16
EfeDurmaz16 requested a balanced review from Copilot September 17, 2026 09:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@greptile-apps

greptile-apps Bot commented Sep 17, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; both previous blocking findings are fully addressed and no new actionable issues were found.

Findings

  1. P1 Trailing lambda compatibility breaks
  2. P1 Malformed hint bypasses validation

Summary

This PR replaces the six-decimal fallback for x402 SPL payments with authoritative mint-account lookups across Go, Kotlin, Python, Rust, and Swift.

  • Preserves explicit decimal hints while rejecting malformed or out-of-range values.
  • Avoids mint lookups for native SOL payments.
  • Preserves Kotlin trailing-lambda compatibility and supports Python’s built-in SolanaRpc.
  • Updates transaction verification, shared vectors, and language-specific regression tests.
  • Isolates Swift HTTP test state for parallel execution.

Reviews (2) · Last reviewed commit: "test(kotlin): retain original x402 build..."

fun x402(
rpcBlockhashProvider: () -> ByteArray,
selection: ChallengeSelection = ChallengeSelection(),
mintDecimalsProvider: ((String) -> UByte)? = null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Trailing lambda compatibility breaks

Adding mintDecimalsProvider as the final function-typed parameter breaks the public builder's existing trailing-lambda form. A consumer that previously wrote .x402 { fetchBlockhash() } had that lambda bound to rpcBlockhashProvider; it is now matched against (String) -> UByte and fails to compile. Preserve the old overload separately or move the new provider before the existing trailing-lambda parameter.

Knowledge Base Used: Kotlin SDK

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

The original main signature also ends in selection: ChallengeSelection = ChallengeSelection(), so .x402 { fetchBlockhash() } did not compile before this PR either. I verified that with the Kotlin compiler. The existing named and parenthesized call forms remain supported; 7dc3498 adds a behavioral regression test for both, including a successful payment retry. Full Kotlin suite: 318 tests passed. Could you re-evaluate this finding against the original signature?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You're right — I withdraw this finding. With the original signature, selection was the last parameter and was not function-typed, so .x402 { fetchBlockhash() } did not compile before this PR. After the change, the new final parameter is function-typed, but rpcBlockhashProvider is still required, so that expression still cannot compile because it would not satisfy the required first argument. The supported forms covered by the regression test — .x402({ fetchBlockhash() }) and .x402(rpcBlockhashProvider = { fetchBlockhash() }) — remain compatible, including the successful retry behavior. No change is required for this comment.

Comment on lines 530 to 532
decimals_raw = req.get("decimals")
if not isinstance(decimals_raw, int) or isinstance(decimals_raw, bool):
decimals_raw = extra.get("decimals")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Malformed hint bypasses validation

When a challenge contains an invalid top-level decimals and a valid extra.decimals, these lines discard the invalid value before validation. For example, "decimals": "9" is silently ignored and the payment is signed using the nested hint instead of rejecting the malformed offer. Only an absent or null top-level value should fall back to extra.decimals.

Suggested change
decimals_raw = req.get("decimals")
if not isinstance(decimals_raw, int) or isinstance(decimals_raw, bool):
decimals_raw = extra.get("decimals")
decimals_raw = req.get("decimals")
if decimals_raw is None:
decimals_raw = extra.get("decimals")

Knowledge Base Used:

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.

Fixed in 38aa832. Only an absent or null top-level value falls back now. Added malformed-hint cases with valid and absent nested hints, plus null fallback coverage. Full Python suite: 1,280 tests passed.

Only absent or null hints may fall back to extra.decimals. Cover malformed hints with and without a valid nested value, plus the null fallback.
@latent-9

Copy link
Copy Markdown

Thanks for carrying this forward, @EfeDurmaz16 — and for the co-author credit. 🙏

I skimmed the carry-forward: Rust mint fetch, Kotlin wiring, Go/Swift strictness, and the Python mask removal are all preserved, and your follow-ups (built-in RPC support, Swift mock isolation, Kotlin trailing-lambda compat) address exactly the gaps I'd have hit next.

Happy to help with any review fallout on the decimals path. I'll leave #297 open until this merges, then close it as superseded.

@joshinikhil415-lgtm

Copy link
Copy Markdown

Kael Pulse protocol-watch $0.05 Exact SVM — Solana L1/runtime/policy diffs since your cursor → https://kael-ecosystem-pulse.onrender.com/v1/pulse/watch?src=cash19 (402→PayAI); free teaser https://kael-ecosystem-pulse.onrender.com/v1/pulse/sample?src=cash19

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.

4 participants