Skip to content

feat(swift): mpp/session client#178

Open
EfeDurmaz16 wants to merge 2 commits into
solana-foundation:mainfrom
EfeDurmaz16:feat/swift-mpp-session
Open

feat(swift): mpp/session client#178
EfeDurmaz16 wants to merge 2 commits into
solana-foundation:mainfrom
EfeDurmaz16:feat/swift-mpp-session

Conversation

@EfeDurmaz16

Copy link
Copy Markdown
Collaborator

Adds the client-side MPP payment-channel session intent to the Swift SDK, mirroring the Rust reference spine (rust/crates/mpp + rust/crates/core) and the Go reference.

What

  • PayCore PaymentChannels: the 48-byte Ed25519 voucher preimage, channel + event-authority PDA derivation, and the open instruction 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 (the open passes its recipients inline rather than hashed).
  • Session wire types: SessionRequest, OpenPayload, SignedVoucher/VoucherData (cumulativeAmount with a cumulative read-alias), commit/topUp/close payloads, MeteringDirective/MeteringUsage, CommitReceipt, and the internally-tagged SessionAction codec (the action key, salt as a decimal string, the topUp tag).
  • ActiveSession: monotonic cumulative watermark, nonce accounting, retry-safe prepare/record voucher, the reconcileSettled lost-response clamp (Go parity), and the open/voucher/topUp/close action builders. recordVoucher binds the voucher to the active channel and advances the nonce to at least nonce + 1, matching Go RecordVoucher.
  • 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.
  • Session credential framing (serializeSessionCredential) and a solana/session challenge dispatch accessor.

Verification

  • swift test is green; the session source files are at 94 to 99 percent line coverage.
  • The runner manifest declares the session intent and the conformance runner gained a voucherPreimage branch driving the real PaymentChannels.voucherMessageBytes. Both frozen session-voucher vectors pass through the cross-SDK conformance driver (byte-identical to Go), now enforced in CI in the harness-swift job.

Notes

  • Client-only. Operability caveats that apply to a client are satisfied (the session reuses the MPP charge mint resolver, so localnet USDC resolves to the mainnet mint); the server-side caveats are not applicable.
  • The session credential uses the same sorted-keys canonicalization the existing Swift charge credential uses.

@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds the Swift client-side MPP payment-channel session intent to SolanaPayKit, mirroring the Go and Rust reference implementations. It introduces PaymentChannels (48-byte voucher preimage, PDA derivation, partial-signed open transaction), all session wire types, ActiveSession/SessionConsumer (monotonic watermark, retry-safe voucher signing, reconcileSettled lost-response clamp), and the SessionStream/HttpCommitTransport demo components.

  • Core session client (PaymentChannels.swift, SessionClient.swift, SessionConsumer.swift, SessionTypes.swift): protocol-level types and watermark logic, with conformance cross-validated against Go via the frozen session-voucher vectors now enforced in CI.
  • Demo app (SessionStream.swift, OpenAPI.swift, ContentView.swift): drives the full open → SSE stream → commit → settle flow and switches the playground discovery from a hardcoded catalog to dynamic OpenAPI parsing.
  • Harness + CI: adds the session intent to the Swift conformance runner, wires cross-SDK vector enforcement into swift.yml, and guards the Rust/TS surfpool tests behind a non-empty SURFPOOL_DATASOURCE_RPC_URL check rather than falling back to mainnet-beta.

Confidence Score: 5/5

Safe to merge. All previously-flagged bugs are addressed, the implementation is well-tested, and the cross-SDK conformance vectors are now CI-enforced.

All five bugs identified in prior review rounds have been resolved: the signerIndex bounds check is now explicit, the programId constant uses preconditionFailure instead of force-try, the salt silent-nil path is documented and bad strings now throw, the SessionStream commit endpoint is correct, and openPullAction takes an explicit tokenAccount parameter. Cost accumulation in the stream reader uses plain += which traps on overflow. The watermark/nonce logic mirrors the Go reference exactly and is backed by golden-vector cross-SDK tests enforced in CI. No new correctness issues were found.

No files require special attention.

Important Files Changed

Filename Overview
swift/Sources/SolanaPayKit/PayCore/PaymentChannels.swift New file: 48-byte voucher preimage, PDA derivation, and partially-signed open transaction builder. Previously-reported bugs (force-try on program ID, missing signerIndex bounds check) are fixed with preconditionFailure and an explicit bounds guard.
swift/Sources/SolanaPayKit/Protocols/Mpp/Client/SessionClient.swift New file: ActiveSession watermark/nonce/expiry logic, PaymentChannelSession opener, and credential framing. The previously-reported tokenAccount bug (channelId was passed instead) is fixed — openPullAction now takes an explicit tokenAccount parameter. Overflow in addToWatermark is guarded with addingReportingOverflow.
swift/Sources/SolanaPayKit/Protocols/Mpp/Client/SessionConsumer.swift New file: SessionConsumer/CommitTransport/MeteredDelivery. Watermark advances only on committed receipt; replayed receipts reconcile via min(settled, prepared) preventing both regression and server-inflated advancement. Logic matches Go reference.
swift/Sources/SolanaPayKit/Protocols/Mpp/Core/SessionTypes.swift New file: all session wire types (SessionRequest, OpenPayload, VoucherData, SessionAction, etc.). Internally-tagged SessionAction codec is correct; salt decodes from string-or-number; cumulativeAmount/cumulative alias is handled; previously-noted silent-nil salt path is documented.
swift/Examples/PayKitDemo/PayKitDemo/SessionStream.swift New file: full demo session flow (challenge → open → SSE → commit → poll). Previously-reported bugs are fixed: commitURL now passes the correct commit endpoint (not streamURL), and cost accumulation uses plain += (traps on overflow) instead of the former &+=.
swift/Examples/PayKitDemo/PayKitDemo/OpenAPI.swift New file: decodes playground /openapi.json into [Endpoint]. Sorted for stable order; palette-indexed tints; path-template filling for parameterized routes. Clean implementation, no issues found.
swift/Examples/PayKitDemo/PayKitDemo/ContentView.swift Updated demo UI: replaced hardcoded EndpointCatalog with dynamic OpenAPI discovery, added session-flow dispatch, and protocol-picker (mpp / x402) on charge cards. Small but non-trivial refactor, no issues found.
.github/workflows/swift.yml Adds a cross-SDK conformance vector step (MPP_CONFORMANCE_LANGUAGES=swift vitest run) before the harness smoke test, enforcing the frozen session-voucher vectors in CI.
rust/crates/mpp/tests/charge_integration.rs Updated surfpool initialisation: SURFPOOL_DATASOURCE_RPC_URL must now be non-empty to run the integration tests; without it the test skips cleanly instead of falling back to public mainnet-beta RPC (which rate-limits in CI).
swift/Sources/mpp-conformance/main.swift Adds voucherPreimage branch to the conformance runner, driving the real PaymentChannels.voucherMessageBytes for cross-SDK byte-exact assertion.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant App as SessionStream
    participant Server as Playground Server
    participant Chain as Solana via Operator

    App->>Server: GET stream unauthenticated
    Server-->>App: 402 session challenge with recentBlockhash

    Note over App: PaymentChannelSession.open builds partial-signed tx

    App->>Server: GET stream with open credential
    Server->>Chain: co-sign and broadcast open tx
    Server-->>App: 200 SSE stream

    loop SSE deliveries
        Server-->>App: data chunk with cost string
        App->>App: "total += cost, traps on overflow"
    end
    Server-->>App: data DONE

    App->>Server: POST deliveries to reserve
    Server-->>App: MeteringDirective

    Note over App: SessionConsumer.commitDirective signs voucher

    App->>Server: POST commit with signed voucher
    Server-->>App: CommitReceipt committed or replayed

    alt committed
        Note over App: recordVoucher advances watermark
    else replayed
        Note over App: reconcileSettled clamps to min of settled and prepared
    end

    loop poll up to 8 times
        App->>Server: GET receipt for channelId
        Server-->>App: finalized with settle signature
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant App as SessionStream
    participant Server as Playground Server
    participant Chain as Solana via Operator

    App->>Server: GET stream unauthenticated
    Server-->>App: 402 session challenge with recentBlockhash

    Note over App: PaymentChannelSession.open builds partial-signed tx

    App->>Server: GET stream with open credential
    Server->>Chain: co-sign and broadcast open tx
    Server-->>App: 200 SSE stream

    loop SSE deliveries
        Server-->>App: data chunk with cost string
        App->>App: "total += cost, traps on overflow"
    end
    Server-->>App: data DONE

    App->>Server: POST deliveries to reserve
    Server-->>App: MeteringDirective

    Note over App: SessionConsumer.commitDirective signs voucher

    App->>Server: POST commit with signed voucher
    Server-->>App: CommitReceipt committed or replayed

    alt committed
        Note over App: recordVoucher advances watermark
    else replayed
        Note over App: reconcileSettled clamps to min of settled and prepared
    end

    loop poll up to 8 times
        App->>Server: GET receipt for channelId
        Server-->>App: finalized with settle signature
    end
Loading

Reviews (15): Last reviewed commit: "Update swift/Examples/PayKitDemo/PayKitD..." | Re-trigger Greptile

Comment thread swift/Sources/SolanaPayKit/PayCore/PaymentChannels.swift
Comment thread swift/Sources/SolanaPayKit/PayCore/PaymentChannels.swift Outdated
Comment thread swift/Sources/SolanaPayKit/Protocols/Mpp/Core/SessionTypes.swift
Comment thread swift/README.md Outdated
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request Jun 19, 2026
… salt decode

Address solana-foundation#178 review: guard the signature-slot subscript so a payer index outside
the required-signer prefix throws instead of crashing (greptile P1), and document
the silent-nil behaviour of the string-or-number salt decode (greptile P2).
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request Jun 19, 2026
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.
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request Jun 19, 2026
Address solana-foundation#178 review (Ludo): the demo now fetches the playground's
/openapi.json on launch and renders every priced operation (from each route's
x-payment-info offers) as the tappable endpoint collection, instead of a static
catalog, then consumes one over MPP. Adds OpenAPI.swift (JSONSerialization
decode of paths + x-payment-info), points the app at the playground on :3000,
refreshes the README, and updates the screenshot to the live OpenAPI-driven flow
(Stock quote settled over MPP).
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request Jun 19, 2026
Comment thread swift/Examples/PayKitDemo/PayKitDemo/SessionStream.swift Outdated
Comment thread swift/Examples/PayKitDemo/docs/paykit-demo-screenshot.png
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request Jun 22, 2026
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.
@EfeDurmaz16 EfeDurmaz16 force-pushed the feat/swift-mpp-session branch from 53bdaea to ba3f79d Compare June 22, 2026 17:56
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request Jun 22, 2026
… salt decode

Address solana-foundation#178 review: guard the signature-slot subscript so a payer index outside
the required-signer prefix throws instead of crashing (greptile P1), and document
the silent-nil behaviour of the string-or-number salt decode (greptile P2).
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request Jun 22, 2026
Address solana-foundation#178 review (Ludo): the demo now fetches the playground's
/openapi.json on launch and renders every priced operation (from each route's
x-payment-info offers) as the tappable endpoint collection, instead of a static
catalog, then consumes one over MPP. Adds OpenAPI.swift (JSONSerialization
decode of paths + x-payment-info), points the app at the playground on :3000,
refreshes the README, and updates the screenshot to the live OpenAPI-driven flow
(Stock quote settled over MPP).
EfeDurmaz16 added a commit to EfeDurmaz16/mpp-sdk that referenced this pull request Jun 22, 2026
Comment thread swift/Sources/SolanaPayKit/Protocols/Mpp/Client/SessionClient.swift Outdated
@EfeDurmaz16 EfeDurmaz16 force-pushed the feat/swift-mpp-session branch from 617d15e to 009977c Compare June 22, 2026 21:59
lgalabru
lgalabru previously approved these changes Jun 23, 2026
Adds the client-side MPP payment-channel session intent to the Swift SDK,
mirroring the Rust reference spine and the Go reference.

What

- PayCore PaymentChannels: the 48-byte Ed25519 voucher preimage, channel +
  event-authority PDA derivation, and the open instruction 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 (the open passes its
  recipients inline rather than hashed).
- Session wire types: SessionRequest, OpenPayload, SignedVoucher/VoucherData
  (cumulativeAmount with a cumulative read-alias), commit/topUp/close payloads,
  MeteringDirective/MeteringUsage, CommitReceipt, and the internally-tagged
  SessionAction codec (the action key, salt as a decimal string, the topUp tag).
- ActiveSession: monotonic cumulative watermark, nonce accounting, retry-safe
  prepare/record voucher, the reconcileSettled lost-response clamp (Go parity),
  and the open/voucher/topUp/close action builders. recordVoucher binds the
  voucher to the active channel and advances the nonce to at least nonce + 1,
  matching Go RecordVoucher.
- 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.
- Session credential framing (serializeSessionCredential) and a solana/session
  challenge dispatch accessor.

Verification

- swift test is green; the session source files are at 94 to 99 percent line
  coverage.
- The runner manifest declares the session intent and the conformance runner
  gained a voucherPreimage branch driving the real
  PaymentChannels.voucherMessageBytes. Both frozen session-voucher vectors pass
  through the cross-SDK conformance driver (byte-identical to Go), now enforced
  in CI in the harness-swift job.

Review feedback addressed

- Bounds-check the open-tx payer signer index subscript (greptile P1).
- Replace the force-try programId with a lazy precondition guard (greptile P2).
- Document the silent-nil salt decode and throw on non-numeric strings (greptile P2).
- Pass the commit endpoint URL (not the stream URL) to the delivery reservation (greptile P1).
- Add an explicit tokenAccount parameter to openPullAction instead of misusing the channel PDA (greptile P1).
- Restore the original demo screenshot.
- Mark mpp/session as supported in the compatibility matrix.

Notes

- Client-only. Operability caveats that apply to a client are satisfied (the
  session reuses the MPP charge mint resolver, so localnet USDC resolves to the
  mainnet mint); the server-side caveats are not applicable.
- The session credential uses the same sorted-keys canonicalization the existing
  Swift charge credential uses.
@EfeDurmaz16 EfeDurmaz16 force-pushed the feat/swift-mpp-session branch from 009977c to 649b5a6 Compare June 23, 2026 10:08
Comment thread swift/Examples/PayKitDemo/PayKitDemo/SessionStream.swift
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
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