diff --git a/androidwalletdemo/build.gradle.kts b/androidwalletdemo/build.gradle.kts index 709e113f..7df87504 100644 --- a/androidwalletdemo/build.gradle.kts +++ b/androidwalletdemo/build.gradle.kts @@ -2,7 +2,6 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import java.io.FileInputStream import java.util.* -@Suppress("DSL_SCOPE_VIOLATION") plugins { id(libs.plugins.androidApplication.get().pluginId) kotlin("android") @@ -20,7 +19,7 @@ try { val isCI: Boolean = System.getenv("CI") == "true" val jwtServerUrl: String = if (isCI) "" else - gradleLocalProperties(rootDir).getProperty("jwtServerUrl") + gradleLocalProperties(rootDir, providers).getProperty("jwtServerUrl") ?: throw Error("You must set the jwtServerUrl property in a local.properties file") android { diff --git a/build.gradle.kts b/build.gradle.kts index a582977e..b6eb74e6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -83,7 +83,7 @@ subprojects { plugins.withId("com.vanniktech.maven.publish.base") { configure { - publishToMavenCentral(SonatypeHost.S01, automaticRelease = true) + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true) signAllPublications() pom { name.set(project.name) @@ -147,6 +147,7 @@ subprojects { if (project.name !in DEMO_APPS) { apply(plugin = "org.jetbrains.kotlin.multiplatform") + apply(plugin = "com.android.library") configure { jvmToolchain(11) androidTarget { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index a8613771..d2d14f1f 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,7 +1,6 @@ import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING import com.mgd.core.gradle.S3Upload -@Suppress("DSL_SCOPE_VIOLATION") plugins { kotlin("multiplatform") alias(libs.plugins.kotlinSerialization) @@ -127,14 +126,14 @@ tasks.register("generateSdkDocs") { } s3 { - bucket = "lsdev.web-dev" + bucket = "lightspark-dev-web" region = "us-west-2" } tasks.register("uploadDocsToS3") { group = "documentation" dependsOn("generateSdkDocs") - bucket = "ldev.web-dev" + bucket = "lightspark-dev-web" keyPrefix = "docs/kotlin" sourceDir = "docs/html" } diff --git a/core/gradle.properties b/core/gradle.properties index 070b813a..336ee0f5 100644 --- a/core/gradle.properties +++ b/core/gradle.properties @@ -1,7 +1,7 @@ GROUP=com.lightspark POM_ARTIFACT_ID=lightspark-core # Don't bump this manually. Run `scripts/versions.main.kt ` to bump the version instead. -VERSION_NAME=0.6.0 +VERSION_NAME=0.6.1 POM_DESCRIPTION=The Lightspark shared utilities library for Kotlin and Java. POM_INCEPTION_YEAR=2023 diff --git a/crypto/build.gradle.kts b/crypto/build.gradle.kts index 85c08568..448b5567 100644 --- a/crypto/build.gradle.kts +++ b/crypto/build.gradle.kts @@ -1,7 +1,6 @@ import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING import de.undercouch.gradle.tasks.download.Download -@Suppress("DSL_SCOPE_VIOLATION") plugins { kotlin("multiplatform") id(libs.plugins.androidLibrary.get().pluginId) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8bab457e..252f566f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # Core SDK deps (or common): acinqSecp256k1 = "0.10.1" -androidGradlePlugin = "8.1.2" +androidGradlePlugin = "8.10.0" appAuth = "0.11.1" buildKonfig = "0.13.3" dataStore = "1.0.0" @@ -12,17 +12,17 @@ gradleS3 = "1.2.1" jna = "5.13.0" kase64 = "1.0.6" kotest = "5.5.4" -kotlin = "1.9.0" +kotlin = "1.9.20" kotlinCompilerExtension = "1.4.0" kotlinCoroutines = "1.7.2" kotlinxDateTime = "0.4.0" kotlinSerializationJson = "1.4.1" ktlint = "11.3.1" ktor = "2.3.7" -lightsparkCore = "0.6.0" +lightsparkCore = "0.6.1" lightsparkCrypto = "0.6.0" -uma = "1.3.0" -mavenPublish = "0.25.2" +uma = "1.5.0" +mavenPublish = "0.32.0" mockitoCore = "5.5.0" taskTree = "2.1.1" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f6afdefa..b556af04 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Feb 02 10:27:20 PST 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/lightspark-sdk/README.md b/lightspark-sdk/README.md index e2d84790..14577555 100644 --- a/lightspark-sdk/README.md +++ b/lightspark-sdk/README.md @@ -17,14 +17,14 @@ Start by installing the SDK from maven: **build.gradle:** ```groovy dependencies { - implementation "com.lightspark:lightspark-sdk:0.19.0" + implementation "com.lightspark:lightspark-sdk:0.19.1" } ``` or with **build.gradle.kts:** ```kotlin dependencies { - implementation("com.lightspark:lightspark-sdk:0.19.0") + implementation("com.lightspark:lightspark-sdk:0.19.1") } ``` diff --git a/lightspark-sdk/build.gradle.kts b/lightspark-sdk/build.gradle.kts index 64cda832..942d3926 100644 --- a/lightspark-sdk/build.gradle.kts +++ b/lightspark-sdk/build.gradle.kts @@ -1,7 +1,6 @@ import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING import com.mgd.core.gradle.S3Upload -@Suppress("DSL_SCOPE_VIOLATION") plugins { kotlin("multiplatform") alias(libs.plugins.kotlinSerialization) @@ -137,14 +136,14 @@ tasks.register("generateSdkDocs") { } s3 { - bucket = "lsdev.web-dev" + bucket = "lightspark-dev-web" region = "us-west-2" } tasks.register("uploadDocsToS3") { group = "documentation" dependsOn("generateSdkDocs") - bucket = "ldev.web-dev" + bucket = "lightspark-dev-web" keyPrefix = "docs/kotlin" sourceDir = "docs/html" } diff --git a/lightspark-sdk/gradle.properties b/lightspark-sdk/gradle.properties index 27033c36..399428d5 100644 --- a/lightspark-sdk/gradle.properties +++ b/lightspark-sdk/gradle.properties @@ -1,7 +1,7 @@ GROUP=com.lightspark POM_ARTIFACT_ID=lightspark-sdk # Don't bump this manually. Run `scripts/versions.main.kt ` to bump the version instead. -VERSION_NAME=0.19.0 +VERSION_NAME=0.19.1 POM_DESCRIPTION=The Lightspark API SDK for Kotlin and Java. POM_INCEPTION_YEAR=2023 diff --git a/lightspark-sdk/src/commonJvmAndroidMain/kotlin/com/lightspark/sdk/LightsparkFuturesClient.kt b/lightspark-sdk/src/commonJvmAndroidMain/kotlin/com/lightspark/sdk/LightsparkFuturesClient.kt index de40f052..6349e90e 100644 --- a/lightspark-sdk/src/commonJvmAndroidMain/kotlin/com/lightspark/sdk/LightsparkFuturesClient.kt +++ b/lightspark-sdk/src/commonJvmAndroidMain/kotlin/com/lightspark/sdk/LightsparkFuturesClient.kt @@ -21,6 +21,7 @@ import com.lightspark.sdk.model.InvoiceType import com.lightspark.sdk.model.OutgoingPayment import com.lightspark.sdk.model.PaymentDirection import com.lightspark.sdk.model.RegionCode +import com.lightspark.sdk.model.ReleasePaymentPreimageOutput import com.lightspark.sdk.model.RiskRating import com.lightspark.sdk.model.TransactionStatus import com.lightspark.sdk.model.UmaInvitation @@ -116,6 +117,23 @@ class LightsparkFuturesClient(config: ClientConfig) { ): CompletableFuture = coroutineScope.future { coroutinesClient.getSingleNodeDashboard(nodeId, numTransactions, bitcoinNetwork) } + /** + * Marks a payment preimage as released. To be used when the recipient has received the payment. + * + * @param invoiceId The invoice the preimage belongs to. + * @param paymentPreimage The preimage to release. + */ + fun releasePaymentPreimage( + invoiceId: String, + paymentPreimage: String + ): CompletableFuture = + coroutineScope.future { + coroutinesClient.releasePaymentPreimage( + invoiceId, + paymentPreimage + ) + } + /** * Creates a lightning invoice for the given node. * @@ -126,6 +144,8 @@ class LightsparkFuturesClient(config: ClientConfig) { * @param memo Optional memo to include in the invoice. * @param type The type of invoice to create. Defaults to [InvoiceType.STANDARD]. * @param expirySecs The number of seconds until the invoice expires. Defaults to 1 day. + * @param paymentHash Optional payment hash to include in the invoice. + * @param preimageNonce Optional preimage nonce to include in the invoice. */ @JvmOverloads fun createInvoice( @@ -134,8 +154,20 @@ class LightsparkFuturesClient(config: ClientConfig) { memo: String? = null, type: InvoiceType = InvoiceType.STANDARD, expirySecs: Int? = null, + paymentHash: String? = null, + preimageNonce: String? = null, ): CompletableFuture = - coroutineScope.future { coroutinesClient.createInvoice(nodeId, amountMsats, memo, type, expirySecs) } + coroutineScope.future { + coroutinesClient.createInvoice( + nodeId, + amountMsats, + memo, + type, + expirySecs, + paymentHash, + preimageNonce, + ) + } /** * Creates a Lightning invoice for the given node. This should only be used for generating invoices for LNURLs, with @@ -146,6 +178,8 @@ class LightsparkFuturesClient(config: ClientConfig) { * @param metadata The LNURL metadata payload field from the initial payreq response. This will be hashed and * present in the h-tag (SHA256 purpose of payment) of the resulting Bolt 11 invoice. * @param expirySecs The number of seconds until the invoice expires. Defaults to 1 day. + * @param paymentHash Optional payment hash to include in the invoice. + * @param preimageNonce Optional preimage nonce to include in the invoice. */ @JvmOverloads fun createLnurlInvoice( @@ -153,6 +187,8 @@ class LightsparkFuturesClient(config: ClientConfig) { amountMsats: Long, metadata: String, expirySecs: Int? = null, + paymentHash: String? = null, + preimageNonce: String? = null, ): CompletableFuture = coroutineScope.future { coroutinesClient.createLnurlInvoice( @@ -160,6 +196,8 @@ class LightsparkFuturesClient(config: ClientConfig) { amountMsats, metadata, expirySecs, + paymentHash, + preimageNonce, ) } @@ -175,6 +213,8 @@ class LightsparkFuturesClient(config: ClientConfig) { * @param signingPrivateKey The receiver's signing private key. Used to hash the receiver identifier. * @param receiverIdentifier Optional identifier of the receiver. If provided, this will be hashed using a * monthly-rotated seed and used for anonymized analysis. + * @param paymentHash Optional payment hash to include in the invoice. + * @param preimageNonce Optional preimage nonce to include in the invoice. */ @JvmOverloads @Throws(IllegalArgumentException::class) @@ -185,6 +225,8 @@ class LightsparkFuturesClient(config: ClientConfig) { expirySecs: Int? = null, signingPrivateKey: ByteArray? = null, receiverIdentifier: String? = null, + paymentHash: String? = null, + preimageNonce: String? = null, ): CompletableFuture = coroutineScope.future { coroutinesClient.createUmaInvoice( @@ -194,6 +236,8 @@ class LightsparkFuturesClient(config: ClientConfig) { expirySecs, signingPrivateKey, receiverIdentifier, + paymentHash, + preimageNonce, ) } diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkCoroutinesClient.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkCoroutinesClient.kt index e8b3abe3..68b9fbba 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkCoroutinesClient.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkCoroutinesClient.kt @@ -172,6 +172,30 @@ class LightsparkCoroutinesClient private constructor( ) } + /** + * Marks a payment preimage as released. To be used when the recipient has received the payment. + * + * @param invoiceId The invoice the preimage belongs to. + * @param paymentPreimage The preimage to release. + */ + suspend fun releasePaymentPreimage(invoiceId: String, paymentPreimage: String): ReleasePaymentPreimageOutput { + requireValidAuth() + + return executeQuery( + Query( + ReleasePaymentPreimageMutation, + { + add("invoice_id", invoiceId) + add("payment_preimage", paymentPreimage) + }, + ) { + val releasePaymentPreimageJson = + requireNotNull(it["release_payment_preimage"]) { "Invalid response for payment preimage release" } + serializerFormat.decodeFromJsonElement(releasePaymentPreimageJson) + }, + ) + } + /** * Creates a lightning invoice for the given node. * @@ -182,6 +206,8 @@ class LightsparkCoroutinesClient private constructor( * @param memo Optional memo to include in the invoice. * @param type The type of invoice to create. Defaults to [InvoiceType.STANDARD]. * @param expirySecs The number of seconds until the invoice expires. Defaults to 1 day. + * @param paymentHash Optional payment hash to include in the invoice. + * @param preimageNonce Optional preimage nonce to include in the invoice. */ suspend fun createInvoice( nodeId: String, @@ -189,6 +215,8 @@ class LightsparkCoroutinesClient private constructor( memo: String? = null, type: InvoiceType = InvoiceType.STANDARD, expirySecs: Int? = null, + paymentHash: String? = null, + preimageNonce: String? = null, ): Invoice { requireValidAuth() return executeQuery( @@ -200,6 +228,8 @@ class LightsparkCoroutinesClient private constructor( memo?.let { add("memo", memo) } add("type", serializerFormat.encodeToJsonElement(type)) expirySecs?.let { add("expirySecs", expirySecs) } + paymentHash?.let { add("paymentHash", paymentHash) } + preimageNonce?.let { add("preimageNonce", preimageNonce) } }, ) { val invoiceJson = @@ -220,12 +250,16 @@ class LightsparkCoroutinesClient private constructor( * @param metadata The LNURL metadata payload field from the initial payreq response. This will be hashed and * present in the h-tag (SHA256 purpose of payment) of the resulting Bolt 11 invoice. * @param expirySecs The number of seconds until the invoice expires. Defaults to 1 day. + * @param paymentHash Optional payment hash to include in the invoice. + * @param preimageNonce Optional preimage nonce to include in the invoice. */ suspend fun createLnurlInvoice( nodeId: String, amountMsats: Long, metadata: String, expirySecs: Int? = null, + paymentHash: String? = null, + preimageNonce: String? = null, ): Invoice { requireValidAuth() @@ -241,6 +275,8 @@ class LightsparkCoroutinesClient private constructor( add("amountMsats", amountMsats) add("metadataHash", metadataHash) expirySecs?.let { add("expirySecs", expirySecs) } + paymentHash?.let { add("paymentHash", paymentHash) } + preimageNonce?.let { add("preimageNonce", preimageNonce) } }, ) { val invoiceJson = @@ -264,6 +300,8 @@ class LightsparkCoroutinesClient private constructor( * @param signingPrivateKey The receiver's signing private key. Used to hash the receiver identifier. * @param receiverIdentifier Optional identifier of the receiver. If provided, this will be hashed using a * monthly-rotated seed and used for anonymized analysis. + * @param paymentHash Optional payment hash to include in the invoice. + * @param preimageNonce Optional preimage nonce to include in the invoice. */ @Throws(IllegalArgumentException::class) suspend fun createUmaInvoice( @@ -273,6 +311,8 @@ class LightsparkCoroutinesClient private constructor( expirySecs: Int? = null, signingPrivateKey: ByteArray? = null, receiverIdentifier: String? = null, + paymentHash: String? = null, + preimageNonce: String? = null, ): Invoice { requireValidAuth() @@ -296,6 +336,8 @@ class LightsparkCoroutinesClient private constructor( add("metadataHash", metadataHash) expirySecs?.let { add("expirySecs", expirySecs) } receiverHash?.let { add("receiverHash", receiverHash) } + paymentHash?.let { add("paymentHash", paymentHash) } + preimageNonce?.let { add("preimageNonce", preimageNonce) } }, ) { val invoiceJson = diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkSyncClient.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkSyncClient.kt index 481cdecb..99b0b624 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkSyncClient.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkSyncClient.kt @@ -98,6 +98,22 @@ class LightsparkSyncClient constructor(config: ClientConfig) { bitcoinNetwork: BitcoinNetwork = defaultBitcoinNetwork, ): WalletDashboard? = runBlocking { asyncClient.getSingleNodeDashboard(nodeId, numTransactions, bitcoinNetwork) } + /** + * Marks a payment preimage as released. To be used when the recipient has received the payment. + * + * @param invoiceId The invoice the preimage belongs to. + * @param paymentPreimage The preimage to release. + */ + fun releasePaymentPreimage( + invoiceId: String, + paymentPreimage: String + ): ReleasePaymentPreimageOutput = runBlocking { + asyncClient.releasePaymentPreimage( + invoiceId, + paymentPreimage + ) + } + /** * Creates a lightning invoice for the given node. * @@ -108,6 +124,8 @@ class LightsparkSyncClient constructor(config: ClientConfig) { * @param memo Optional memo to include in the invoice. * @param type The type of invoice to create. Defaults to [InvoiceType.STANDARD]. * @param expirySecs The number of seconds until the invoice expires. Defaults to 1 day. + * @param paymentHash Optional payment hash to include in the invoice. + * @param preimageNonce Optional preimage nonce to include in the invoice. */ @JvmOverloads fun createInvoice( @@ -116,7 +134,19 @@ class LightsparkSyncClient constructor(config: ClientConfig) { memo: String? = null, type: InvoiceType = InvoiceType.STANDARD, expirySecs: Int? = null, - ): Invoice = runBlocking { asyncClient.createInvoice(nodeId, amountMsats, memo, type, expirySecs) } + paymentHash: String? = null, + preimageNonce: String? = null, + ): Invoice = runBlocking { + asyncClient.createInvoice( + nodeId, + amountMsats, + memo, + type, + expirySecs, + paymentHash, + preimageNonce, + ) + } /** * Creates a Lightning invoice for the given node. This should only be used for generating invoices for LNURLs, with @@ -127,6 +157,8 @@ class LightsparkSyncClient constructor(config: ClientConfig) { * @param metadata The LNURL metadata payload field from the initial payreq response. This will be hashed and * present in the h-tag (SHA256 purpose of payment) of the resulting Bolt 11 invoice. * @param expirySecs The number of seconds until the invoice expires. Defaults to 1 day. + * @param paymentHash Optional payment hash to include in the invoice. + * @param preimageNonce Optional preimage nonce to include in the invoice. */ @JvmOverloads fun createLnurlInvoice( @@ -134,12 +166,16 @@ class LightsparkSyncClient constructor(config: ClientConfig) { amountMsats: Long, metadata: String, expirySecs: Int? = null, + paymentHash: String? = null, + preimageNonce: String? = null, ): Invoice = runBlocking { asyncClient.createLnurlInvoice( nodeId, amountMsats, metadata, expirySecs, + paymentHash, + preimageNonce, ) } @@ -155,6 +191,8 @@ class LightsparkSyncClient constructor(config: ClientConfig) { * @param signingPrivateKey The receiver's signing private key. Used to hash the receiver identifier. * @param receiverIdentifier Optional identifier of the receiver. If provided, this will be hashed using a * monthly-rotated seed and used for anonymized analysis. + * @param paymentHash Optional payment hash to include in the invoice. + * @param preimageNonce Optional preimage nonce to include in the invoice. */ @JvmOverloads @Throws(IllegalArgumentException::class) @@ -165,6 +203,8 @@ class LightsparkSyncClient constructor(config: ClientConfig) { expirySecs: Int? = null, signingPrivateKey: ByteArray? = null, receiverIdentifier: String? = null, + paymentHash: String? = null, + preimageNonce: String? = null, ): Invoice = runBlocking { asyncClient.createUmaInvoice( nodeId, @@ -173,6 +213,8 @@ class LightsparkSyncClient constructor(config: ClientConfig) { expirySecs, signingPrivateKey, receiverIdentifier, + paymentHash, + preimageNonce, ) } diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/CreateInvoice.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/CreateInvoice.kt index b831235f..24828b6d 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/CreateInvoice.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/CreateInvoice.kt @@ -9,8 +9,19 @@ const val CreateInvoiceMutation = """ ${'$'}memo: String ${'$'}type: InvoiceType = null ${'$'}expirySecs: Int = null + ${'$'}paymentHash: Hash32 = null + ${'$'}preimageNonce: Hash32 = null ) { - create_invoice(input: { node_id: ${'$'}nodeId, amount_msats: ${'$'}amountMsats, memo: ${'$'}memo, invoice_type: ${'$'}type, expiry_secs: ${'$'}expirySecs }) { + create_invoice( + input: { + node_id: ${'$'}nodeId + amount_msats: ${'$'}amountMsats + memo: ${'$'}memo + invoice_type: ${'$'}type + expiry_secs: ${'$'}expirySecs + payment_hash: ${'$'}paymentHash + preimage_nonce: ${'$'}preimageNonce + }) { invoice { ...InvoiceFragment } diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/CreateLnurlInvoice.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/CreateLnurlInvoice.kt index 11897545..9ecb041d 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/CreateLnurlInvoice.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/CreateLnurlInvoice.kt @@ -9,6 +9,8 @@ const val CreateLnurlInvoiceMutation = """ ${'$'}metadataHash: String! ${'$'}expirySecs: Int = null ${'$'}receiverHash: String = null + ${'$'}paymentHash: Hash32 = null + ${'$'}preimageNonce: Hash32 = null ) { create_lnurl_invoice( input: { @@ -17,7 +19,9 @@ const val CreateLnurlInvoiceMutation = """ metadata_hash: ${'$'}metadataHash expiry_secs: ${'$'}expirySecs receiver_hash: ${'$'}receiverHash - } + payment_hash: ${'$'}paymentHash + preimage_nonce: ${'$'}preimageNonce + } ) { invoice { ...InvoiceFragment diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/CreateUmaInvoice.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/CreateUmaInvoice.kt index f56442cf..2b2b54bc 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/CreateUmaInvoice.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/CreateUmaInvoice.kt @@ -9,6 +9,8 @@ const val CreateUmaInvoiceMutation = """ ${'$'}metadataHash: String! ${'$'}expirySecs: Int = null ${'$'}receiverHash: String = null + ${'$'}paymentHash: Hash32 = null + ${'$'}preimageNonce: Hash32 = null ) { create_uma_invoice( input: { @@ -17,6 +19,8 @@ const val CreateUmaInvoiceMutation = """ metadata_hash: ${'$'}metadataHash expiry_secs: ${'$'}expirySecs receiver_hash: ${'$'}receiverHash + payment_hash: ${'$'}paymentHash + preimage_nonce: ${'$'}preimageNonce } ) { invoice { diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/OutgoingPaymentForIdempotencyKey.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/OutgoingPaymentForIdempotencyKey.kt index 13e68ccd..ed6893dc 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/OutgoingPaymentForIdempotencyKey.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/OutgoingPaymentForIdempotencyKey.kt @@ -7,8 +7,7 @@ query OutgoingPaymentForIdempotencyKey( ${'$'}idempotency_key: String! ) { outgoing_payment_for_idempotency_key(input: { - idempotency_key: ${'$'}idempotency_key, - statuses: ${'$'}transactionStatuses + idempotency_key: ${'$'}idempotency_key }) { payment { ...OutgoingPaymentFragment diff --git a/oauth/build.gradle.kts b/oauth/build.gradle.kts index 9f17ead8..b185382e 100644 --- a/oauth/build.gradle.kts +++ b/oauth/build.gradle.kts @@ -1,4 +1,3 @@ -@Suppress("DSL_SCOPE_VIOLATION") plugins { kotlin("android") id(libs.plugins.androidLibrary.get().pluginId) diff --git a/umaserverdemo/build.gradle.kts b/umaserverdemo/build.gradle.kts index 229493a9..f576b360 100644 --- a/umaserverdemo/build.gradle.kts +++ b/umaserverdemo/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation("io.ktor:ktor-server-auth-jvm") implementation("io.ktor:ktor-server-compression-jvm") implementation("io.ktor:ktor-server-netty-jvm") + implementation("io.ktor:ktor-server-status-pages-jvm") implementation(libs.uma) implementation(project(":lightspark-sdk")) implementation(project(":core")) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt index 48db747c..d51d8406 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt @@ -29,18 +29,24 @@ import io.ktor.server.routing.Routing import io.ktor.server.routing.get import io.ktor.server.routing.post import io.ktor.util.toMap +import java.util.UUID import java.util.concurrent.CompletableFuture import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.future.future import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.plus import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonPrimitive import me.uma.InMemoryNonceCache import me.uma.UMA_VERSION_STRING +import me.uma.UmaException import me.uma.UmaInvoiceCreator import me.uma.UmaProtocolHelper -import me.uma.UnsupportedVersionException +import me.uma.generated.ErrorCode import me.uma.protocol.CounterPartyDataOptions import me.uma.protocol.InvoiceCurrency import me.uma.protocol.KycStatus @@ -49,12 +55,6 @@ import me.uma.protocol.PayRequest import me.uma.protocol.createCounterPartyDataOptions import me.uma.protocol.createPayeeData import me.uma.protocol.identifier -import java.util.UUID -import kotlinx.datetime.DateTimeUnit -import kotlinx.datetime.plus -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonPrimitive - class ReceivingVasp( private val config: UmaConfig, @@ -87,13 +87,11 @@ class ReceivingVasp( suspend fun createAndSendInvoice(call: ApplicationCall): String { val senderUma = call.parameters["senderUma"] ?: run { - call.respond(HttpStatusCode.BadRequest, "SenderUma not provided.") - return "SenderUma not provided." + throw UmaException("SenderUma not provided.", ErrorCode.INVALID_INPUT) } val senderUmaComponents = senderUma.split("@") if (senderUmaComponents.size != 2) { - call.respond(HttpStatusCode.BadRequest, "SenderUma format invalid: $senderUma.") - return "SenderUma format invalid: $senderUma." + throw UmaException("SenderUma format invalid: $senderUma.", ErrorCode.INVALID_INPUT) } val (status, data) = createUmaInvoice(call, senderUma) if (status != HttpStatusCode.OK) { @@ -102,32 +100,32 @@ class ReceivingVasp( } val senderComponents = senderUma.split("@") val sendingVaspDomain = senderComponents.getOrNull(1) ?: run { - call.respond(HttpStatusCode.BadRequest, "Invalid senderUma.") - return "Invalid senderUma." + throw UmaException("Invalid senderUma.", ErrorCode.INVALID_INPUT) } val wellKnownConfiguration = "http://$sendingVaspDomain/.well-known/uma-configuration" val umaEndpoint = try { val umaConfigResponse = httpClient.get(wellKnownConfiguration) if (umaConfigResponse.status != HttpStatusCode.OK) { - call.respond( - HttpStatusCode.FailedDependency, + throw UmaException( "failed to fetch request / pay endpoint at $wellKnownConfiguration", + ErrorCode.INTERNAL_ERROR, ) - return "failed to fetch request / pay endpoint at $wellKnownConfiguration" } Json.decodeFromString( umaConfigResponse.bodyAsText(), )["uma_request_endpoint"]?.jsonPrimitive?.content } catch (e: Exception) { - call.respond( - HttpStatusCode.FailedDependency, + throw UmaException( "failed to fetch request / pay endpoint at $wellKnownConfiguration", + ErrorCode.INTERNAL_ERROR, + e, ) - return "failed to fetch request / pay endpoint at $wellKnownConfiguration" } if (umaEndpoint == null) { - call.respond(HttpStatusCode.FailedDependency, "failed to fetch $wellKnownConfiguration") - return "failed to fetch $wellKnownConfiguration" + throw UmaException( + "failed to fetch request / pay endpoint at $wellKnownConfiguration", + ErrorCode.INTERNAL_ERROR, + ) } val response = try { httpClient.post(umaEndpoint) { @@ -135,26 +133,25 @@ class ReceivingVasp( setBody(parameter("invoice", data)) } } catch (e: Exception) { - call.respond(HttpStatusCode.FailedDependency, "failed to fetch $umaEndpoint") - return "failed to fetch $umaEndpoint" + throw UmaException("Failed to make request to $umaEndpoint", ErrorCode.PAYREQ_REQUEST_FAILED, e) } if (response.status != HttpStatusCode.OK) { - call.respond(HttpStatusCode.InternalServerError, "Payreq to Sending Vasp failed: ${response.status}") - return "Payreq to sending failed: ${response.status}" + throw UmaException("Payreq to sending Vasp failed: ${response.status}", ErrorCode.PAYREQ_REQUEST_FAILED) } call.respond(response.body()) return "OK" } private fun createUmaInvoice( - call: ApplicationCall, senderUma: String? = null + call: ApplicationCall, + senderUma: String? = null, ): Pair { val amount = try { call.parameters["amount"]?.toLong() ?: run { - return HttpStatusCode.BadRequest to "Amount not provided." + throw UmaException("Amount not provided.", ErrorCode.INVALID_INPUT) } } catch (e: NumberFormatException) { - return HttpStatusCode.BadRequest to "Amount not parsable as number." + throw UmaException("Amount not parsable as number.", ErrorCode.INVALID_INPUT, e) } val currency = call.parameters["currencyCode"]?.let { currencyCode -> @@ -162,17 +159,17 @@ class ReceivingVasp( getReceivingCurrencies(UMA_VERSION_STRING).firstOrNull { it.code == currencyCode } ?: run { - return HttpStatusCode.BadRequest to "Unsupported CurrencyCode $currencyCode." + throw UmaException("Unsupported CurrencyCode $currencyCode.", ErrorCode.INVALID_CURRENCY) } } ?: run { - return HttpStatusCode.BadRequest to "CurrencyCode not provided." + throw UmaException("CurrencyCode not provided.", ErrorCode.INVALID_INPUT) } - + if (amount < currency.minSendable() || amount > currency.maxSendable()) { - return HttpStatusCode.BadRequest to "CurrencyCode amount is outside of sendable range." + throw UmaException("CurrencyCode amount is outside of sendable range.", ErrorCode.AMOUNT_OUT_OF_RANGE) } - val expiresIn2Days = Clock.System.now().plus(2, DateTimeUnit.HOUR*24) + val expiresIn2Days = Clock.System.now().plus(2, DateTimeUnit.HOUR * 24) val receiverUma = buildReceiverUma(call) @@ -181,7 +178,10 @@ class ReceivingVasp( invoiceUUID = UUID.randomUUID().toString(), amount = amount, receivingCurrency = InvoiceCurrency( - currency.code, currency.name, currency.symbol, currency.decimals + currency.code, + currency.name, + currency.symbol, + currency.decimals, ), expiration = expiresIn2Days.epochSeconds, isSubjectToTravelRule = true, @@ -193,7 +193,7 @@ class ReceivingVasp( ), callback = getLnurlpCallback(call), // structured the same, going to /api/uma/payreq/{user_id} privateSigningKey = config.umaSigningPrivKey, - senderUma = senderUma + senderUma = senderUma, ) return HttpStatusCode.OK to invoice.toBech32() @@ -201,25 +201,18 @@ class ReceivingVasp( suspend fun handleLnurlp(call: ApplicationCall): String { val username = call.parameters["username"] - - if (username == null) { - call.respond(HttpStatusCode.BadRequest, "Username not provided.") - return "Username not provided." - } + ?: throw UmaException("Username not provided.", ErrorCode.MISSING_REQUIRED_UMA_PARAMETERS) if (username != config.username && username != "$${config.username}") { - call.respond(HttpStatusCode.NotFound, "Username not found.") - return "Username not found." + throw UmaException("Username not found.", ErrorCode.USER_NOT_FOUND) } val requestUrl = call.request.fullUrl() val request = try { uma.parseLnurlpRequest(requestUrl) - } catch (e: UnsupportedVersionException) { - call.respond(HttpStatusCode.PreconditionFailed, e.toLnurlpResponseJson()) - return "Unsupported version: ${e.unsupportedVersion}." + } catch (e: UmaException) { + throw e } catch (e: Exception) { - call.respond(HttpStatusCode.BadRequest, "Invalid lnurlp request.") - return "Invalid lnurlp request." + throw UmaException("Failed to parse lnurlp request. ${e.message}", ErrorCode.PARSE_LNURLP_REQUEST_ERROR, e) }.asUmaRequest() ?: run { senderUmaVersion = UMA_VERSION_STRING // Handle non-UMA LNURL requests. @@ -246,39 +239,34 @@ class ReceivingVasp( val pubKeys = try { uma.fetchPublicKeysForVasp(request.vaspDomain) } catch (e: Exception) { - call.respond(HttpStatusCode.BadRequest, "Failed to fetch public keys. ${e.message}") - return "Failed to fetch public keys." + throw UmaException( + "Failed to fetch public keys. ${e.message}", + ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, + e, + ) } - try { - require(uma.verifyUmaLnurlpQuerySignature(request, pubKeys, nonceCache)) { "Invalid lnurlp signature." } - } catch (e: Exception) { - call.respond(HttpStatusCode.BadRequest, "Invalid lnurlp signature. ${e.message}") - return "Invalid lnurlp signature." + if (!uma.verifyUmaLnurlpQuerySignature(request, pubKeys, nonceCache)) { + throw UmaException("Invalid lnurlp signature.", ErrorCode.INVALID_SIGNATURE) } - val response = try { - uma.getLnurlpResponse( - query = request.asLnurlpRequest(), - privateKeyBytes = config.umaSigningPrivKey, - requiresTravelRuleInfo = true, - callback = getLnurlpCallback(call), - encodedMetadata = getEncodedMetadata(), - minSendableSats = 1, - maxSendableSats = 100_000_000, - payerDataOptions = createCounterPartyDataOptions( - "name" to false, - "email" to false, - "compliance" to true, - "identifier" to true, - ), - currencyOptions = getReceivingCurrencies(senderUmaVersion), - receiverKycStatus = KycStatus.VERIFIED, - ) - } catch (e: Exception) { - call.respond(HttpStatusCode.InternalServerError, "Failed to generate lnurlp response.") - return "Failed to generate lnurlp response." - } + val response = uma.getLnurlpResponse( + query = request.asLnurlpRequest(), + privateKeyBytes = config.umaSigningPrivKey, + requiresTravelRuleInfo = true, + callback = getLnurlpCallback(call), + encodedMetadata = getEncodedMetadata(), + minSendableSats = 1, + maxSendableSats = 100_000_000, + payerDataOptions = createCounterPartyDataOptions( + "name" to false, + "email" to false, + "compliance" to true, + "identifier" to true, + ), + currencyOptions = getReceivingCurrencies(senderUmaVersion), + receiverKycStatus = KycStatus.VERIFIED, + ) call.respond(response) @@ -287,27 +275,27 @@ class ReceivingVasp( suspend fun handleLnurlPayreq(call: ApplicationCall): String { val uuid = call.parameters["uuid"] - - if (uuid == null) { - call.respond(HttpStatusCode.BadRequest, "UUID not provided.") - return "UUID not provided." - } + ?: throw UmaException("UUID not provided.", ErrorCode.MISSING_REQUIRED_UMA_PARAMETERS) if (uuid != config.userID) { - call.respond(HttpStatusCode.NotFound, "UUID not found.") - return "UUID not found." + throw UmaException("UUID not found.", ErrorCode.REQUEST_NOT_FOUND) } val paramMap = call.request.queryParameters.toMap() val payreq = try { PayRequest.fromQueryParamMap(paramMap) - } catch (e: IllegalArgumentException) { - call.respond(HttpStatusCode.BadRequest, "Invalid pay request.") - return "Invalid pay request." + } catch (e: UmaException) { + throw e + } catch (e: Exception) { + throw UmaException("Failed to parse pay request. ${e.message}", ErrorCode.PARSE_PAYREQ_REQUEST_ERROR, e) } val lnurlInvoiceCreator = object : UmaInvoiceCreator { - override fun createUmaInvoice(amountMsats: Long, metadata: String, receiverIdentifier: String?,): CompletableFuture { + override fun createUmaInvoice( + amountMsats: Long, + metadata: String, + receiverIdentifier: String?, + ): CompletableFuture { return coroutineScope.future { lightsparkClient.createLnurlInvoice(config.nodeID, amountMsats, metadata).data.encodedPaymentRequest } @@ -317,9 +305,8 @@ class ReceivingVasp( val receivingCurrency = payreq.receivingCurrencyCode()?.let { getReceivingCurrencies(senderUmaVersion) .firstOrNull { it.code == payreq.receivingCurrencyCode() } ?: run { - call.respond(HttpStatusCode.BadRequest, "Unsupported currency.") - return "Unsupported currency." - } + throw UmaException("Unsupported currency.", ErrorCode.INVALID_CURRENCY) + } } val response = uma.getPayReqResponse( @@ -343,48 +330,37 @@ class ReceivingVasp( suspend fun handleUmaPayreq(call: ApplicationCall): String { val uuid = call.parameters["uuid"] - - if (uuid == null) { - call.respond(HttpStatusCode.BadRequest, "UUID not provided.") - return "UUID not provided." - } + ?: throw UmaException("UUID not provided.", ErrorCode.MISSING_REQUIRED_UMA_PARAMETERS) if (uuid != config.userID) { - call.respond(HttpStatusCode.NotFound, "UUID not found.") - return "UUID not found." + throw UmaException("UUID not found.", ErrorCode.REQUEST_NOT_FOUND) } val request = try { uma.parseAsPayRequest(call.receiveText()) } catch (e: Exception) { - call.respond(HttpStatusCode.BadRequest, "Invalid pay request. ${e.message}") - return "Invalid pay request. ${e.message}" + throw UmaException("Failed to parse pay request. ${e.message}", ErrorCode.PARSE_PAYREQ_REQUEST_ERROR, e) } if (!request.isUmaRequest()) { - call.respond(HttpStatusCode.BadRequest, "Invalid UMA pay request to POST endpoint.") - return "Invalid UMA pay request to POST endpoint." + throw UmaException("Invalid UMA pay request.", ErrorCode.MISSING_REQUIRED_UMA_PARAMETERS) } val pubKeys = try { val sendingVaspDomain = uma.getVaspDomainFromUmaAddress(request.payerData!!.identifier()!!) uma.fetchPublicKeysForVasp(sendingVaspDomain) } catch (e: Exception) { - call.respond(HttpStatusCode.BadRequest, "Failed to fetch public keys.") - return "Failed to fetch public keys." + throw UmaException("Failed to fetch public keys.", ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, e) } - try { - require(uma.verifyPayReqSignature(request, pubKeys, nonceCache)) - } catch (e: Exception) { - call.respond(HttpStatusCode.BadRequest, "Invalid payreq signature.") - return "Invalid payreq signature." + + if (!uma.verifyPayReqSignature(request, pubKeys, nonceCache)) { + throw UmaException("Invalid payreq signature.", ErrorCode.INVALID_SIGNATURE) } senderUmaVersion = UMA_VERSION_STRING val receivingCurrency = getReceivingCurrencies(senderUmaVersion) .firstOrNull { it.code == request.receivingCurrencyCode() } ?: run { - call.respond(HttpStatusCode.BadRequest, "Unsupported currency.") - return "Unsupported currency." + throw UmaException("Unsupported currency.", ErrorCode.INVALID_CURRENCY) } val client = LightsparkCoroutinesClient( @@ -396,38 +372,32 @@ class ReceivingVasp( val expirySecs = 60 * 5 val payeeProfile = getPayeeProfile(request.requestedPayeeData(), call) - val response = try { - uma.getPayReqResponse( - query = request, - invoiceCreator = LightsparkClientUmaInvoiceCreator( - client = client, - nodeId = config.nodeID, - expirySecs = expirySecs, - enableUmaAnalytics = true, - signingPrivateKey = config.umaSigningPrivKey, - ), - metadata = getEncodedMetadata(), - receivingCurrencyCode = receivingCurrency.code, - receivingCurrencyDecimals = receivingCurrency.decimals, - conversionRate = receivingCurrency.millisatoshiPerUnit, - receiverFeesMillisats = 0, - // TODO(Jeremy): Actually get the UTXOs from the request. - receiverChannelUtxos = emptyList(), - receiverNodePubKey = getNodePubKey(), - utxoCallback = getUtxoCallback(call, "1234"), - receivingVaspPrivateKey = config.umaSigningPrivKey, - payeeData = createPayeeData( - identifier = payeeProfile.identifier, - name = payeeProfile.name, - email = payeeProfile.email, - ), - senderUmaVersion = senderUmaVersion, - ) - } catch (e: Exception) { - call.application.environment.log.error("Failed to create payreq response.", e) - call.respond(HttpStatusCode.InternalServerError, "Failed to create payreq response.") - return "Failed to create payreq response." - } + val response = uma.getPayReqResponse( + query = request, + invoiceCreator = LightsparkClientUmaInvoiceCreator( + client = client, + nodeId = config.nodeID, + expirySecs = expirySecs, + enableUmaAnalytics = true, + signingPrivateKey = config.umaSigningPrivKey, + ), + metadata = getEncodedMetadata(), + receivingCurrencyCode = receivingCurrency.code, + receivingCurrencyDecimals = receivingCurrency.decimals, + conversionRate = receivingCurrency.millisatoshiPerUnit, + receiverFeesMillisats = 0, + // TODO(Jeremy): Actually get the UTXOs from the request. + receiverChannelUtxos = emptyList(), + receiverNodePubKey = getNodePubKey(), + utxoCallback = getUtxoCallback(call, "1234"), + receivingVaspPrivateKey = config.umaSigningPrivKey, + payeeData = createPayeeData( + identifier = payeeProfile.identifier, + name = payeeProfile.name, + email = payeeProfile.email, + ), + senderUmaVersion = senderUmaVersion, + ) call.respond(response.toJson()) @@ -494,7 +464,7 @@ fun Routing.registerReceivingVaspRoutes(receivingVasp: ReceivingVasp) { call.debugLog(receivingVasp.handleLnurlp(call)) } - get("/api/lnurl/payreq/{uuid}") { + get("/api/uma/payreq/{uuid}") { call.debugLog(receivingVasp.handleLnurlPayreq(call)) } @@ -503,7 +473,7 @@ fun Routing.registerReceivingVaspRoutes(receivingVasp: ReceivingVasp) { } post("/api/uma/create_invoice") { - call.debugLog(receivingVasp.createInvoice(call)); + call.debugLog(receivingVasp.createInvoice(call)) } post("/api/uma/create_and_send_invoice") { diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt index 36ae8448..48ad9124 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt @@ -47,7 +47,9 @@ import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonArray import me.uma.InMemoryNonceCache import me.uma.UMA_VERSION_STRING +import me.uma.UmaException import me.uma.UmaProtocolHelper +import me.uma.generated.ErrorCode import me.uma.protocol.CounterPartyDataOptions import me.uma.protocol.CurrencySerializer import me.uma.protocol.Invoice @@ -86,27 +88,21 @@ class SendingVasp( Invoice.fromBech32(invoiceStr) } } ?: run { - call.respond(HttpStatusCode.BadRequest, "Unable to decode invoice.") - return "Unable to decode invoice." + throw UmaException("Missing the invoice.", ErrorCode.INVALID_INVOICE) } val receiverVaspDomain = umaInvoice.receiverUma.split("@").getOrNull(1) ?: run { - call.respond(HttpStatusCode.FailedDependency, "Failed to parse receiver vasp.") - return "Failed to parse receiver vasp." + throw UmaException("Failed to parse receiver vasp.", ErrorCode.INVALID_INVOICE) } val receiverVaspPubKeys = try { uma.fetchPublicKeysForVasp(receiverVaspDomain) } catch (e: Exception) { - call.application.environment.log.error("Failed to fetch pubkeys", e) - call.respond(HttpStatusCode.FailedDependency, "Failed to fetch public keys.") - return "Failed to fetch public keys." + throw UmaException("Failed to fetch public keys", ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, e) } if (!uma.verifyUmaInvoice(umaInvoice, receiverVaspPubKeys)) { - call.respond(HttpStatusCode.BadRequest, "Unable to decode invoice.") - return "Unable to decode invoice." + throw UmaException("Invalid invoice signature.", ErrorCode.INVALID_SIGNATURE) } if (umaInvoice.expiration < Clock.System.now().epochSeconds) { - call.respond(HttpStatusCode.BadRequest, "Invoice ${umaInvoice.invoiceUUID} has expired.") - return "Invoice ${umaInvoice.invoiceUUID} has expired." + throw UmaException("Invoice ${umaInvoice.invoiceUUID} has expired.", ErrorCode.INVOICE_EXPIRED) } val payer = getPayerProfile(umaInvoice.requiredPayerData ?: emptyMap(), call) @@ -116,8 +112,7 @@ class SendingVasp( it.code == umaInvoice.receivingCurrency.code } if (!currencyValid) { - call.respond(HttpStatusCode.BadRequest, "Receiving currency code not supported.") - return "Receiving currency code not supported." + throw UmaException("Receiving currency code not supported.", ErrorCode.INVALID_CURRENCY) } val trInfo = "Here is some fake travel rule info. It's up to you to actually implement this if needed." @@ -138,7 +133,6 @@ class SendingVasp( payerName = payer.name, payerEmail = payer.email, comment = call.request.queryParameters["comment"], - receiverUmaVersion = umaInvoice.umaVersion, ) val response = try { @@ -147,46 +141,33 @@ class SendingVasp( setBody(payReq.toJson()) } } catch (e: Exception) { - call.respond(HttpStatusCode.FailedDependency, "Unable to connect to ${umaInvoice.callback}") - return "Unable to connect to ${umaInvoice.callback}" + throw UmaException("Unable to connect to ${umaInvoice.callback}", ErrorCode.PAYREQ_REQUEST_FAILED, e) } if (response.status != HttpStatusCode.OK) { - call.respond( - HttpStatusCode.InternalServerError, - "Payreq to receiving vasp failed: ${response.status}" - ) - return "Payreq to receiving vasp failed: ${response.status}" + throw UmaException("Payreq to receiving vasp failed: ${response.status}", ErrorCode.PAYREQ_REQUEST_FAILED) } val payReqResponse = try { uma.parseAsPayReqResponse(response.body()) } catch (e: Exception) { - call.respond(HttpStatusCode.InternalServerError, "Failed to parse payreq response.") - return "Failed to parse payreq response." + throw UmaException("Failed to parse payreq response", ErrorCode.PARSE_PAYREQ_RESPONSE_ERROR, e) } if (!payReqResponse.isUmaResponse()) { - call.application.environment.log.error("Got a non-UMA response: ${payReqResponse.toJson()}") - call.respond( - HttpStatusCode.FailedDependency, - "Received non-UMA response from receiving vasp for an UMA request" + throw UmaException( + "Got a non-UMA response: ${payReqResponse.toJson()}", + ErrorCode.MISSING_REQUIRED_UMA_PARAMETERS, ) - return "Received non-UMA response from receiving vasp." } - try { - uma.verifyPayReqResponseSignature(payReqResponse, receiverVaspPubKeys, payer.identifier, nonceCache) - } catch (e: Exception) { - call.respond(HttpStatusCode.BadRequest, "Failed to verify lnurlp response signature.") - return "Failed to verify lnurlp response signature." + if (!uma.verifyPayReqResponseSignature(payReqResponse, receiverVaspPubKeys, payer.identifier, nonceCache)) { + throw UmaException("Invalid payreq response signature.", ErrorCode.INVALID_SIGNATURE) } val invoice = try { lightsparkClient.decodeInvoice(payReqResponse.encodedInvoice) } catch (e: Exception) { - call.application.environment.log.error("Failed to decode invoice", e) - call.respond(HttpStatusCode.InternalServerError, "Failed to decode invoice.") - return "Failed to decode invoice." + throw UmaException("Failed to decode invoice", ErrorCode.INVALID_INVOICE, e) } val newCallbackId = requestDataCache.savePayReqData( @@ -216,23 +197,18 @@ class SendingVasp( suspend fun requestInvoicePayment(call: ApplicationCall): String { val umaInvoice = call.parameters["invoice"]?.let(Invoice::fromBech32) ?: run { - call.respond(HttpStatusCode.BadRequest, "Unable to decode invoice.") - return "Unable to decode invoice." + throw UmaException("Unable to decode invoice.", ErrorCode.INVALID_INVOICE) } val receiverVaspDomain = umaInvoice.receiverUma.split("@").getOrNull(1) ?: run { - call.respond(HttpStatusCode.FailedDependency, "Failed to parse receiver vasp.") - return "Failed to parse receiver vasp." + throw UmaException("Failed to parse receiver vasp.", ErrorCode.INVALID_INVOICE) } val receiverVaspPubKeys = try { uma.fetchPublicKeysForVasp(receiverVaspDomain) } catch (e: Exception) { - call.application.environment.log.error("Failed to fetch pubkeys", e) - call.respond(HttpStatusCode.FailedDependency, "Failed to fetch public keys.") - return "Failed to fetch public keys." + throw UmaException("Failed to fetch public keys", ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, e) } if (!uma.verifyUmaInvoice(umaInvoice, receiverVaspPubKeys)) { - call.respond(HttpStatusCode.BadRequest, "Invalid invoice signature.") - return "Unable to decode invoice." + throw UmaException("Invalid invoice signature.", ErrorCode.INVALID_SIGNATURE) } requestDataCache.saveUmaInvoice(umaInvoice.invoiceUUID, umaInvoice) return "OK" @@ -241,14 +217,12 @@ class SendingVasp( suspend fun handleClientUmaLookup(call: ApplicationCall): String { val receiverAddress = call.parameters["receiver"] if (receiverAddress == null) { - call.respond(HttpStatusCode.BadRequest, "Receiver not provided.") - return "Receiver not provided." + throw UmaException("Receiver not provided.", ErrorCode.INVALID_INPUT) } val addressParts = receiverAddress.split("@") if (addressParts.size != 2) { - call.respond(HttpStatusCode.BadRequest, "Invalid receiver address.") - return "Invalid receiver address." + throw UmaException("Invalid receiver address.", ErrorCode.INVALID_INPUT) } val receiverId = addressParts[0] val receiverVasp = addressParts[1] @@ -268,8 +242,7 @@ class SendingVasp( var response = try { httpClient.get(lnurlpRequest) } catch (e: Exception) { - call.respond(HttpStatusCode.FailedDependency, "Failed to fetch lnurlp response.") - return "Failed to fetch lnurlp response." + throw UmaException("Failed to fetch lnurlp response.", ErrorCode.LNURLP_REQUEST_FAILED, e) } if (response.status == HttpStatusCode.PreconditionFailed) { @@ -279,12 +252,16 @@ class SendingVasp( it.jsonPrimitive.int } ?: emptyList() if (supportedMajorVersions.isEmpty()) { - call.respond(HttpStatusCode.FailedDependency, "Failed to fetch lnurlp response.") - return "Failed to fetch lnurlp response." + throw UmaException( + "Failed to parse supported major versions from lnurlp response.", + ErrorCode.NO_COMPATIBLE_UMA_VERSION, + ) } val newSupportedVersion = selectHighestSupportedVersion(supportedMajorVersions) ?: run { - call.respond(HttpStatusCode.FailedDependency, "No matching UMA version compatible with receiving VASP.") - return "No matching UMA version compatible with receiving VASP." + throw UmaException( + "No matching UMA version compatible with receiving VASP.", + ErrorCode.NO_COMPATIBLE_UMA_VERSION, + ) } val retryLnurlpRequest = uma.getSignedLnurlpRequestUrl( @@ -297,22 +274,21 @@ class SendingVasp( response = try { httpClient.get(retryLnurlpRequest) } catch (e: Exception) { - call.respond(HttpStatusCode.FailedDependency, "Failed to fetch lnurlp response.") - return "Failed to fetch lnurlp response." + throw UmaException("Failed to fetch lnurlp response.", ErrorCode.LNURLP_REQUEST_FAILED, e) } } if (response.status != HttpStatusCode.OK) { - call.respond(HttpStatusCode.FailedDependency, "Failed to fetch lnurlp response. Status: ${response.status}") - return "Failed to fetch lnurlp response." + throw UmaException( + "Failed to fetch lnurlp response. Status: ${response.status}", + ErrorCode.LNURLP_REQUEST_FAILED, + ) } val lnurlpResponse = try { uma.parseAsLnurlpResponse(response.body()) } catch (e: Exception) { - call.application.environment.log.error("Failed to parse lnurlp response\n${response.bodyAsText()}", e) - call.respond(HttpStatusCode.FailedDependency, "Failed to parse lnurlp response.") - return "Failed to parse lnurlp response." + throw UmaException("Failed to parse lnurlp response", ErrorCode.PARSE_LNURLP_RESPONSE_ERROR, e) } val umaLnurlpResponse = lnurlpResponse.asUmaResponse() @@ -321,16 +297,11 @@ class SendingVasp( val vasp2PubKeys = try { uma.fetchPublicKeysForVasp(receiverVasp) } catch (e: Exception) { - call.application.environment.log.error("Failed to fetch pubkeys", e) - call.respond(HttpStatusCode.FailedDependency, "Failed to fetch public keys.") - return "Failed to fetch public keys." + throw UmaException("Failed to fetch pubkeys", ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, e) } - try { - uma.verifyLnurlpResponseSignature(umaLnurlpResponse, vasp2PubKeys, nonceCache) - } catch (e: Exception) { - call.respond(HttpStatusCode.BadRequest, "Failed to verify lnurlp response signature.") - return "Failed to verify lnurlp response signature." + if (!uma.verifyLnurlpResponseSignature(umaLnurlpResponse, vasp2PubKeys, nonceCache)) { + throw UmaException("Invalid lnurlp response signature.", ErrorCode.INVALID_SIGNATURE) } receiverUmaVersion = umaLnurlpResponse.umaVersion @@ -342,7 +313,9 @@ class SendingVasp( call.respond( buildJsonObject { putJsonArray("receiverCurrencies") { - addAll(receiverCurrencies.map { Json.encodeToJsonElement(CurrencySerializer, it) }) + for (currency in receiverCurrencies) { + add(Json.encodeToJsonElement(CurrencySerializer, currency)) + } } put("minSendSats", lnurlpResponse.minSendable) put("maxSendSats", lnurlpResponse.maxSendable) @@ -357,18 +330,15 @@ class SendingVasp( suspend fun handleClientUmaPayReq(call: ApplicationCall): String { val callbackUuid = call.parameters["callbackUuid"] ?: run { - call.respond(HttpStatusCode.BadRequest, "Callback UUID not provided.") - return "Callback UUID not provided." + throw UmaException("Callback UUID not provided.", ErrorCode.INVALID_INPUT) } val initialRequestData = requestDataCache.getLnurlpResponseData(callbackUuid) ?: run { - call.respond(HttpStatusCode.BadRequest, "Callback UUID not found.") - return "Callback UUID not found." + throw UmaException("Callback UUID not found.", ErrorCode.FORBIDDEN) } val amount = call.request.queryParameters["amount"]?.toLongOrNull() if (amount == null || amount <= 0) { - call.respond(HttpStatusCode.BadRequest, "Amount invalid or not provided.") - return "Amount invalid or not provided." + throw UmaException("Amount invalid or not found.", ErrorCode.INVALID_INPUT) } val currencyCode = call.request.queryParameters["receivingCurrencyCode"] @@ -380,8 +350,7 @@ class SendingVasp( ?: listOf(getSatsCurrency(UMA_VERSION_STRING)) ).any { it.code == currencyCode } if (!currencyValid) { - call.respond(HttpStatusCode.BadRequest, "Receiving currency code not supported.") - return "Receiving currency code not supported." + throw UmaException("Receiving currency code not supported.", ErrorCode.INVALID_CURRENCY) } val umaLnurlpResponse = initialRequestData.lnurlpResponse.asUmaResponse() val isUma = umaLnurlpResponse != null @@ -392,9 +361,7 @@ class SendingVasp( try { uma.fetchPublicKeysForVasp(initialRequestData.receivingVaspDomain) } catch (e: Exception) { - call.application.environment.log.error("Failed to fetch pubkeys", e) - call.respond(HttpStatusCode.FailedDependency, "Failed to fetch public keys.") - return "Failed to fetch public keys." + throw UmaException("Failed to fetch pubkeys", ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, e) } } else { null @@ -404,81 +371,79 @@ class SendingVasp( val trInfo = "Here is some fake travel rule info. It's up to you to actually implement this if needed." val payerUtxos = emptyList() - val payReq = try { - if (isUma) { - uma.getPayRequest( - receiverEncryptionPubKey = receiverVaspPubKeys!!.getEncryptionPublicKey(), - sendingVaspPrivateKey = config.umaSigningPrivKey, - receivingCurrencyCode = currencyCode, - isAmountInReceivingCurrency = !isAmountInMsats, - amount = amount, - payerIdentifier = payer.identifier, - payerKycStatus = KycStatus.VERIFIED, - payerNodePubKey = getNodePubKey(), - utxoCallback = getUtxoCallback(call, "1234abc"), - travelRuleInfo = trInfo, - payerUtxos = payerUtxos, - payerName = payer.name, - payerEmail = payer.email, - comment = call.request.queryParameters["comment"], - receiverUmaVersion = receiverUmaVersion, - ) - } else { - val comment = call.request.queryParameters["comment"] - val payerData = createPayerData(identifier = payer.identifier, name = payer.name, email = payer.email) - val params = mapOf( - "amount" to if (isAmountInMsats) listOf(amount.toString()) else listOf("$amount.$currencyCode"), - "convert" to listOf(currencyCode), - "payerData" to listOf(serialFormat.encodeToString(payerData)), - "comment" to (comment?.let { listOf(it) } ?: emptyList()), - ) - PayRequest.fromQueryParamMap(params) - } - } catch (e: Exception) { - call.application.environment.log.error("Failed to generate payreq", e) - call.respond(HttpStatusCode.InternalServerError, "Failed to generate payreq.") - return "Failed to generate payreq." + val payReq = if (isUma) { + uma.getPayRequest( + receiverEncryptionPubKey = receiverVaspPubKeys!!.getEncryptionPublicKey(), + sendingVaspPrivateKey = config.umaSigningPrivKey, + receivingCurrencyCode = currencyCode, + isAmountInReceivingCurrency = !isAmountInMsats, + amount = amount, + payerIdentifier = payer.identifier, + payerKycStatus = KycStatus.VERIFIED, + payerNodePubKey = getNodePubKey(), + utxoCallback = getUtxoCallback(call, "1234abc"), + travelRuleInfo = trInfo, + payerUtxos = payerUtxos, + payerName = payer.name, + payerEmail = payer.email, + comment = call.request.queryParameters["comment"], + receiverUmaVersion = receiverUmaVersion, + ) + } else { + val comment = call.request.queryParameters["comment"] + val payerData = createPayerData(identifier = payer.identifier, name = payer.name, email = payer.email) + val params = mapOf( + "amount" to if (isAmountInMsats) listOf(amount.toString()) else listOf("$amount.$currencyCode"), + "convert" to listOf(currencyCode), + "payerData" to listOf(serialFormat.encodeToString(payerData)), + "comment" to (comment?.let { listOf(it) } ?: emptyList()), + ) + PayRequest.fromQueryParamMap(params) } - val response = if (isUma) { - httpClient.post(initialRequestData.lnurlpResponse.callback) { - contentType(ContentType.Application.Json) - setBody(payReq.toJson()) - } - } else { - httpClient.get(initialRequestData.lnurlpResponse.callback) { - contentType(ContentType.Application.Json) - payReq.toQueryParamMap().forEach { (key, values) -> - parameter(key, values) + val response = try { + if (isUma) { + httpClient.post(initialRequestData.lnurlpResponse.callback) { + contentType(ContentType.Application.Json) + setBody(payReq.toJson()) + } + } else { + httpClient.get(initialRequestData.lnurlpResponse.callback) { + contentType(ContentType.Application.Json) + payReq.toQueryParamMap().forEach { (key, values) -> + parameter(key, values) + } } } + } catch (e: Exception) { + throw UmaException("Failed to fetch payreq response", ErrorCode.PAYREQ_REQUEST_FAILED, e) } if (response.status != HttpStatusCode.OK) { - call.respond(HttpStatusCode.InternalServerError, "Payreq to vasp2 failed: ${response.status}") - return "Payreq to vasp2 failed: ${response.status}" + throw UmaException("Payreq to vasp2 failed: ${response.status}", ErrorCode.PAYREQ_REQUEST_FAILED) } val payReqResponse = try { uma.parseAsPayReqResponse(response.body()) } catch (e: Exception) { - call.respond(HttpStatusCode.InternalServerError, "Failed to parse payreq response.") - return "Failed to parse payreq response." + throw UmaException("Failed to parse payreq response", ErrorCode.PARSE_PAYREQ_RESPONSE_ERROR, e) } if (isUma && !payReqResponse.isUmaResponse()) { - call.application.environment.log.error("Got a non-UMA response: ${payReqResponse.toJson()}") - call.respond(HttpStatusCode.FailedDependency, "Received non-UMA response from vasp2 for an UMA request") - return "Received non-UMA response from vasp2." + throw UmaException( + "Got a non-UMA response: ${payReqResponse.toJson()}", + ErrorCode.MISSING_REQUIRED_UMA_PARAMETERS, + ) } - if (isUma) { - try { - uma.verifyPayReqResponseSignature(payReqResponse, receiverVaspPubKeys!!, payer.identifier, nonceCache) - } catch (e: Exception) { - call.respond(HttpStatusCode.BadRequest, "Failed to verify lnurlp response signature.") - return "Failed to verify lnurlp response signature." - } + if (isUma && !uma.verifyPayReqResponseSignature( + payReqResponse, + receiverVaspPubKeys!!, + payer.identifier, + nonceCache, + ) + ) { + throw UmaException("Invalid payreq response signature.", ErrorCode.INVALID_SIGNATURE) } // TODO(Yun): Pre-screen the UTXOs from payreqResponse.compliance.utxos @@ -486,9 +451,7 @@ class SendingVasp( val invoice = try { lightsparkClient.decodeInvoice(payReqResponse.encodedInvoice) } catch (e: Exception) { - call.application.environment.log.error("Failed to decode invoice", e) - call.respond(HttpStatusCode.InternalServerError, "Failed to decode invoice.") - return "Failed to decode invoice." + throw UmaException("Failed to decode invoice", ErrorCode.INVALID_INVOICE, e) } val newCallbackId = requestDataCache.savePayReqData( @@ -536,22 +499,18 @@ class SendingVasp( suspend fun handleClientSendPayment(call: ApplicationCall): String { val callbackUuid = call.parameters["callbackUuid"] ?: run { - call.respond(HttpStatusCode.BadRequest, "Callback UUID not provided.") - return "Callback UUID not provided." + throw UmaException("Callback UUID not provided.", ErrorCode.INVALID_INPUT) } val payReqData = requestDataCache.getPayReqData(callbackUuid) ?: run { - call.respond(HttpStatusCode.BadRequest, "Callback UUID not found.") - return "Callback UUID not found." + throw UmaException("Callback UUID not found.", ErrorCode.FORBIDDEN) } if (payReqData.invoiceData.expiresAt < Clock.System.now()) { - call.respond(HttpStatusCode.BadRequest, "Invoice expired.") - return "Invoice expired." + throw UmaException("Invoice expired.", ErrorCode.INVOICE_EXPIRED) } if (payReqData.invoiceData.amount.originalValue <= 0) { - call.respond(HttpStatusCode.BadRequest, "Invoice amount invalid. Uma requires positive amounts.") - return "Invoice amount invalid." + throw UmaException("Invoice amount invalid.", ErrorCode.INVALID_INVOICE) } val payment = try { @@ -565,9 +524,7 @@ class SendingVasp( ) waitForPaymentCompletion(pendingPayment) } catch (e: Exception) { - call.application.environment.log.error("Failed to pay invoice", e) - call.respond(HttpStatusCode.InternalServerError, "Failed to pay invoice.") - return "Failed to pay invoice." + throw UmaException("Failed to pay invoice", ErrorCode.INTERNAL_ERROR, e) } sendPostTransactionCallback(payment, payReqData, call) @@ -593,7 +550,7 @@ class SendingVasp( val postTransactionCallback = uma.getPostTransactionCallback( utxos = utxos, vaspDomain = getSendingVaspDomain(call), - signingPrivateKey = config.umaSigningPrivKey + signingPrivateKey = config.umaSigningPrivKey, ) val postTxHookResponse = try { httpClient.post(payReqData.utxoCallback) { @@ -601,11 +558,10 @@ class SendingVasp( setBody(postTransactionCallback.toJson()) } } catch (e: Exception) { - call.errorLog("Failed to post tx hook", e) - null + throw UmaException("Failed to post tx hook", ErrorCode.INTERNAL_ERROR, e) } - if (postTxHookResponse?.status != HttpStatusCode.OK) { - call.errorLog("Failed to post tx hook: ${postTxHookResponse?.status}") + if (postTxHookResponse.status != HttpStatusCode.OK) { + throw UmaException("Failed to post tx hook: ${postTxHookResponse.status}", ErrorCode.INTERNAL_ERROR) } } @@ -614,7 +570,7 @@ class SendingVasp( private fun getNonUmaLnurlRequestUrl(receiverAddress: String): String { val receiverAddressParts = receiverAddress.split("@") if (receiverAddressParts.size != 2) { - throw IllegalArgumentException("Invalid receiverAddress: $receiverAddress") + throw UmaException("Invalid receiverAddress: $receiverAddress", ErrorCode.INVALID_INPUT) } val scheme = if (isDomainLocalhost(receiverAddressParts[1])) URLProtocol.HTTP else URLProtocol.HTTPS val url = URLBuilder( @@ -632,10 +588,10 @@ class SendingVasp( while (payment.status == TransactionStatus.PENDING && attemptsLeft-- > 0) { delay(250) payment = OutgoingPayment.getOutgoingPaymentQuery(payment.id).execute(lightsparkClient) - ?: throw Exception("Payment not found.") + ?: throw UmaException("Payment not found.", ErrorCode.INTERNAL_ERROR) } if (payment.status == TransactionStatus.PENDING) { - throw Exception("Payment timed out.") + throw UmaException("Payment timed out.", ErrorCode.INTERNAL_ERROR) } return payment } @@ -645,19 +601,31 @@ class SendingVasp( when (val node = lightsparkClient.executeQuery(getLightsparkNodeQuery(nodeId))) { is LightsparkNodeWithOSK -> { if (config.oskNodePassword.isNullOrEmpty()) { - throw IllegalArgumentException("Node is an OSK, but no signing key password was provided in the " + - "config. Set the LIGHTSPARK_UMA_OSK_NODE_SIGNING_KEY_PASSWORD environment variable") + throw UmaException( + "Node is an OSK, but no signing key password was provided in the " + + "config. Set the LIGHTSPARK_UMA_OSK_NODE_SIGNING_KEY_PASSWORD environment variable", + ErrorCode.INTERNAL_ERROR, + ) } - lightsparkClient.loadNodeSigningKey(nodeId, PasswordRecoverySigningKeyLoader(nodeId, config.oskNodePassword)) + lightsparkClient.loadNodeSigningKey( + nodeId, + PasswordRecoverySigningKeyLoader(nodeId, config.oskNodePassword), + ) } is LightsparkNodeWithRemoteSigning -> { val remoteSigningKey = config.remoteSigningNodeKey - ?: throw IllegalArgumentException("Node is a remote signing node, but no master seed was provided in " + - "the config. Set the LIGHTSPARK_UMA_REMOTE_SIGNING_NODE_MASTER_SEED environment variable") - lightsparkClient.loadNodeSigningKey(nodeId, Secp256k1SigningKeyLoader(remoteSigningKey, node.bitcoinNetwork)) + ?: throw UmaException( + "Node is a remote signing node, but no master seed was provided in " + + "the config. Set the LIGHTSPARK_UMA_REMOTE_SIGNING_NODE_MASTER_SEED environment variable", + ErrorCode.INTERNAL_ERROR, + ) + lightsparkClient.loadNodeSigningKey( + nodeId, + Secp256k1SigningKeyLoader(remoteSigningKey, node.bitcoinNetwork), + ) } else -> { - throw IllegalArgumentException("Invalid node type.") + throw UmaException("Invalid node type.", ErrorCode.INTERNAL_ERROR) } } } diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVaspRequestCache.kt b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVaspRequestCache.kt index 90a6ffe9..cb74d8f8 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVaspRequestCache.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVaspRequestCache.kt @@ -1,8 +1,8 @@ package com.lightspark import com.lightspark.sdk.model.InvoiceData -import me.uma.protocol.Invoice import java.util.UUID +import me.uma.protocol.Invoice import me.uma.protocol.LnurlpResponse /** diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt b/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt index e03e98ad..fb04b3ea 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt @@ -1,39 +1,62 @@ package com.lightspark.plugins -import com.lightspark.UmaConfig -import com.lightspark.SendingVasp import com.lightspark.ReceivingVasp +import com.lightspark.SendingVasp +import com.lightspark.UmaConfig import com.lightspark.debugLog import com.lightspark.handlePubKeyRequest import com.lightspark.isDomainLocalhost import com.lightspark.originWithPort -import com.lightspark.registerSendingVaspRoutes import com.lightspark.registerReceivingVaspRoutes +import com.lightspark.registerSendingVaspRoutes import com.lightspark.sdk.ClientConfig import com.lightspark.sdk.LightsparkCoroutinesClient import com.lightspark.sdk.auth.AccountApiTokenAuthProvider import io.ktor.http.HttpStatusCode import io.ktor.server.application.Application import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.plugins.statuspages.StatusPages import io.ktor.server.request.receiveText import io.ktor.server.response.respond import io.ktor.server.routing.get import io.ktor.server.routing.post import io.ktor.server.routing.routing import kotlinx.datetime.Clock -import me.uma.InMemoryNonceCache -import me.uma.InMemoryPublicKeyCache -import me.uma.UmaProtocolHelper import kotlinx.serialization.json.add import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put +import me.uma.InMemoryNonceCache +import me.uma.InMemoryPublicKeyCache +import me.uma.UmaException +import me.uma.UmaProtocolHelper +import me.uma.generated.ErrorCode fun Application.configureRouting( config: UmaConfig, uma: UmaProtocolHelper = UmaProtocolHelper(InMemoryPublicKeyCache()), lightsparkClient: LightsparkCoroutinesClient? = null, ) { + install(StatusPages) { + exception { call, cause -> + call.debugLog("Responding to exception: ${cause.message}") + when (cause) { + is UmaException -> { + call.respond(HttpStatusCode.fromValue(cause.toHttpStatusCode()), cause.toJSON()) + } + else -> { + val umaException = UmaException( + "Internal server error: ${cause.message}", + ErrorCode.INTERNAL_ERROR, + cause, + ) + call.respond(HttpStatusCode.fromValue(umaException.toHttpStatusCode()), umaException.toJSON()) + } + } + } + } + val client = lightsparkClient ?: LightsparkCoroutinesClient( ClientConfig( serverUrl = config.clientBaseURL ?: "api.lightspark.com", @@ -73,23 +96,27 @@ fun Application.configureRouting( val postTransactionCallback = try { uma.parseAsPostTransactionCallback(call.receiveText()) } catch (e: Exception) { - call.respond(HttpStatusCode.BadRequest, "Invalid utxo callback.") - return@post + throw UmaException("Failed to parse post transaction callback", ErrorCode.PARSE_UTXO_CALLBACK_ERROR, e) } val pubKeys = try { uma.fetchPublicKeysForVasp(postTransactionCallback.vaspDomain) } catch (e: Exception) { - call.respond(HttpStatusCode.BadRequest, "Failed to fetch public keys. ${e.message}") - return@post + throw UmaException("Failed to fetch public keys", ErrorCode.COUNTERPARTY_PUBKEY_FETCH_ERROR, e) } val nonceCache = InMemoryNonceCache(Clock.System.now().epochSeconds) try { - uma.verifyPostTransactionCallbackSignature(postTransactionCallback, pubKeys, nonceCache) + if (!uma.verifyPostTransactionCallbackSignature(postTransactionCallback, pubKeys, nonceCache)) { + throw UmaException("Invalid post transaction callback signature", ErrorCode.INVALID_SIGNATURE) + } } catch (e: Exception) { - call.respond(HttpStatusCode.BadRequest, "Failed to verify post transaction callback signature.") - return@post + if (e is UmaException) throw e + throw UmaException( + "Failed to verify post transaction callback signature", + ErrorCode.INVALID_SIGNATURE, + e, + ) } call.debugLog("Received UTXO callback: $postTransactionCallback") diff --git a/wallet-sdk/build.gradle.kts b/wallet-sdk/build.gradle.kts index db59859f..d986cbb6 100644 --- a/wallet-sdk/build.gradle.kts +++ b/wallet-sdk/build.gradle.kts @@ -1,7 +1,6 @@ import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING import com.mgd.core.gradle.S3Upload -@Suppress("DSL_SCOPE_VIOLATION") plugins { kotlin("multiplatform") alias(libs.plugins.kotlinSerialization) @@ -146,14 +145,14 @@ tasks.register("generateSdkDocs") { } s3 { - bucket = "lsdev.web-dev" + bucket = "lightspark-dev-web" region = "us-west-2" } tasks.register("uploadDocsToS3") { group = "documentation" dependsOn("generateSdkDocs") - bucket = "ldev.web-dev" + bucket = "lightspark-dev-web" keyPrefix = "docs/kotlin" sourceDir = "docs/html" }