feat(kotlin): mpp/session client#179
Open
EfeDurmaz16 wants to merge 14 commits into
Open
Conversation
Port the client-side MPP session surface to Kotlin (com.solana.paykit), mirroring the Rust spine and the Go reference: - paycore PaymentChannels: 48-byte Ed25519 voucher preimage, channel + event-authority PDA derivation, and the open instruction + payer-signed (operator-fee-payer-unsigned) open transaction. Client subset only; the server-side settle/finalize/distribute + ed25519 precompile + BLAKE3 distribution hash are out of scope for this client-only SDK. - Session wire types (kotlinx.serialization): SessionRequest, OpenPayload (salt as decimal string via SaltStringSerializer), SignedVoucher/VoucherData (cumulativeAmount with cumulative read-alias), commit/topUp/close payloads, MeteringDirective/Usage, CommitReceipt, and the SessionAction sealed union with the SessionActionCodec flatten/unflatten codec (action key, topUp tag). - ActiveSession: monotonic cumulative watermark, nonce accounting, retry-safe prepare/record voucher, the ReconcileSettled lost-response clamp (Go solana-foundation#162 parity), and the open/voucher/topUp/close action builders. - SessionConsumer: validate -> sign -> commit -> advance, advancing only on a committed receipt and reconciling (clamped, never regressing) on a replayed one. - Session credential framing (serializeSessionCredential) + solana/session challenge dispatch (sessionRequest / requireSolanaSession).
Voucher preimage pinned against the frozen cross-SDK session-voucher vector (channelId(32)||cumulative LE u64||expiresAt LE i64, incl. near-u64-max with no precision loss) + signature verification, channel PDA stability, ActiveSession watermark/nonce/expiry + reconcile clamp, SessionConsumer commit + replay reconcile-and-clamp, the SessionAction wire codec, and the opener guards (pull + clientVoucher only).
… advance recordVoucher now rejects a voucher whose channelId does not match the active session and advances the nonce to at least nonce+1 (or the voucher nonce when higher), matching Go RecordVoucher. Mirrors the same fix applied to the swift port; the kotlin session client is Go-flavoured (carries Go's ReconcileSettled).
…tent Add harness/kotlin-conformance, a small gradle application (path-included SDK build, like harness/kotlin-client) that reads a conformance vector on stdin and emits a RunnerResult, driving the real PaymentChannels.voucherMessageBytes encoder for the canonical-bytes voucherPreimage branch. Register it via harness/runners/kotlin.json with intents=["session"] so the frozen session-voucher vector now runs against the Kotlin SDK through the cross-SDK driver (verified: both session vectors -> accept, byte-identical to swift/go).
…in job Pre-warm the kotlin-conformance project and run the conformance driver for the kotlin runner, so the session-voucher byte invariant is enforced in CI.
Comment on lines
+70
to
+72
| } else { | ||
| element.longOrNull?.toULong() ?: throw MppException.InvalidJson() | ||
| } |
There was a problem hiding this comment.
The number branch of
SaltStringSerializer uses JsonPrimitive.longOrNull to read the salt. Any salt value greater than Long.MAX_VALUE (~9.2 × 10¹⁸) will cause longOrNull to return null, which throws InvalidJson. Since salt is a random u64, about half of all valid salts would silently break deserialization if a peer sends them as a JSON number rather than a string. Using content.toULongOrNull() on the primitive's raw string avoids the Long range limit entirely.
Suggested change
| } else { | |
| element.longOrNull?.toULong() ?: throw MppException.InvalidJson() | |
| } | |
| } else { | |
| element.content.toULongOrNull() ?: throw MppException.InvalidJson() | |
| } |
Mirror the swift solana-foundation#178 review fix: guard the signature-slot index so a payer outside the required-signer prefix throws instead of going out of bounds.
…penAPI Address Ludo's review (solana-foundation#179): rewrite the Compose demo to mirror the iOS PayKitDemo. It fetches the playground's /openapi.json (over 10.0.2.2:3000), renders every priced operation (from each route's x-payment-info offers) as a tappable card collection, generates a local signer, tops up over Surfpool cheatcodes, and consumes one over MPP. iOS-styled grouped UI (bold title, inset rounded cards, log). Adds OpenApi.kt, embeds the screenshot in kotlin/README.md, and drops the stale Mobile-Wallet-Adapter e2e assets. Two real fixes surfaced while capturing the screenshot on the emulator: consume() now runs OkHttp on Dispatchers.IO (was NetworkOnMainThreadException), and the Section card lays its children in a Column (rows previously overlapped).
… binary The runner manifest cwd resolves against the repo root (runners.ts), so it must be harness/kotlin-conformance, not kotlin-conformance (which pointed at a nonexistent <repo>/kotlin-conformance and failed the spawn). Invoke the self-contained installDist binary via sh -c so it resolves relative to that cwd and runs through $JAVA_HOME/bin/java (no gradle on the spawn PATH needed); the CI step now builds it with gradle installDist.
…Null Address solana-foundation#179 greptile P1: SaltStringSerializer's number branch used JsonPrimitive.longOrNull, so a salt above Long.MAX_VALUE (about half of all random u64 salts) sent as a JSON number deserialized to null and threw. Parse the raw primitive content as ULong for both the string and number forms.
Address Ludo's review (solana-foundation#179): the Compose UI now mirrors the SwiftUI demo's iOS inset-grouped design. Thin uppercase gray section headers, white 10dp rounded section cards on a #F2F2F7 grouped background with hairline row separators; an Account section with a green dollar Balance + monospaced values; endpoint cards (150x130, 14dp radius) with a vertical tint gradient, a per- endpoint Material icon (chart/quote/sparkles/credit-card, not a single $ glyph) matching the iOS SF Symbols, and a white capsule GET/POST badge; and a log row with the green check-seal / red / info status icons, a monospaced timestamp + signature + body, and a blue 'View receipt on pay.sh' link. Adds material-icons-extended and refreshes the screenshot.
…xactly The iOS demo picks the card glyph by payment intent (every charge endpoint shows the credit-card icon); the Android demo was keying off path/label and showed a different icon per card. Mirror systemImage(intent:scheme:method:) so the cards carry the same glyph as Swift, and refresh the screenshot.
…onsume by intent Mirror the swift fix: read every offer's method (mpp/x402) and the intent, show them on each card (e.g. 'x402 · mpp', 'mpp', 'mpp · session'), and only settle one-shot charge endpoints over MPP. Non-charge intents (session streaming, subscription, x402 upto) show a clear message about their dedicated multi-step API instead of firing a charge the server's non-charge 402 rejects. Re-aligned to the current OpenAPI offers and refreshed the screenshot.
…tream + voucher + settle) Mirror the swift session flow: the stream card now opens a payment channel (PaymentChannelSession.open, server co-signs + broadcasts), reads the SSE metered deliveries, reserves a delivery + signs and commits a cumulative voucher via SessionConsumer, and polls the receipt for the on-chain settle signature. Adds SessionStream.kt (OkHttp SSE + side-channel transport, mirroring TS SessionFetch) and routes session intent to it. Verified e2e on the emulator against the playground + Surfpool sandbox: streamed 7 chunks, settled. Also surfaces the selected protocol per card: the accepted methods (x402/mpp) render with the one the demo settles over (mpp) emphasized.
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.
Adds the client-side MPP payment-channel session intent to the Kotlin SDK (
com.solana.paykit), mirroring the Rust reference spine and the Go reference.What
PaymentChannels: the 48-byte Ed25519 voucher preimage, channel + event-authority PDA derivation, and theopeninstruction plus the payer-signed (operator-fee-payer-unsigned) open transaction. Client subset only; the server-side settle/finalize/distribute, the Ed25519 precompile, and the BLAKE3 distribution hash are out of scope for this client-only SDK.SessionRequest,OpenPayload(salt as a decimal string viaSaltStringSerializer),SignedVoucher/VoucherData(cumulativeAmountwith acumulativeread-alias), commit/topUp/close payloads,MeteringDirective/MeteringUsage,CommitReceipt, and theSessionActionsealed union with theSessionActionCodecflatten/unflatten codec (theactionkey, thetopUptag).ActiveSession: monotonic cumulative watermark, nonce accounting, retry-safe prepare/record voucher, thereconcileSettledlost-response clamp (Go parity), and the action builders.recordVoucherbinds the voucher to the active channel and advances the nonce to at leastnonce + 1, matching GoRecordVoucher.SessionConsumer: validate, sign, commit, advance. The local watermark advances only on a committed receipt and reconciles (clamped to the prepared voucher, never regressing) on a replayed one.serializeSessionCredential, reusing the existing JCS canonicalization) and asolana/sessionchallenge dispatch.Verification
gradle checkis green (tests, jacoco coverage gate, ktlint).harness/kotlin-conformance(a small gradle application that path-includes the SDK, mirroringharness/kotlin-client) reads a vector on stdin and drives the realPaymentChannels.voucherMessageBytes, registered viaharness/runners/kotlin.jsonwithintents=["session"]. Both frozensession-vouchervectors pass through the cross-SDK conformance driver (byte-identical to Swift and Go), now enforced in CI in theharness-kotlinjob.Notes