-
Notifications
You must be signed in to change notification settings - Fork 0
CI rehearsal only (do not merge) #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8c59ab9
2291822
1aa8693
efff41c
66449a5
c4341ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"autoReview": []} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| .gradle/ | ||
| build/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| 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") | ||
| testImplementation(kotlin("test")) | ||
| } | ||
|
|
||
| kotlin { | ||
| jvmToolchain(17) | ||
| } | ||
|
|
||
| application { | ||
| mainClass.set("com.solana.paykit.protocolrunner.MainKt") | ||
| } | ||
|
|
||
| tasks.named<JavaExec>("run") { | ||
| standardInput = System.`in` | ||
| } | ||
|
|
||
| tasks.test { | ||
| useJUnitPlatform() | ||
| } |
| 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-protocol-runner" | ||
| includeBuild("../../kotlin") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| package com.solana.paykit.protocolrunner | ||
|
|
||
| import com.solana.paykit.protocols.mpp.core.MppHeaders | ||
| import com.solana.paykit.protocols.mpp.core.PaymentCredential | ||
| import kotlinx.serialization.json.Json | ||
| import kotlinx.serialization.json.JsonElement | ||
| import kotlinx.serialization.json.JsonNull | ||
| import kotlinx.serialization.json.JsonObject | ||
| import kotlinx.serialization.json.JsonPrimitive | ||
| import kotlinx.serialization.json.buildJsonObject | ||
| import kotlinx.serialization.json.jsonObject | ||
| import kotlinx.serialization.json.jsonPrimitive | ||
| import kotlinx.serialization.json.put | ||
| import java.util.Base64 | ||
| import kotlin.system.exitProcess | ||
|
|
||
| private val unsupported = setOf( | ||
| "challenge.format", | ||
| "credential.parse", | ||
| "receipt.parse", | ||
| "receipt.format", | ||
| "base64url.encode", | ||
| "base64url.decode", | ||
| "challenge.id", | ||
| ) | ||
|
|
||
| fun main() { | ||
| val response = respond(System.`in`.readBytes().decodeToString()) | ||
| println(response) | ||
| if (Json.parseToJsonElement(response).jsonObject["error_type"] == JsonPrimitive("runner_error")) { | ||
| exitProcess(1) | ||
| } | ||
| } | ||
|
|
||
| internal fun respond(line: String): String { | ||
| val request = try { | ||
| Json.parseToJsonElement(line).jsonObject | ||
| } catch (error: IllegalArgumentException) { | ||
| return failure(error.message ?: "malformed request", "runner_error") | ||
| } | ||
| val op = (request["op"] as? JsonPrimitive)?.content.orEmpty() | ||
| val input = request["input"] ?: JsonNull | ||
| return try { | ||
| when (op) { | ||
| "challenge.parse" -> success(parseChallenge(input)) | ||
| "credential.format" -> success(buildJsonObject { put("header", formatCredential(input)) }) | ||
| in unsupported -> failure("$op unsupported by the Kotlin SDK", family(op)) | ||
| else -> failure("unknown operation: $op", "unsupported_operation") | ||
| } | ||
| } catch (error: Exception) { | ||
| failure(error.message ?: error.toString(), family(op)) | ||
| } | ||
| } | ||
|
|
||
| private fun parseChallenge(input: JsonElement): JsonObject { | ||
| val challenge = MppHeaders.parseWWWAuthenticate(input.jsonObject.getValue("header").jsonPrimitive.content) | ||
| return buildJsonObject { | ||
| put("id", challenge.id) | ||
| put("realm", challenge.realm) | ||
| put("method", challenge.method) | ||
| put("intent", challenge.intent) | ||
| put("request", decodeJson(challenge.request)) | ||
| challenge.expires?.let { put("expires", it) } | ||
| challenge.digest?.let { put("digest", it) } | ||
| challenge.opaque?.let { put("opaque", decodeJson(it)) } | ||
| } | ||
|
Comment on lines
+57
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| private fun formatCredential(input: JsonElement): String { | ||
| val credential = input.jsonObject | ||
| val challenge = credential.getValue("challenge").jsonObject | ||
| val request = challenge["request"] ?: JsonObject(emptyMap()) | ||
| val encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(request.toString().encodeToByteArray()) | ||
| val wire = JsonObject(credential + ("challenge" to JsonObject(challenge + ("request" to JsonPrimitive(encoded))))) | ||
| return MppHeaders.formatAuthorization(Json.decodeFromJsonElement(PaymentCredential.serializer(), wire)) | ||
| } | ||
|
|
||
| private fun decodeJson(value: String): JsonElement = | ||
| Json.parseToJsonElement(Base64.getUrlDecoder().decode(value).decodeToString()) | ||
|
|
||
| private fun family(op: String): String = when { | ||
| op.endsWith(".parse") -> "parse_error" | ||
| op.endsWith(".format") -> "format_error" | ||
| op.startsWith("base64url.") -> "encoding_error" | ||
| else -> "generation_error" | ||
| } | ||
|
|
||
| private fun success(result: JsonElement): String = | ||
| buildJsonObject { | ||
| put("success", true) | ||
| put("result", result) | ||
| }.toString() | ||
|
|
||
| private fun failure(error: String, errorType: String): String = | ||
| buildJsonObject { | ||
| put("success", false) | ||
| put("error", error) | ||
| put("error_type", errorType) | ||
| }.toString() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package com.solana.paykit.protocolrunner | ||
|
|
||
| import kotlinx.serialization.json.Json | ||
| import kotlinx.serialization.json.JsonObject | ||
| import kotlinx.serialization.json.JsonPrimitive | ||
| import kotlinx.serialization.json.jsonObject | ||
| import kotlinx.serialization.json.jsonPrimitive | ||
| import kotlin.test.Test | ||
| import kotlin.test.assertEquals | ||
| import kotlin.test.assertFalse | ||
| import kotlin.test.assertTrue | ||
|
|
||
| class MainTest { | ||
| private fun respondTo(line: String): JsonObject { | ||
| val output = respond(line) | ||
| assertFalse(output.contains('\n'), output) | ||
| return Json.parseToJsonElement(output).jsonObject | ||
| } | ||
|
|
||
| @Test | ||
| fun parsesBasicChallenge() { | ||
| val response = respondTo( | ||
| """{"op":"challenge.parse","input":{"header":"Payment id=\"ch_abc123\", realm=\"api.example.com\", method=\"tempo\", intent=\"charge\", request=\"eyJhbW91bnQiOiIxMDAwMDAwIiwiY3VycmVuY3kiOiIweDIwYzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEiLCJyZWNpcGllbnQiOiIweDEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3ODkwYWJjZGVmMTIzNDU2NzgifQ\""}}""", | ||
| ) | ||
| val golden = Json.parseToJsonElement( | ||
| """{"id":"ch_abc123","intent":"charge","method":"tempo","realm":"api.example.com","request":{"amount":"1000000","currency":"0x20c0000000000000000000000000000000000001","recipient":"0x1234567890abcdef1234567890abcdef12345678"}}""", | ||
| ) | ||
| assertEquals(JsonPrimitive(true), response["success"]) | ||
| assertEquals(golden, response["result"]) | ||
| } | ||
|
|
||
| @Test | ||
| fun refusesNonJsonStdin() { | ||
| val response = respondTo("not json") | ||
| assertEquals(JsonPrimitive(false), response["success"]) | ||
| assertEquals(JsonPrimitive("runner_error"), response["error_type"]) | ||
| } | ||
|
|
||
| @Test | ||
| fun reportsUnknownOperation() { | ||
| val response = respondTo("""{"op":"nope.op","input":{}}""") | ||
| assertEquals(JsonPrimitive(false), response["success"]) | ||
| assertEquals(JsonPrimitive("unsupported_operation"), response["error_type"]) | ||
| } | ||
|
|
||
| @Test | ||
| fun reportsMissingSdkFunctionAsFamilyError() { | ||
| val response = respondTo("""{"op":"receipt.parse","input":{"header":"eyJ9"}}""") | ||
| assertEquals(JsonPrimitive(false), response["success"]) | ||
| assertEquals(JsonPrimitive("parse_error"), response["error_type"]) | ||
| assertTrue(response.getValue("error").jsonPrimitive.content.contains("unsupported")) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "language": "kotlin", | ||
| "command": ["sh", "-c", "gradle -q installDist >&2 && exec build/install/mpp-kotlin-protocol-runner/bin/mpp-kotlin-protocol-runner"], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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! |
||
| "cwd": "harness/kotlin-protocol-runner" | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
opaqueas a pass-through string. When a valid challenge contains a plain value such astrace-123, decoding it as base64url JSON throws, so the runner returnsparse_errorinstead of the parsed challenge.