Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ jobs:
- name: Run tests with coverage
run: pnpm vitest run --coverage --config vitest.config.ci.ts
env:
SURFPOOL_REPORT: "1"
# Reliable RPC datasource for the embedded surfnet (see the Rust job).
SURFPOOL_DATASOURCE_RPC_URL: ${{ secrets.SURFPOOL_DATASOURCE_RPC_URL }}

Expand Down Expand Up @@ -176,7 +175,6 @@ jobs:
- name: Run tests with coverage
working-directory: rust
env:
SURFPOOL_REPORT: "1"
# Reliable RPC datasource for the embedded surfnet — without it the
# surfpool integration tests clone from the public mainnet-beta RPC,
# which rate-limits and crashes surfnet mid-test in CI.
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/kotlin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ jobs:
- name: Typecheck harness
working-directory: harness
run: pnpm typecheck
- name: Build the Kotlin conformance runner
working-directory: harness/kotlin-conformance
run: gradle installDist --no-daemon
- name: Run Kotlin cross-SDK conformance vectors
working-directory: harness
env:
MPP_CONFORMANCE_LANGUAGES: kotlin
run: pnpm exec vitest run test/conformance.test.ts
- name: Pre-warm Gradle for the Kotlin harness client
working-directory: harness/kotlin-client
run: gradle installDist --no-daemon
Expand Down
2 changes: 2 additions & 0 deletions harness/kotlin-conformance/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.gradle/
build/
23 changes: 23 additions & 0 deletions harness/kotlin-conformance/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
plugins {
kotlin("jvm") version "2.3.21"
kotlin("plugin.serialization") version "2.3.21"
application
}

dependencies {
// Path-included build, see settings.gradle.kts.
implementation("com.solana.paykit:solana-pay-kit-kotlin")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
}

kotlin {
jvmToolchain(17)
}

application {
mainClass.set("com.solana.paykit.conformance.MainKt")
}

tasks.named<JavaExec>("run") {
standardInput = System.`in`
}
16 changes: 16 additions & 0 deletions harness/kotlin-conformance/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
}
}

rootProject.name = "mpp-kotlin-conformance"
includeBuild("../../kotlin")
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.solana.paykit.conformance

import com.solana.paykit.paycore.PaymentChannels
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.util.Base64

/**
* Cross-SDK conformance-vector runner for the Kotlin SDK.
*
* Honors the same stdin/stdout contract as the TypeScript reference runner
* (harness/src/conformance/ts-runner.ts), the Go runner (go/cmd/conformance),
* and the Swift runner (swift Sources/mpp-conformance): read one conformance
* vector as JSON on stdin, drive the real SDK path for the requested
* intent + mode, and emit one RunnerResult line as JSON on stdout. Anything
* else (JVM/BouncyCastle chatter) must go to stderr so the harness parses a
* single clean JSON line.
*
* The Kotlin SDK is CLIENT-only and the harness drives it for the `session`
* intent only (harness/runners/kotlin.json). The single session vector shipped
* today is the canonical-bytes 48-byte voucher preimage; this runner decodes
* input.voucherPreimage and emits the bytes via the real
* PaymentChannels.voucherMessageBytes encoder. Any other mode is reported as an
* unsupported-mode reject the driver skips.
*/

@Serializable
private data class VoucherPreimage(val channelId: String, val cumulativeAmount: String, val expiresAt: Long)

@Serializable
private data class VectorInput(val voucherPreimage: VoucherPreimage? = null)

@Serializable
private data class Vector(val id: String, val intent: String? = null, val mode: String, val input: VectorInput)

@Serializable
private data class ExactBytes(val bytes: List<Int>? = null, val base64Url: String? = null)

@Serializable
private data class RunnerResult(
val id: String,
val outcome: String,
val exactBytes: ExactBytes? = null,
val error: String? = null,
)

private val json = Json {
ignoreUnknownKeys = true
encodeDefaults = false
explicitNulls = false
}

fun main() {
val raw = System.`in`.readBytes().decodeToString()
val result = try {
runVector(json.decodeFromString(Vector.serializer(), raw))
} catch (error: Throwable) {
System.err.println("kotlin conformance runner error: ${error.message}")
RunnerResult(id = "unknown", outcome = "reject", error = error.message ?: "unknown error")
}
println(json.encodeToString(RunnerResult.serializer(), result))
}

private fun runVector(vector: Vector): RunnerResult {
if (vector.mode != "canonical-bytes") {
return RunnerResult(
vector.id, "reject",
error = "unsupported-mode: kotlin conformance runner only implements canonical-bytes session vectors",
)
}
val preimage = vector.input.voucherPreimage
?: return RunnerResult(
vector.id, "reject",
error = "kotlin conformance runner only supports the session voucherPreimage canonical-bytes vector",
)
val cumulative = preimage.cumulativeAmount.toULongOrNull()
?: return RunnerResult(vector.id, "reject", error = "invalid cumulativeAmount ${preimage.cumulativeAmount}")

// Drive the real SDK encoder so the byte assertion exercises the same path
// the session voucher signer uses.
val bytes = PaymentChannels.voucherMessageBytes(preimage.channelId, cumulative, preimage.expiresAt)
return RunnerResult(
id = vector.id,
outcome = "accept",
exactBytes = ExactBytes(
bytes = bytes.map { it.toInt() and 0xff },
base64Url = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes),
),
)
}
6 changes: 6 additions & 0 deletions harness/runners/kotlin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"language": "kotlin",
"command": ["sh", "-c", "exec build/install/mpp-kotlin-conformance/bin/mpp-kotlin-conformance"],
"cwd": "harness/kotlin-conformance",
"intents": ["session"]
}
14 changes: 10 additions & 4 deletions kotlin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,15 @@ curl -i https://402.surfnet.dev/paid
MPP_CLIENT_SECRET_KEY_HEX=<hex> ChargeClient https://402.surfnet.dev/paid
```

`examples/AndroidDemo` is a full Seeker / Android demo app that gates one
endpoint behind a wallet signature; its README walks through running it on a
device or emulator.
`examples/AndroidDemo` is a Jetpack Compose app that mirrors the iOS demo: on
launch it fetches the playground's `/openapi.json`, renders the priced
endpoints as a tappable collection, generates a local signer, tops it up over
Surfpool cheatcodes, and consumes one over MPP. Its README walks through
running it on an emulator.

<div align="center">
<img alt="AndroidDemo app" width="320" src="https://github.com/solana-foundation/pay-kit/raw/main/kotlin/examples/AndroidDemo/docs/android-demo-screenshot.png">
</div>

## x402

Expand All @@ -135,7 +141,7 @@ The Machine Payments Protocol charge intent. The client parses the
|---|:---:|
| `mpp/charge/pull` | ✅ |
| `mpp/charge/push` | ✅ |
| `mpp/session` | |
| `mpp/session` | |
| `mpp/subscription` | — |

## Vocabulary
Expand Down
26 changes: 15 additions & 11 deletions kotlin/examples/AndroidDemo/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# MPP Charge Demo (Android)
# PayKit Demo (Android)

A minimal Jetpack Compose Android app that pays a 402-protected route
using the Kotlin MPP SDK at `kotlin/` and signs the transaction with a
real Solana wallet via Mobile Wallet Adapter.

Tracked under issue #114.
A Jetpack Compose app that mirrors the iOS PayKitDemo. On launch it
fetches the pay-kit playground's `/openapi.json` (over `10.0.2.2:3000`,
the emulator's host loopback), renders every priced operation (read from
each route's `x-payment-info` offers) as a tappable collection, generates
a local signer, tops it up over Surfpool cheatcodes, and consumes any
endpoint over MPP, surfacing each charge's settlement signature in an
append-only log.

## Layout

Expand All @@ -14,7 +16,7 @@ kotlin/examples/AndroidDemo
│ ├── build.gradle.kts AGP + Compose configuration
│ └── src/main/
│ ├── AndroidManifest.xml single launcher activity
│ └── java/com/solana/mpp/demo/MainActivity.kt
│ └── java/com/solana/paykit/demo/ MainActivity.kt + OpenApi.kt
├── build.gradle.kts root project, declares plugins
├── settings.gradle.kts single `:app` module
└── gradle/wrapper/ pinned Gradle 8.10.2
Expand Down Expand Up @@ -81,11 +83,13 @@ destinations remain HTTPS-only.

## End-to-end screenshot

End-to-end run in the Android 34 (arm64) emulator against local
Surfpool + the iOSDemo's `MerchantServer/serve.py`. App shows
"HTTP 200", the fortune body, and the on-chain settlement signature.
End-to-end run in the Android 34 (arm64) emulator against the pay-kit
playground (`10.0.2.2:3000`) + the hosted Surfpool sandbox. The
endpoints are rendered from the playground's OpenAPI spec; the log shows
a `Stock quote — 200 OK` consumed over MPP with its on-chain settlement
signature.

![Android emulator screenshot showing HTTP 200 and settlement signature](docs/android-demo-screenshot.png)
![Android emulator screenshot: OpenAPI endpoints and a settled MPP charge](docs/android-demo-screenshot.png)

## Expected UI state

Expand Down
4 changes: 4 additions & 0 deletions kotlin/examples/AndroidDemo/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ dependencies {
implementation(composeBom)
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
// Extended Material icon set (CreditCard, ShowChart, Verified, Dangerous,
// MonetizationOn, etc.) used by the endpoint cards and log rows to mirror
// the iOS demo's SF Symbols. Version is supplied by the Compose BOM above.
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
implementation("androidx.activity:activity-compose:1.9.2")
Expand Down
Loading
Loading