From 192326f68d0aea1f6c0eaddb293a0daa6b18e6c6 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Tue, 10 Sep 2024 13:24:50 -0700 Subject: [PATCH 01/31] Add the uma-configuration file to the demo VASP --- .../kotlin/com/lightspark/plugins/Routing.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt b/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt index 0d2e40e1..6d8954ac 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt @@ -5,6 +5,8 @@ import com.lightspark.Vasp1 import com.lightspark.Vasp2 import com.lightspark.debugLog import com.lightspark.handlePubKeyRequest +import com.lightspark.isDomainLocalhost +import com.lightspark.originWithPort import com.lightspark.registerVasp1Routes import com.lightspark.registerVasp2Routes import com.lightspark.sdk.ClientConfig @@ -25,6 +27,11 @@ import kotlinx.serialization.json.JsonObject import me.uma.InMemoryNonceCache import me.uma.InMemoryPublicKeyCache import me.uma.UmaProtocolHelper +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put fun Application.configureRouting( config: UmaConfig, @@ -48,6 +55,24 @@ fun Application.configureRouting( call.debugLog(handlePubKeyRequest(call, config)) } + get("/.well-known/uma-configuration") { + val domain = config.vaspDomain ?: call.originWithPort() + val scheme = if (isDomainLocalhost(domain)) "http" else "https" + call.respond( + HttpStatusCode.OK, + buildJsonObject { + put("uma_request_endpoint", "$scheme://$domain/api/uma/request_pay_invoice") + put( + "uma_major_versions", + buildJsonArray { + add(0) + add(1) + }, + ) + }, + ) + } + post("/api/uma/utxoCallback") { val postTransactionCallback = try { uma.parseAsPostTransactionCallback(call.receiveText()) From a7488b2ea01509642d4a18ce51589a2717374571 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 3 Sep 2024 19:26:32 -0700 Subject: [PATCH 02/31] adding sending / receiving vasp methods for demo --- .../lightspark/{Vasp2.kt => ReceivingVasp.kt} | 30 ++++++++++++++----- .../lightspark/{Vasp1.kt => SendingVasp.kt} | 26 ++++++++++++---- .../kotlin/com/lightspark/plugins/Routing.kt | 20 +++++-------- 3 files changed, 52 insertions(+), 24 deletions(-) rename umaserverdemo/src/main/kotlin/com/lightspark/{Vasp2.kt => ReceivingVasp.kt} (94%) rename umaserverdemo/src/main/kotlin/com/lightspark/{Vasp1.kt => SendingVasp.kt} (97%) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/Vasp2.kt b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt similarity index 94% rename from umaserverdemo/src/main/kotlin/com/lightspark/Vasp2.kt rename to umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt index 61fd37c9..31a57d88 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/Vasp2.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt @@ -38,7 +38,7 @@ import me.uma.protocol.createCounterPartyDataOptions import me.uma.protocol.createPayeeData import me.uma.protocol.identifier -class Vasp2( +class ReceivingVasp( private val config: UmaConfig, private val uma: UmaProtocolHelper, private val lightsparkClient: LightsparkCoroutinesClient, @@ -47,6 +47,14 @@ class Vasp2( private val coroutineScope = CoroutineScope(Dispatchers.IO) private lateinit var senderUmaVersion: String + suspend fun createInvoice(call: ApplicationCall): String { + return "OK" + } + + suspend fun createAndSendInvoice(call: ApplicationCall): String { + return "OK" + } + suspend fun handleLnurlp(call: ApplicationCall): String { val username = call.parameters["username"] @@ -155,7 +163,7 @@ class Vasp2( } val lnurlInvoiceCreator = object : UmaInvoiceCreator { - override fun createUmaInvoice(amountMsats: Long, metadata: String): CompletableFuture { + override fun createUmaInvoice(amountMsats: Long, metadata: String, receiverIdentifier: String?,): CompletableFuture { return coroutineScope.future { lightsparkClient.createLnurlInvoice(config.nodeID, amountMsats, metadata).data.encodedPaymentRequest } @@ -334,17 +342,25 @@ class Vasp2( private fun getReceivingVaspDomain(call: ApplicationCall) = config.vaspDomain ?: call.originWithPort() } -fun Routing.registerVasp2Routes(vasp2: Vasp2) { +fun Routing.registerReceivingVaspRoutes(receivingVasp: ReceivingVasp) { get("/.well-known/lnurlp/{username}") { - call.debugLog(vasp2.handleLnurlp(call)) + call.debugLog(receivingVasp.handleLnurlp(call)) } - get("/api/uma/payreq/{uuid}") { - call.debugLog(vasp2.handleLnurlPayreq(call)) + get("/api/lnurl/payreq/{uuid}") { + call.debugLog(receivingVasp.handleLnurlPayreq(call)) } post("/api/uma/payreq/{uuid}") { - call.debugLog(vasp2.handleUmaPayreq(call)) + call.debugLog(receivingVasp.handleUmaPayreq(call)) + } + + get("/api/uma/create_invoice") { + call.debugLog(receivingVasp.createInvoice(call)); + } + + get("/api/uma/create_and_send_invoice") { + call.debugLog(receivingVasp.createAndSendInvoice(call)) } } diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/Vasp1.kt b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt similarity index 97% rename from umaserverdemo/src/main/kotlin/com/lightspark/Vasp1.kt rename to umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt index 9f9fecb9..789e7877 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/Vasp1.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt @@ -57,7 +57,7 @@ import me.uma.protocol.createPayerData import me.uma.selectHighestSupportedVersion import me.uma.utils.serialFormat -class Vasp1( +class SendingVasp( private val config: UmaConfig, private val uma: UmaProtocolHelper, private val lightsparkClient: LightsparkCoroutinesClient, @@ -75,6 +75,14 @@ class Vasp1( private val nonceCache = InMemoryNonceCache(Clock.System.now().epochSeconds) private lateinit var receiverUmaVersion: String + suspend fun payInvoice(call: ApplicationCall): String { + return "OK" + } + + suspend fun requestAndPayInvoice(call: ApplicationCall): String { + return "OK" + } + suspend fun handleClientUmaLookup(call: ApplicationCall): String { val receiverAddress = call.parameters["receiver"] if (receiverAddress == null) { @@ -500,17 +508,25 @@ class Vasp1( } } -fun Routing.registerVasp1Routes(vasp1: Vasp1) { +fun Routing.registerSendingVaspRoutes(sendingVasp: SendingVasp) { get("/api/umalookup/{receiver}") { - call.debugLog(vasp1.handleClientUmaLookup(call)) + call.debugLog(sendingVasp.handleClientUmaLookup(call)) } get("/api/umapayreq/{callbackUuid}") { - call.debugLog(vasp1.handleClientUmaPayReq(call)) + call.debugLog(sendingVasp.handleClientUmaPayReq(call)) } post("/api/sendpayment/{callbackUuid}") { - call.debugLog(vasp1.handleClientSendPayment(call)) + call.debugLog(sendingVasp.handleClientSendPayment(call)) + } + + post("/api/uma/pay_invoice") { + call.debugLog(sendingVasp.payInvoice(call)) + } + + post("/api/uma/request_and_pay_invoice") { + call.debugLog(sendingVasp.requestAndPayInvoice(call)) } } diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt b/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt index 6d8954ac..2d2b3eb3 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt @@ -1,33 +1,29 @@ package com.lightspark.plugins import com.lightspark.UmaConfig -import com.lightspark.Vasp1 -import com.lightspark.Vasp2 +import com.lightspark.SendingVasp +import com.lightspark.ReceivingVasp import com.lightspark.debugLog import com.lightspark.handlePubKeyRequest import com.lightspark.isDomainLocalhost import com.lightspark.originWithPort -import com.lightspark.registerVasp1Routes -import com.lightspark.registerVasp2Routes +import com.lightspark.registerSendingVaspRoutes +import com.lightspark.registerReceivingVaspRoutes 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.request.ContentTransformationException -import io.ktor.server.request.receive 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 kotlinx.serialization.json.JsonObject import me.uma.InMemoryNonceCache import me.uma.InMemoryPublicKeyCache import me.uma.UmaProtocolHelper -import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.add import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject @@ -44,12 +40,12 @@ fun Application.configureRouting( authProvider = AccountApiTokenAuthProvider(config.apiClientID, config.apiClientSecret), ), ) - val vasp1 = Vasp1(config, uma, client) - val vasp2 = Vasp2(config, uma, client) + val sendingVasp = SendingVasp(config, uma, client) + val receivingVasp = ReceivingVasp(config, uma, client) routing { - registerVasp1Routes(vasp1) - registerVasp2Routes(vasp2) + registerSendingVaspRoutes(sendingVasp) + registerReceivingVaspRoutes(receivingVasp) get("/.well-known/lnurlpubkey") { call.debugLog(handlePubKeyRequest(call, config)) From 9817cd7f3e9b5e261bdad74acaaea5fd9bad32a4 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 9 Sep 2024 15:31:02 -0700 Subject: [PATCH 03/31] change path for requesting invoice --- umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt index 789e7877..060a3a33 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt @@ -79,7 +79,7 @@ class SendingVasp( return "OK" } - suspend fun requestAndPayInvoice(call: ApplicationCall): String { + suspend fun requestInvoicePayment(call: ApplicationCall): String { return "OK" } @@ -525,8 +525,8 @@ fun Routing.registerSendingVaspRoutes(sendingVasp: SendingVasp) { call.debugLog(sendingVasp.payInvoice(call)) } - post("/api/uma/request_and_pay_invoice") { - call.debugLog(sendingVasp.requestAndPayInvoice(call)) + post("/api/uma/request_invoice_payment") { + call.debugLog(sendingVasp.requestInvoicePayment(call)) } } From adbbca98bb9ee6ded8a08f3e6c0a686aea716c54 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 9 Sep 2024 11:25:23 -0700 Subject: [PATCH 04/31] adding basic receiver vasp methods --- gradle/libs.versions.toml | 2 +- .../kotlin/com/lightspark/ReceivingVasp.kt | 137 +++++++++++++++++- 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1f971d5b..8bab457e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ ktlint = "11.3.1" ktor = "2.3.7" lightsparkCore = "0.6.0" lightsparkCrypto = "0.6.0" -uma = "1.2.1" +uma = "1.3.0" mavenPublish = "0.25.2" mockitoCore = "5.5.0" taskTree = "2.1.1" diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt index 31a57d88..8b4ff239 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt @@ -4,7 +4,17 @@ import com.lightspark.sdk.ClientConfig import com.lightspark.sdk.LightsparkCoroutinesClient import com.lightspark.sdk.auth.AccountApiTokenAuthProvider import com.lightspark.sdk.model.Node +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.parameter +import io.ktor.client.request.post +import io.ktor.client.request.request +import io.ktor.client.request.setBody +import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.serialization.kotlinx.json.json import io.ktor.server.application.ApplicationCall import io.ktor.server.application.call import io.ktor.server.plugins.origin @@ -31,12 +41,17 @@ import me.uma.UmaInvoiceCreator import me.uma.UmaProtocolHelper import me.uma.UnsupportedVersionException import me.uma.protocol.CounterPartyDataOptions +import me.uma.protocol.InvoiceCurrency import me.uma.protocol.KycStatus import me.uma.protocol.LnurlpResponse 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 + class ReceivingVasp( private val config: UmaConfig, @@ -46,12 +61,130 @@ class ReceivingVasp( private val nonceCache = InMemoryNonceCache(Clock.System.now().epochSeconds) private val coroutineScope = CoroutineScope(Dispatchers.IO) private lateinit var senderUmaVersion: String + private val httpClient = HttpClient { + install(ContentNegotiation) { + json( + Json { + isLenient = true + }, + ) + } + } suspend fun createInvoice(call: ApplicationCall): String { + // get currencyCode, Amount, verify user + val amount = try { + call.parameters["amount"]?.toLong() ?: run { + call.respond(HttpStatusCode.BadRequest, "Amount not provided.") + return "Amount not provided." + } + } catch (e: NumberFormatException) { + call.respond(HttpStatusCode.BadRequest, "Amount not parsable as number.") + return "Amount not parsable as number." + } + + val currency = call.parameters["currencyCode"]?.let { currencyCode -> + // check if we support this currency code. + getReceivingCurrencies(UMA_VERSION_STRING).firstOrNull { + it.code == currencyCode + } ?: run { + call.respond(HttpStatusCode.BadRequest, "Unsupported CurrencyCode $currencyCode.") + return "Unsupported CurrencyCode $currencyCode." + } + } ?: run { + call.respond(HttpStatusCode.BadRequest, "CurrencyCode not provided.") + return "CurrencyCode not provided." + } + + val expiresIn2Days = Clock.System.now().plus(2, DateTimeUnit.HOUR*24) //? + + val receiverUma = "${config.username}:${getReceivingVaspDomain(call)}" + println(config.vaspDomain) + + val response = uma.getInvoice( + receiverUma = receiverUma, + invoiceUUID = UUID.randomUUID().toString(), + amount = amount, + receivingCurrency = InvoiceCurrency( + currency.code, currency.name, currency.symbol, currency.decimals + ), + expiration = expiresIn2Days.toEpochMilliseconds(), + isSubjectToTravelRule = true, + requiredPayerData = createCounterPartyDataOptions( + "name" to false, + "email" to false, + "compliance" to true, + "identifier" to true, + ), + callback = getLnurlpCallback(call), // structured the same, going to /api/uma/payreq/{user_id} + privateSigningKey = config.umaSigningPrivKey + ) + call.respond(response.toBech32()) return "OK" } suspend fun createAndSendInvoice(call: ApplicationCall): String { + val senderUma = call.parameters["senderUma"] ?: run { + call.respond(HttpStatusCode.BadRequest, "SenderUma not provided.") + return "SenderUma not provided." + } + val amount = try { + call.parameters["amount"]?.toLong() ?: run { + call.respond(HttpStatusCode.BadRequest, "Amount not provided.") + return "Amount not provided." + } + } catch (e: NumberFormatException) { + call.respond(HttpStatusCode.BadRequest, "Amount not parsable as number.") + return "Amount not parsable as number." + } + + val currency = call.parameters["currencyCode"]?.let { currencyCode -> + // check if we support this currency code. + getReceivingCurrencies(UMA_VERSION_STRING).firstOrNull { + it.code == currencyCode + } ?: run { + call.respond(HttpStatusCode.BadRequest, "Unsupported CurrencyCode $currencyCode.") + return "Unsupported CurrencyCode $currencyCode." + } + } ?: run { + call.respond(HttpStatusCode.BadRequest, "CurrencyCode not provided.") + return "CurrencyCode not provided." + } + + val expiresIn2Days = Clock.System.now().plus(2, DateTimeUnit.HOUR*24) //? + + val receiverUma = "${config.username}:${getReceivingVaspDomain(call)}" + println(config.vaspDomain) + + val invoice = uma.getInvoice( + receiverUma = receiverUma, + invoiceUUID = UUID.randomUUID().toString(), + amount = amount, + receivingCurrency = InvoiceCurrency( + currency.code, currency.name, currency.symbol, currency.decimals + ), + expiration = expiresIn2Days.toEpochMilliseconds(), + isSubjectToTravelRule = true, + requiredPayerData = createCounterPartyDataOptions( + "name" to false, + "email" to false, + "compliance" to true, + "identifier" to true, + ), + callback = getLnurlpCallback(call), // structured the same, going to /api/uma/payreq/{user_id} + privateSigningKey = config.umaSigningPrivKey, + senderUma = senderUma + ) + val encodedInvoice = invoice.toBech32() + val response = httpClient.post("/api/uma/payreq/${config.userID}") { + contentType(ContentType.Application.Json) + setBody(parameter("invoice", encodedInvoice)) + } + if (response.status != HttpStatusCode.OK) { + call.respond(HttpStatusCode.InternalServerError, "Payreq to Sending Vasp: ${response.status}") + return "Payreq to vasp2 failed: ${response.status}" + } + call.respond(response.body()) return "OK" } @@ -355,11 +488,11 @@ fun Routing.registerReceivingVaspRoutes(receivingVasp: ReceivingVasp) { call.debugLog(receivingVasp.handleUmaPayreq(call)) } - get("/api/uma/create_invoice") { + post("/api/uma/create_invoice") { call.debugLog(receivingVasp.createInvoice(call)); } - get("/api/uma/create_and_send_invoice") { + post("/api/uma/create_and_send_invoice") { call.debugLog(receivingVasp.createAndSendInvoice(call)) } } From e6e92e8c4fb0d9889e0153e5a85695bd7239a207 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 9 Sep 2024 15:46:45 -0700 Subject: [PATCH 05/31] remove duplicate code for create invoices --- .../kotlin/com/lightspark/ReceivingVasp.kt | 103 ++++++------------ 1 file changed, 36 insertions(+), 67 deletions(-) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt index 8b4ff239..fa72ad87 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt @@ -9,7 +9,6 @@ import io.ktor.client.call.body import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.parameter import io.ktor.client.request.post -import io.ktor.client.request.request import io.ktor.client.request.setBody import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode @@ -72,54 +71,13 @@ class ReceivingVasp( } suspend fun createInvoice(call: ApplicationCall): String { - // get currencyCode, Amount, verify user - val amount = try { - call.parameters["amount"]?.toLong() ?: run { - call.respond(HttpStatusCode.BadRequest, "Amount not provided.") - return "Amount not provided." - } - } catch (e: NumberFormatException) { - call.respond(HttpStatusCode.BadRequest, "Amount not parsable as number.") - return "Amount not parsable as number." - } - - val currency = call.parameters["currencyCode"]?.let { currencyCode -> - // check if we support this currency code. - getReceivingCurrencies(UMA_VERSION_STRING).firstOrNull { - it.code == currencyCode - } ?: run { - call.respond(HttpStatusCode.BadRequest, "Unsupported CurrencyCode $currencyCode.") - return "Unsupported CurrencyCode $currencyCode." - } - } ?: run { - call.respond(HttpStatusCode.BadRequest, "CurrencyCode not provided.") - return "CurrencyCode not provided." + val (status, data) = createUmaInvoice(call) + if (status != HttpStatusCode.OK) { + call.respond(status, data) + return data + } else { + call.respond(data) } - - val expiresIn2Days = Clock.System.now().plus(2, DateTimeUnit.HOUR*24) //? - - val receiverUma = "${config.username}:${getReceivingVaspDomain(call)}" - println(config.vaspDomain) - - val response = uma.getInvoice( - receiverUma = receiverUma, - invoiceUUID = UUID.randomUUID().toString(), - amount = amount, - receivingCurrency = InvoiceCurrency( - currency.code, currency.name, currency.symbol, currency.decimals - ), - expiration = expiresIn2Days.toEpochMilliseconds(), - isSubjectToTravelRule = true, - requiredPayerData = createCounterPartyDataOptions( - "name" to false, - "email" to false, - "compliance" to true, - "identifier" to true, - ), - callback = getLnurlpCallback(call), // structured the same, going to /api/uma/payreq/{user_id} - privateSigningKey = config.umaSigningPrivKey - ) - call.respond(response.toBech32()) return "OK" } @@ -128,14 +86,37 @@ class ReceivingVasp( call.respond(HttpStatusCode.BadRequest, "SenderUma not provided.") return "SenderUma not provided." } + val (status, data) = createUmaInvoice(call, senderUma) + if (status != HttpStatusCode.OK) { + call.respond(status, data) + return data + } + val response = try { + httpClient.post("/api/uma/payreq/${config.userID}") { + contentType(ContentType.Application.Json) + setBody(parameter("invoice", data)) + } + } catch (e: Exception) { + call.respond(HttpStatusCode.FailedDependency, "failed to fetch /api/uma/payreq/${config.userID}") + return "failed to fetch /api/uma/payreq/${config.userID}" + } + if (response.status != HttpStatusCode.OK) { + call.respond(HttpStatusCode.InternalServerError, "Payreq to Sending Vasp: ${response.status}") + return "Payreq to sending vasp failed: ${response.status}" + } + call.respond(response.body()) + return "OK" + } + + private fun createUmaInvoice( + call: ApplicationCall, senderUma: String? = null + ): Pair { val amount = try { call.parameters["amount"]?.toLong() ?: run { - call.respond(HttpStatusCode.BadRequest, "Amount not provided.") - return "Amount not provided." + return HttpStatusCode.BadRequest to "Amount not provided." } } catch (e: NumberFormatException) { - call.respond(HttpStatusCode.BadRequest, "Amount not parsable as number.") - return "Amount not parsable as number." + return HttpStatusCode.BadRequest to "Amount not parsable as number." } val currency = call.parameters["currencyCode"]?.let { currencyCode -> @@ -143,12 +124,10 @@ class ReceivingVasp( getReceivingCurrencies(UMA_VERSION_STRING).firstOrNull { it.code == currencyCode } ?: run { - call.respond(HttpStatusCode.BadRequest, "Unsupported CurrencyCode $currencyCode.") - return "Unsupported CurrencyCode $currencyCode." + return HttpStatusCode.BadRequest to "Unsupported CurrencyCode $currencyCode." } } ?: run { - call.respond(HttpStatusCode.BadRequest, "CurrencyCode not provided.") - return "CurrencyCode not provided." + return HttpStatusCode.BadRequest to "CurrencyCode not provided." } val expiresIn2Days = Clock.System.now().plus(2, DateTimeUnit.HOUR*24) //? @@ -175,17 +154,7 @@ class ReceivingVasp( privateSigningKey = config.umaSigningPrivKey, senderUma = senderUma ) - val encodedInvoice = invoice.toBech32() - val response = httpClient.post("/api/uma/payreq/${config.userID}") { - contentType(ContentType.Application.Json) - setBody(parameter("invoice", encodedInvoice)) - } - if (response.status != HttpStatusCode.OK) { - call.respond(HttpStatusCode.InternalServerError, "Payreq to Sending Vasp: ${response.status}") - return "Payreq to vasp2 failed: ${response.status}" - } - call.respond(response.body()) - return "OK" + return HttpStatusCode.OK to invoice.toBech32() } suspend fun handleLnurlp(call: ApplicationCall): String { From 4ba42984e254ecc6ea0e752f7845152698b5cac3 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 17 Sep 2024 13:45:00 -0700 Subject: [PATCH 06/31] change to fetching well known configuration from sending vasp --- .../kotlin/com/lightspark/ReceivingVasp.kt | 35 +++++++++++++++++-- .../main/kotlin/com/lightspark/SendingVasp.kt | 2 +- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt index fa72ad87..b66df72f 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt @@ -7,9 +7,11 @@ import com.lightspark.sdk.model.Node import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.get import io.ktor.client.request.parameter import io.ktor.client.request.post import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.http.contentType @@ -50,6 +52,8 @@ 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( @@ -91,14 +95,39 @@ class ReceivingVasp( call.respond(status, data) return data } + val senderComponents = senderUma.split("@") + val sendingVaspDomain = senderComponents.getOrNull(1) ?: run { + call.respond(HttpStatusCode.BadRequest, "Invalid senderUma.") + return "Invalid senderUma." + } + val wellKnownConfiguration = "http://$sendingVaspDomain/.well-known/uma-configuration" + val umaEndpoint = try { + val response = httpClient.get(wellKnownConfiguration) + if (response.status != HttpStatusCode.OK) { + call.respond( + HttpStatusCode.FailedDependency, + "failed to fetch request / pay endpoint at $wellKnownConfiguration" + ) + return "failed to fetch request / pay endpoint at $wellKnownConfiguration" + } else { + Json.decodeFromString( + response.bodyAsText())["uma_request_endpoint"]?.jsonPrimitive?.content ?: "" + } + } catch (e: Exception) { + call.respond( + HttpStatusCode.FailedDependency, + "failed to fetch request / pay endpoint at $wellKnownConfiguration" + ) + return "failed to fetch request / pay endpoint at $wellKnownConfiguration" + } val response = try { - httpClient.post("/api/uma/payreq/${config.userID}") { + httpClient.post(umaEndpoint) { contentType(ContentType.Application.Json) setBody(parameter("invoice", data)) } } catch (e: Exception) { - call.respond(HttpStatusCode.FailedDependency, "failed to fetch /api/uma/payreq/${config.userID}") - return "failed to fetch /api/uma/payreq/${config.userID}" + call.respond(HttpStatusCode.FailedDependency, "failed to fetch $umaEndpoint") + return "failed to fetch $umaEndpoint" } if (response.status != HttpStatusCode.OK) { call.respond(HttpStatusCode.InternalServerError, "Payreq to Sending Vasp: ${response.status}") diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt index 060a3a33..139094cc 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt @@ -525,7 +525,7 @@ fun Routing.registerSendingVaspRoutes(sendingVasp: SendingVasp) { call.debugLog(sendingVasp.payInvoice(call)) } - post("/api/uma/request_invoice_payment") { + post("/api/uma/request_pay_invoice") { call.debugLog(sendingVasp.requestInvoicePayment(call)) } } From 55251cfdaefa783ed459b6e787fb0dbb783b99a8 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 17 Sep 2024 15:28:11 -0700 Subject: [PATCH 07/31] revert to previous endpoint --- .../kotlin/com/lightspark/ReceivingVasp.kt | 19 +++++++++++-------- .../main/kotlin/com/lightspark/SendingVasp.kt | 2 +- .../kotlin/com/lightspark/plugins/Routing.kt | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt index b66df72f..c8f87085 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt @@ -102,24 +102,28 @@ class ReceivingVasp( } val wellKnownConfiguration = "http://$sendingVaspDomain/.well-known/uma-configuration" val umaEndpoint = try { - val response = httpClient.get(wellKnownConfiguration) - if (response.status != HttpStatusCode.OK) { + val umaConfigResponse = httpClient.get(wellKnownConfiguration) + if (umaConfigResponse.status != HttpStatusCode.OK) { call.respond( HttpStatusCode.FailedDependency, - "failed to fetch request / pay endpoint at $wellKnownConfiguration" + "failed to fetch request / pay endpoint at $wellKnownConfiguration", ) return "failed to fetch request / pay endpoint at $wellKnownConfiguration" - } else { - Json.decodeFromString( - response.bodyAsText())["uma_request_endpoint"]?.jsonPrimitive?.content ?: "" } + Json.decodeFromString( + umaConfigResponse.bodyAsText(), + )["uma_request_endpoint"]?.jsonPrimitive?.content } catch (e: Exception) { call.respond( HttpStatusCode.FailedDependency, - "failed to fetch request / pay endpoint at $wellKnownConfiguration" + "failed to fetch request / pay endpoint at $wellKnownConfiguration", ) 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" + } val response = try { httpClient.post(umaEndpoint) { contentType(ContentType.Application.Json) @@ -162,7 +166,6 @@ class ReceivingVasp( val expiresIn2Days = Clock.System.now().plus(2, DateTimeUnit.HOUR*24) //? val receiverUma = "${config.username}:${getReceivingVaspDomain(call)}" - println(config.vaspDomain) val invoice = uma.getInvoice( receiverUma = receiverUma, diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt index 139094cc..060a3a33 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt @@ -525,7 +525,7 @@ fun Routing.registerSendingVaspRoutes(sendingVasp: SendingVasp) { call.debugLog(sendingVasp.payInvoice(call)) } - post("/api/uma/request_pay_invoice") { + post("/api/uma/request_invoice_payment") { call.debugLog(sendingVasp.requestInvoicePayment(call)) } } diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt b/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt index 2d2b3eb3..e03e98ad 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt @@ -57,7 +57,7 @@ fun Application.configureRouting( call.respond( HttpStatusCode.OK, buildJsonObject { - put("uma_request_endpoint", "$scheme://$domain/api/uma/request_pay_invoice") + put("uma_request_endpoint", "$scheme://$domain/api/uma/request_invoice_payment") put( "uma_major_versions", buildJsonArray { From f56e77110c7f2fee5188f9ea478158b8fe02b52f Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 9 Sep 2024 15:06:28 -0700 Subject: [PATCH 08/31] adding sender vasp demo endpoints --- .../kotlin/com/lightspark/ReceivingVasp.kt | 12 +- .../main/kotlin/com/lightspark/SendingVasp.kt | 164 +++++++++++++++++- ...estCache.kt => SendingVaspRequestCache.kt} | 35 ++-- 3 files changed, 191 insertions(+), 20 deletions(-) rename umaserverdemo/src/main/kotlin/com/lightspark/{Vasp1RequestCache.kt => SendingVaspRequestCache.kt} (62%) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt index c8f87085..075188fc 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt @@ -134,8 +134,8 @@ class ReceivingVasp( return "failed to fetch $umaEndpoint" } if (response.status != HttpStatusCode.OK) { - call.respond(HttpStatusCode.InternalServerError, "Payreq to Sending Vasp: ${response.status}") - return "Payreq to sending vasp failed: ${response.status}" + call.respond(HttpStatusCode.InternalServerError, "Payreq to Sending Vasp failed: ${response.status}") + return "Payreq to sending failed: ${response.status}" } call.respond(response.body()) return "OK" @@ -163,9 +163,9 @@ class ReceivingVasp( return HttpStatusCode.BadRequest to "CurrencyCode not provided." } - val expiresIn2Days = Clock.System.now().plus(2, DateTimeUnit.HOUR*24) //? + val expiresIn2Days = Clock.System.now().plus(2, DateTimeUnit.HOUR*24) - val receiverUma = "${config.username}:${getReceivingVaspDomain(call)}" + val receiverUma = buildReceiverUma(call) val invoice = uma.getInvoice( receiverUma = receiverUma, @@ -186,6 +186,7 @@ class ReceivingVasp( privateSigningKey = config.umaSigningPrivKey, senderUma = senderUma ) + return HttpStatusCode.OK to invoice.toBech32() } @@ -370,6 +371,7 @@ class ReceivingVasp( return "Invalid payreq signature." } + senderUmaVersion = UMA_VERSION_STRING val receivingCurrency = getReceivingCurrencies(senderUmaVersion) .firstOrNull { it.code == request.receivingCurrencyCode() } ?: run { call.respond(HttpStatusCode.BadRequest, "Unsupported currency.") @@ -473,6 +475,8 @@ class ReceivingVasp( return "$protocol://$host$port$path" } + private fun buildReceiverUma(call: ApplicationCall) = "$${config.username}@${getReceivingVaspDomain(call)}" + private fun getReceivingVaspDomain(call: ApplicationCall) = config.vaspDomain ?: call.originWithPort() } diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt index 060a3a33..60da41e5 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt @@ -50,6 +50,7 @@ import me.uma.UMA_VERSION_STRING import me.uma.UmaProtocolHelper import me.uma.protocol.CounterPartyDataOptions import me.uma.protocol.CurrencySerializer +import me.uma.protocol.Invoice import me.uma.protocol.KycStatus import me.uma.protocol.PayRequest import me.uma.protocol.UtxoWithAmount @@ -71,15 +72,166 @@ class SendingVasp( ) } } - private val requestDataCache = Vasp1RequestCache() + private val requestDataCache = SendingVaspRequestCache() private val nonceCache = InMemoryNonceCache(Clock.System.now().epochSeconds) private lateinit var receiverUmaVersion: String suspend fun payInvoice(call: ApplicationCall): String { + // or get it from the cache + val umaInvoice = call.request.queryParameters["invoice"]?.let { invoiceStr -> + // handle the case where users have provided the uuid of a cached invoice, rather + // than a full bech32 encoded invoice + if (!invoiceStr.startsWith("uma")) { + requestDataCache.getUmaInvoiceData(invoiceStr) + } else { + Invoice.fromBech32(invoiceStr) + } + } ?: run { + call.respond(HttpStatusCode.BadRequest, "Unable to decode invoice.") + return "Unable to decode invoice." + } + println(umaInvoice.receiverUma) + val receiverVaspDomain = umaInvoice.receiverUma.split("@").getOrNull(1) ?: run { + call.respond(HttpStatusCode.FailedDependency, "Failed to parse receiver vasp.") + return "Failed to parse receiver vasp." + } + 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." + } + if (!uma.verifyUmaInvoice(umaInvoice, receiverVaspPubKeys)) { + call.respond(HttpStatusCode.BadRequest, "Unable to decode invoice.") + return "Unable to decode invoice." + } + + val payer = getPayerProfile(umaInvoice.requiredPayerData ?: emptyMap(), call) + // initial request data is cached in request and pay invoice + + val currencyValid = getReceivingCurrencies(UMA_VERSION_STRING).any { + it.code == umaInvoice.receivingCurrency.code + } + if (!currencyValid) { + call.respond(HttpStatusCode.BadRequest, "Receiving currency code not supported.") + return "Receiving currency code not supported." + } + + val trInfo = "Here is some fake travel rule info. It's up to you to actually implement this if needed." + val payerUtxos = emptyList() + + val isAmountInMsats = call.request.queryParameters["isAmountInMsats"]?.toBoolean() ?: + (umaInvoice.receivingCurrency.code == "SAT") + + val payReq = uma.getPayRequest( + receiverEncryptionPubKey = receiverVaspPubKeys.getEncryptionPublicKey(), + sendingVaspPrivateKey = config.umaSigningPrivKey, + receivingCurrencyCode = umaInvoice.receivingCurrency.code, + isAmountInReceivingCurrency = !isAmountInMsats, + amount = umaInvoice.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 = umaInvoice.umaVersion, + ) + + + val response = try { + httpClient.post(umaInvoice.callback) { + contentType(ContentType.Application.Json) + setBody(payReq.toJson()) + } + } catch (e: Exception) { + call.respond(HttpStatusCode.FailedDependency, "Unable to connect to ${umaInvoice.callback}") + return "Unable to connect to ${umaInvoice.callback}" + } + if (response.status != HttpStatusCode.OK) { + call.respond(HttpStatusCode.InternalServerError, "Payreq to receiving vasp failed: ${response.status}") + return "Payreq to receiving vasp failed: ${response.status}" + } + + 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." + } + + if (!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 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." + } + + 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." + } + + val newCallbackId = requestDataCache.savePayReqData( + encodedInvoice = payReqResponse.encodedInvoice, + utxoCallback = getUtxoCallback(call, "1234abc"), + invoiceData = invoice, + ) + + // we've successfully fulfilled this request, so remove cached invoice uuid data + requestDataCache.removeUmaInvoiceData(umaInvoice.invoiceUUID) + + call.respond( + buildJsonObject { + put("encodedInvoice", payReqResponse.encodedInvoice) + put("callbackUuid", newCallbackId) + put("amountMsats", invoice.amount.toMilliSats()) + put("amountReceivingCurrency", payReqResponse.paymentInfo?.amount ?: umaInvoice.amount) + put("receivingCurrencyDecimals", payReqResponse.paymentInfo?.decimals ?: 0) + put("exchangeFeesMsats", payReqResponse.paymentInfo?.exchangeFeesMillisatoshi ?: 0) + put("conversionRate", payReqResponse.paymentInfo?.multiplier ?: 1000) + put("receivingCurrencyCode", payReqResponse.paymentInfo?.currencyCode ?: "SAT") + }, + ) + return "OK" } suspend fun requestInvoicePayment(call: ApplicationCall): String { + val umaInvoice = call.request.queryParameters["invoice"]?.let(Invoice::fromBech32) ?: run { + call.respond(HttpStatusCode.BadRequest, "Unable to decode invoice.") + return "Unable to decode invoice." + } + print(umaInvoice) + val receiverVaspDomain = umaInvoice.receiverUma.split("@").getOrNull(1) ?: run { + call.respond(HttpStatusCode.FailedDependency, "Failed to parse receiver vasp.") + return "Failed to parse receiver vasp." + } + 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." + } + if (!uma.verifyUmaInvoice(umaInvoice, receiverVaspPubKeys)) { + call.respond(HttpStatusCode.BadRequest, "Unable to decode invoice.") + return "Unable to decode invoice." + } + requestDataCache.saveUmaInvoice(umaInvoice.invoiceUUID, umaInvoice) return "OK" } @@ -233,9 +385,9 @@ class SendingVasp( // The default for UMA requests should be to assume the receiving currency, but for non-UMA, we default to msats. val isAmountInMsats = call.request.queryParameters["isAmountInMsats"]?.toBoolean() ?: !isUma - val vasp2PubKeys = if (isUma) { + val receiverVaspPubKeys = if (isUma) { try { - uma.fetchPublicKeysForVasp(initialRequestData.vasp2Domain) + 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.") @@ -252,7 +404,7 @@ class SendingVasp( val payReq = try { if (isUma) { uma.getPayRequest( - receiverEncryptionPubKey = vasp2PubKeys!!.getEncryptionPublicKey(), + receiverEncryptionPubKey = receiverVaspPubKeys!!.getEncryptionPublicKey(), sendingVaspPrivateKey = config.umaSigningPrivKey, receivingCurrencyCode = currencyCode, isAmountInReceivingCurrency = !isAmountInMsats, @@ -319,7 +471,7 @@ class SendingVasp( if (isUma) { try { - uma.verifyPayReqResponseSignature(payReqResponse, vasp2PubKeys!!, payer.identifier, nonceCache) + 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." @@ -429,7 +581,7 @@ class SendingVasp( private suspend fun sendPostTransactionCallback( payment: OutgoingPayment, - payReqData: Vasp1PayReqData, + payReqData: SendingVaspPayReqData, call: ApplicationCall, ) { val utxos = payment.umaPostTransactionData?.map { diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/Vasp1RequestCache.kt b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVaspRequestCache.kt similarity index 62% rename from umaserverdemo/src/main/kotlin/com/lightspark/Vasp1RequestCache.kt rename to umaserverdemo/src/main/kotlin/com/lightspark/SendingVaspRequestCache.kt index d63584a4..90a6ffe9 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/Vasp1RequestCache.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVaspRequestCache.kt @@ -1,6 +1,7 @@ package com.lightspark import com.lightspark.sdk.model.InvoiceData +import me.uma.protocol.Invoice import java.util.UUID import me.uma.protocol.LnurlpResponse @@ -8,42 +9,52 @@ import me.uma.protocol.LnurlpResponse * A simple in-memory cache for data that needs to be remembered between calls to VASP1. In practice, this would be * stored in a database or other persistent storage. */ -class Vasp1RequestCache { +class SendingVaspRequestCache { /** * This is a map of the UMA request UUID to the LnurlpResponse from that initial Lnurlp request. * This is used to cache the LnurlpResponse so that we can use it to generate the UMA payreq without the client * having to make another Lnurlp request or remember lots of details. * NOTE: In production, this should be stored in a database or other persistent storage. */ - private val lnurlpRequestCache: MutableMap = mutableMapOf() + private val lnurlpRequestCache: MutableMap = mutableMapOf() /** * This is a map of the UMA request UUID to the payreq data that we generated for that request. * This is used to cache the payreq data so that we can pay the invoice when the user confirms * NOTE: In production, this should be stored in a database or other persistent storage. */ - private val payReqCache: MutableMap = mutableMapOf() + private val payReqCache: MutableMap = mutableMapOf() - fun getLnurlpResponseData(uuid: String): Vasp1InitialRequestData? { + private val umaInvoiceCache: MutableMap = mutableMapOf() + + fun getLnurlpResponseData(uuid: String): SendingVaspInitialRequestData? { return lnurlpRequestCache[uuid] } - fun getPayReqData(uuid: String): Vasp1PayReqData? { + fun getPayReqData(uuid: String): SendingVaspPayReqData? { return payReqCache[uuid] } + fun getUmaInvoiceData(uuid: String): Invoice? { + return umaInvoiceCache[uuid] + } + fun saveLnurlpResponseData(lnurlpResponse: LnurlpResponse, receiverId: String, vasp2Domain: String): String { val uuid = UUID.randomUUID().toString() - lnurlpRequestCache[uuid] = Vasp1InitialRequestData(lnurlpResponse, receiverId, vasp2Domain) + lnurlpRequestCache[uuid] = SendingVaspInitialRequestData(lnurlpResponse, receiverId, vasp2Domain) return uuid } fun savePayReqData(encodedInvoice: String, utxoCallback: String, invoiceData: InvoiceData): String { val uuid = UUID.randomUUID().toString() - payReqCache[uuid] = Vasp1PayReqData(encodedInvoice, utxoCallback, invoiceData) + payReqCache[uuid] = SendingVaspPayReqData(encodedInvoice, utxoCallback, invoiceData) return uuid } + fun saveUmaInvoice(uuid: String, invoice: Invoice) { + umaInvoiceCache[uuid] = invoice + } + fun removeLnurlpResponseData(uuid: String) { lnurlpRequestCache.remove(uuid) } @@ -51,15 +62,19 @@ class Vasp1RequestCache { fun removePayReqData(uuid: String) { payReqCache.remove(uuid) } + + fun removeUmaInvoiceData(uuid: String) { + umaInvoiceCache.remove(uuid) + } } -data class Vasp1InitialRequestData( +data class SendingVaspInitialRequestData( val lnurlpResponse: LnurlpResponse, val receiverId: String, - val vasp2Domain: String, + val receivingVaspDomain: String, ) -data class Vasp1PayReqData( +data class SendingVaspPayReqData( val encodedInvoice: String, val utxoCallback: String, val invoiceData: InvoiceData, From c8f8a483367eab428d5de689fd0934908c98b6d8 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 10 Sep 2024 11:01:20 -0700 Subject: [PATCH 09/31] nits and such --- .../kotlin/com/lightspark/ReceivingVasp.kt | 9 ++++++++ .../main/kotlin/com/lightspark/SendingVasp.kt | 22 +++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt index 075188fc..bf589adc 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt @@ -90,6 +90,11 @@ class ReceivingVasp( call.respond(HttpStatusCode.BadRequest, "SenderUma not provided.") return "SenderUma not provided." } + val senderUmaComponents = senderUma.split("@") + if (senderUmaComponents.size != 2) { + call.respond(HttpStatusCode.BadRequest, "SenderUma format invalid: $senderUma.") + return "SenderUma format invalid: $senderUma." + } val (status, data) = createUmaInvoice(call, senderUma) if (status != HttpStatusCode.OK) { call.respond(status, data) @@ -162,6 +167,10 @@ class ReceivingVasp( } ?: run { return HttpStatusCode.BadRequest to "CurrencyCode not provided." } + + if (amount < currency.minSendable() || amount > currency.maxSendable()) { + return HttpStatusCode.BadRequest to "CurrencyCode amount is outside of sendable range." + } val expiresIn2Days = Clock.System.now().plus(2, DateTimeUnit.HOUR*24) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt index 60da41e5..ef721e5a 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt @@ -77,7 +77,6 @@ class SendingVasp( private lateinit var receiverUmaVersion: String suspend fun payInvoice(call: ApplicationCall): String { - // or get it from the cache val umaInvoice = call.request.queryParameters["invoice"]?.let { invoiceStr -> // handle the case where users have provided the uuid of a cached invoice, rather // than a full bech32 encoded invoice @@ -90,7 +89,6 @@ class SendingVasp( call.respond(HttpStatusCode.BadRequest, "Unable to decode invoice.") return "Unable to decode invoice." } - println(umaInvoice.receiverUma) val receiverVaspDomain = umaInvoice.receiverUma.split("@").getOrNull(1) ?: run { call.respond(HttpStatusCode.FailedDependency, "Failed to parse receiver vasp.") return "Failed to parse receiver vasp." @@ -106,6 +104,10 @@ class SendingVasp( call.respond(HttpStatusCode.BadRequest, "Unable to decode invoice.") return "Unable to decode invoice." } + if (umaInvoice.expiration < Clock.System.now().toEpochMilliseconds()) { + call.respond(HttpStatusCode.BadRequest, "Invoice ${umaInvoice.invoiceUUID} has expired.") + return "Invoice ${umaInvoice.invoiceUUID} has expired." + } val payer = getPayerProfile(umaInvoice.requiredPayerData ?: emptyMap(), call) // initial request data is cached in request and pay invoice @@ -121,14 +123,11 @@ 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 isAmountInMsats = call.request.queryParameters["isAmountInMsats"]?.toBoolean() ?: - (umaInvoice.receivingCurrency.code == "SAT") - val payReq = uma.getPayRequest( receiverEncryptionPubKey = receiverVaspPubKeys.getEncryptionPublicKey(), sendingVaspPrivateKey = config.umaSigningPrivKey, receivingCurrencyCode = umaInvoice.receivingCurrency.code, - isAmountInReceivingCurrency = !isAmountInMsats, + isAmountInReceivingCurrency = umaInvoice.receivingCurrency.code != "SAT", amount = umaInvoice.amount, payerIdentifier = payer.identifier, payerKycStatus = KycStatus.VERIFIED, @@ -142,7 +141,6 @@ class SendingVasp( receiverUmaVersion = umaInvoice.umaVersion, ) - val response = try { httpClient.post(umaInvoice.callback) { contentType(ContentType.Application.Json) @@ -153,7 +151,10 @@ class SendingVasp( return "Unable to connect to ${umaInvoice.callback}" } if (response.status != HttpStatusCode.OK) { - call.respond(HttpStatusCode.InternalServerError, "Payreq to receiving vasp failed: ${response.status}") + call.respond( + HttpStatusCode.InternalServerError, + "Payreq to receiving vasp failed: ${response.status}" + ) return "Payreq to receiving vasp failed: ${response.status}" } @@ -166,7 +167,10 @@ class SendingVasp( if (!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") + call.respond( + HttpStatusCode.FailedDependency, + "Received non-UMA response from receiving vasp for an UMA request" + ) return "Received non-UMA response from receiving vasp." } From a93d6576a6721ad36c2f59d42627310fb4d8107f Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 10 Sep 2024 11:16:52 -0700 Subject: [PATCH 10/31] moving parameter to json body --- umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt index ef721e5a..0be636bd 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt @@ -215,7 +215,7 @@ class SendingVasp( } suspend fun requestInvoicePayment(call: ApplicationCall): String { - val umaInvoice = call.request.queryParameters["invoice"]?.let(Invoice::fromBech32) ?: run { + val umaInvoice = call.parameters["invoice"]?.let(Invoice::fromBech32) ?: run { call.respond(HttpStatusCode.BadRequest, "Unable to decode invoice.") return "Unable to decode invoice." } From 119a9e7d54859aaf9916e8872f0a55ab79886659 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 17 Sep 2024 17:04:36 -0700 Subject: [PATCH 11/31] Update SendingVasp.kt --- umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt index 0be636bd..24fb453a 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt @@ -232,7 +232,7 @@ class SendingVasp( return "Failed to fetch public keys." } if (!uma.verifyUmaInvoice(umaInvoice, receiverVaspPubKeys)) { - call.respond(HttpStatusCode.BadRequest, "Unable to decode invoice.") + call.respond(HttpStatusCode.BadRequest, "Invalid invoice signature.") return "Unable to decode invoice." } requestDataCache.saveUmaInvoice(umaInvoice.invoiceUUID, umaInvoice) From 7acf4dafbc517439bc8ec782015cdd6d195cbfb3 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 17 Sep 2024 17:40:38 -0700 Subject: [PATCH 12/31] ms -> s --- umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt | 2 +- umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt index bf589adc..48db747c 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt @@ -183,7 +183,7 @@ class ReceivingVasp( receivingCurrency = InvoiceCurrency( currency.code, currency.name, currency.symbol, currency.decimals ), - expiration = expiresIn2Days.toEpochMilliseconds(), + expiration = expiresIn2Days.epochSeconds, isSubjectToTravelRule = true, requiredPayerData = createCounterPartyDataOptions( "name" to false, diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt index 24fb453a..36ae8448 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/SendingVasp.kt @@ -104,7 +104,7 @@ class SendingVasp( call.respond(HttpStatusCode.BadRequest, "Unable to decode invoice.") return "Unable to decode invoice." } - if (umaInvoice.expiration < Clock.System.now().toEpochMilliseconds()) { + if (umaInvoice.expiration < Clock.System.now().epochSeconds) { call.respond(HttpStatusCode.BadRequest, "Invoice ${umaInvoice.invoiceUUID} has expired.") return "Invoice ${umaInvoice.invoiceUUID} has expired." } @@ -219,7 +219,6 @@ class SendingVasp( call.respond(HttpStatusCode.BadRequest, "Unable to decode invoice.") return "Unable to decode invoice." } - print(umaInvoice) val receiverVaspDomain = umaInvoice.receiverUma.split("@").getOrNull(1) ?: run { call.respond(HttpStatusCode.FailedDependency, "Failed to parse receiver vasp.") return "Failed to parse receiver vasp." From 0427a3fd85f07481130e53828dd13f6a73bb7da8 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 23 Sep 2024 11:17:09 -0700 Subject: [PATCH 13/31] Add explicit timeouts to the Requester --- .../kotlin/com/lightspark/sdk/core/requester/Requester.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/src/commonMain/kotlin/com/lightspark/sdk/core/requester/Requester.kt b/core/src/commonMain/kotlin/com/lightspark/sdk/core/requester/Requester.kt index 1d055f77..8bd7eaab 100644 --- a/core/src/commonMain/kotlin/com/lightspark/sdk/core/requester/Requester.kt +++ b/core/src/commonMain/kotlin/com/lightspark/sdk/core/requester/Requester.kt @@ -10,6 +10,7 @@ import com.lightspark.sdk.core.crypto.NodeKeyCache import com.lightspark.sdk.core.crypto.nextLong import com.lightspark.sdk.core.util.getPlatform import io.ktor.client.HttpClient +import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.compression.ContentEncoding import io.ktor.client.plugins.websocket.WebSockets import io.ktor.client.request.headers @@ -52,6 +53,12 @@ class Requester constructor( gzip() // Switch to deflate() when https://youtrack.jetbrains.com/issue/KTOR-6999 is fixed } install(WebSockets) + install(HttpTimeout) { + // See https://ktor.io/docs/client-timeout.html for more info. + requestTimeoutMillis = 20000 + connectTimeoutMillis = 5000 + socketTimeoutMillis = 10000 + } } private val userAgent = "lightspark-kotlin-sdk/${LightsparkCoreConfig.VERSION} ${getPlatform().platformName}/${getPlatform().version}" From a6d5582d4122ba8e1cce681ac19ad2bd7cc8beac Mon Sep 17 00:00:00 2001 From: Joel Weinberger Date: Mon, 30 Sep 2024 14:56:59 -0700 Subject: [PATCH 14/31] Create SECURITY.md --- SECURITY.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..17083d31 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +At Lightspark, we take security very seriously. We appreciate responsible +disclosure of security issues and will endeavor to acknowledge your +contributions. + + +## Reporting a Vulnerability + +If you believe you have found a security issue or problem, please email us +at security@lightspark.com with as much detail as possible. Alternatively, +you may report a security issue through the GitHub Security Advisory +["Report a Vulnerability" tab](https://github.com/lightsparkdev/kotlin-sdk/security/advisories/new). From 86e830e1a621e08f1d64acd148d785a9c4aee613 Mon Sep 17 00:00:00 2001 From: Jason Wang Date: Thu, 10 Oct 2024 09:13:13 -0700 Subject: [PATCH 15/31] Regenerating SDKs following introspection of schemas. --- .../kotlin/com/lightspark/sdk/model/CreateUmaInvoiceInput.kt | 5 +++++ .../kotlin/com/lightspark/sdk/model/CurrencyUnit.kt | 3 +++ .../kotlin/com/lightspark/sdk/wallet/model/CurrencyUnit.kt | 3 +++ 3 files changed, 11 insertions(+) diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/model/CreateUmaInvoiceInput.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/model/CreateUmaInvoiceInput.kt index 2b131666..9f487dcb 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/model/CreateUmaInvoiceInput.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/model/CreateUmaInvoiceInput.kt @@ -8,6 +8,11 @@ import kotlinx.serialization.Serializable /** * + * @param nodeId The node from which to create the invoice. + * @param amountMsats The amount for which the invoice should be created, in millisatoshis. + * @param metadataHash The SHA256 hash of the UMA metadata payload. This will be present in the h-tag (SHA256 purpose of payment) of the resulting Bolt 11 invoice. + * @param expirySecs The expiry of the invoice in seconds. Default value is 86400 (1 day). + * @param receiverHash An optional, monthly-rotated, unique hashed identifier corresponding to the receiver of the payment. */ @Serializable @SerialName("CreateUmaInvoiceInput") diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/model/CurrencyUnit.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/model/CurrencyUnit.kt index dd5aa5d9..84a55fe8 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/model/CurrencyUnit.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/model/CurrencyUnit.kt @@ -23,6 +23,9 @@ enum class CurrencyUnit( /** United States Dollar. **/ USD("USD"), + /** Mexican Peso. **/ + MXN("MXN"), + /** 0.000000001 (10e-9) Bitcoin or a billionth of a Bitcoin. We recommend using the Satoshi unit instead when possible. **/ NANOBITCOIN("NANOBITCOIN"), diff --git a/wallet-sdk/src/commonMain/kotlin/com/lightspark/sdk/wallet/model/CurrencyUnit.kt b/wallet-sdk/src/commonMain/kotlin/com/lightspark/sdk/wallet/model/CurrencyUnit.kt index 804cba24..859ec663 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/lightspark/sdk/wallet/model/CurrencyUnit.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/lightspark/sdk/wallet/model/CurrencyUnit.kt @@ -23,6 +23,9 @@ enum class CurrencyUnit( /** United States Dollar. **/ USD("USD"), + /** Mexican Peso. **/ + MXN("MXN"), + /** 0.000000001 (10e-9) Bitcoin or a billionth of a Bitcoin. We recommend using the Satoshi unit instead when possible. **/ NANOBITCOIN("NANOBITCOIN"), From 6a463b385991cd827d093c9b1b14e4fc58760b11 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Wed, 12 Feb 2025 12:24:11 -0800 Subject: [PATCH 16/31] Bump ci runners to macos-13. (#214) --- .github/workflows/core-build.yaml | 2 +- .github/workflows/core-publish.yaml | 2 +- .github/workflows/crypto-build.yaml | 2 +- .github/workflows/crypto-publish.yaml | 2 +- .github/workflows/docs-publish.yaml | 2 +- .github/workflows/lightspark-sdk-build.yaml | 2 +- .github/workflows/lightspark-sdk-publish.yaml | 2 +- .github/workflows/release-branch-cut.yaml | 2 +- .github/workflows/wallet-build.yaml | 2 +- .github/workflows/wallet-publish.yaml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/core-build.yaml b/.github/workflows/core-build.yaml index 0e610f34..df70df3b 100644 --- a/.github/workflows/core-build.yaml +++ b/.github/workflows/core-build.yaml @@ -19,7 +19,7 @@ on: jobs: build: - runs-on: macos-12 + runs-on: macos-13 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/core-publish.yaml b/.github/workflows/core-publish.yaml index 851af93e..3404b054 100644 --- a/.github/workflows/core-publish.yaml +++ b/.github/workflows/core-publish.yaml @@ -7,7 +7,7 @@ on: jobs: publish-core-sdk: if: ${{ startsWith(github.event.release.tag_name, 'core-') }} - runs-on: macos-12 + runs-on: macos-13 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/crypto-build.yaml b/.github/workflows/crypto-build.yaml index a68c8fc3..0ccbac80 100644 --- a/.github/workflows/crypto-build.yaml +++ b/.github/workflows/crypto-build.yaml @@ -21,7 +21,7 @@ jobs: build: # No point in running this build on a core cut because it will fail until the deploy is done. if: github.event.base_ref == null || !startsWith(github.event.base_ref, 'release/core-') - runs-on: macos-12 + runs-on: macos-13 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/crypto-publish.yaml b/.github/workflows/crypto-publish.yaml index 76f404d7..f8154c8e 100644 --- a/.github/workflows/crypto-publish.yaml +++ b/.github/workflows/crypto-publish.yaml @@ -7,7 +7,7 @@ on: jobs: publish-crypto-sdk: if: ${{ startsWith(github.event.release.tag_name, 'crypto-') }} - runs-on: macos-12 + runs-on: macos-13 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/docs-publish.yaml b/.github/workflows/docs-publish.yaml index 2d105114..542adfff 100644 --- a/.github/workflows/docs-publish.yaml +++ b/.github/workflows/docs-publish.yaml @@ -16,7 +16,7 @@ on: jobs: publish-docs: if: ${{ github.event.workflow_run.conclusion == 'success' }} - runs-on: macos-12 + runs-on: macos-13 environment: "docs" permissions: id-token: write diff --git a/.github/workflows/lightspark-sdk-build.yaml b/.github/workflows/lightspark-sdk-build.yaml index a81db4cd..e7d54771 100644 --- a/.github/workflows/lightspark-sdk-build.yaml +++ b/.github/workflows/lightspark-sdk-build.yaml @@ -23,7 +23,7 @@ jobs: build: # No point in running this build on a core cut because it will fail until the deploy is done. if: github.event.base_ref == null || !startsWith(github.event.base_ref, 'release/core-') - runs-on: macos-12 + runs-on: macos-13 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/lightspark-sdk-publish.yaml b/.github/workflows/lightspark-sdk-publish.yaml index d8f4a5c8..99333252 100644 --- a/.github/workflows/lightspark-sdk-publish.yaml +++ b/.github/workflows/lightspark-sdk-publish.yaml @@ -7,7 +7,7 @@ on: jobs: publish-lightspark-sdk: if: ${{ startsWith(github.event.release.tag_name, 'lightspark-sdk-') }} - runs-on: macos-12 + runs-on: macos-13 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/release-branch-cut.yaml b/.github/workflows/release-branch-cut.yaml index e741d034..bd40cba4 100644 --- a/.github/workflows/release-branch-cut.yaml +++ b/.github/workflows/release-branch-cut.yaml @@ -7,7 +7,7 @@ on: jobs: bump-versions: - runs-on: macos-12 + runs-on: macos-13 permissions: contents: write pull-requests: write diff --git a/.github/workflows/wallet-build.yaml b/.github/workflows/wallet-build.yaml index a8b8769e..b10aa2da 100644 --- a/.github/workflows/wallet-build.yaml +++ b/.github/workflows/wallet-build.yaml @@ -23,7 +23,7 @@ jobs: build: # No point in running this build on a core cut because it will fail until the deploy is done. if: github.event.base_ref == null || !startsWith(github.event.base_ref, 'release/core-') - runs-on: macos-12 + runs-on: macos-13 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/wallet-publish.yaml b/.github/workflows/wallet-publish.yaml index ce4b30c0..53c08fe7 100644 --- a/.github/workflows/wallet-publish.yaml +++ b/.github/workflows/wallet-publish.yaml @@ -7,7 +7,7 @@ on: jobs: publish-wallet-sdk: if: ${{ startsWith(github.event.release.tag_name, 'wallet-sdk-') }} - runs-on: macos-12 + runs-on: macos-13 steps: - uses: actions/checkout@v3 From 8134d1773f465d8978c4ac8cdb8df51d1393a9b8 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Wed, 12 Feb 2025 12:39:23 -0800 Subject: [PATCH 17/31] Expose idempotency keys for operations which allow them. (#212) --- .../lightspark/sdk/LightsparkFuturesClient.kt | 36 ++++++++++--- .../sdk/LightsparkCoroutinesClient.kt | 18 +++++++ .../lightspark/sdk/LightsparkSyncClient.kt | 51 ++++++++++++++++--- .../com/lightspark/sdk/graphql/PayInvoice.kt | 2 + .../lightspark/sdk/graphql/PayUmaInvoice.kt | 2 + .../sdk/graphql/RequestWithdrawal.kt | 2 + .../com/lightspark/sdk/graphql/SendPayment.kt | 2 + 7 files changed, 99 insertions(+), 14 deletions(-) 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 9d76ef88..f32a2ecb 100644 --- a/lightspark-sdk/src/commonJvmAndroidMain/kotlin/com/lightspark/sdk/LightsparkFuturesClient.kt +++ b/lightspark-sdk/src/commonJvmAndroidMain/kotlin/com/lightspark/sdk/LightsparkFuturesClient.kt @@ -222,6 +222,8 @@ class LightsparkFuturesClient(config: ClientConfig) { * for a transaction between 10k sats and 100k sats, this would mean a fee limit of 15 to 150 sats. * @param amountMsats The amount to pay in milli-satoshis. Defaults to the full amount of the invoice. * @param timeoutSecs The number of seconds to wait for the payment to complete. Defaults to 60. + * @param idempotencyKey An optional key to ensure idempotency of the payment. If provided, the same result will be + * returned for the same idempotency key without triggering a new payment. * @return The payment details. */ @JvmOverloads @@ -231,6 +233,7 @@ class LightsparkFuturesClient(config: ClientConfig) { maxFeesMsats: Long, amountMsats: Long? = null, timeoutSecs: Int = 60, + idempotencyKey: String? = null, ): CompletableFuture = coroutineScope.future { coroutinesClient.payInvoice( @@ -239,6 +242,7 @@ class LightsparkFuturesClient(config: ClientConfig) { maxFeesMsats, amountMsats, timeoutSecs, + idempotencyKey, ) } @@ -257,6 +261,8 @@ class LightsparkFuturesClient(config: ClientConfig) { * @param signingPrivateKey The sender's signing private key. Used to hash the sender identifier. * @param senderIdentifier Optional identifier of the sender. If provided, this will be hashed using a * monthly-rotated seed and used for anonymized analysis. + * @param idempotencyKey An optional key to ensure idempotency of the payment. If provided, the same result will be + * returned for the same idempotency key without triggering a new payment. * @return The payment details. */ @JvmOverloads @@ -269,6 +275,7 @@ class LightsparkFuturesClient(config: ClientConfig) { timeoutSecs: Int = 60, signingPrivateKey: ByteArray? = null, senderIdentifier: String? = null, + idempotencyKey: String? = null, ): CompletableFuture = coroutineScope.future { coroutinesClient.payUmaInvoice( @@ -279,6 +286,7 @@ class LightsparkFuturesClient(config: ClientConfig) { timeoutSecs, signingPrivateKey, senderIdentifier, + idempotencyKey, ) } @@ -432,14 +440,26 @@ class LightsparkFuturesClient(config: ClientConfig) { * @param amountSats The amount of funds to withdraw in SATOSHI. * @param bitcoinAddress The Bitcoin address to withdraw funds to. * @param mode The mode to use for the withdrawal. See `WithdrawalMode` for more information. + * @param idempotencyKey An optional key to ensure idempotency of the withdrawal. If provided, the same result will + * be returned for the same idempotency key without triggering a new withdrawal. */ + @JvmOverloads fun requestWithdrawal( nodeId: String, amountSats: Long, bitcoinAddress: String, mode: WithdrawalMode, + idempotencyKey: String? = null, ): CompletableFuture = - coroutineScope.future { coroutinesClient.requestWithdrawal(nodeId, amountSats, bitcoinAddress, mode) } + coroutineScope.future { + coroutinesClient.requestWithdrawal( + nodeId, + amountSats, + bitcoinAddress, + mode, + idempotencyKey, + ) + } /** * Sends a payment directly to a node on the Lightning Network through the public key of the node without an invoice. @@ -451,6 +471,8 @@ class LightsparkFuturesClient(config: ClientConfig) { * As guidance, a maximum fee of 15 basis points should make almost all transactions succeed. For example, * for a transaction between 10k sats and 100k sats, this would mean a fee limit of 15 to 150 sats. * @param timeoutSecs The timeout in seconds that we will try to make the payment. + * @param idempotencyKey An optional key to ensure idempotency of the payment. If provided, the same result will be + * returned for the same idempotency key without triggering a new payment. * @return An `OutgoingPayment` object if the payment was successful, or throws if the payment failed. * @throws LightsparkException if the payment failed. */ @@ -461,6 +483,7 @@ class LightsparkFuturesClient(config: ClientConfig) { amountMsats: Long, maxFeesMsats: Long, timeoutSecs: Int = 60, + idempotencyKey: String? = null, ): CompletableFuture = coroutineScope.future { coroutinesClient.sendPayment( payerNodeId, @@ -468,6 +491,7 @@ class LightsparkFuturesClient(config: ClientConfig) { amountMsats, maxFeesMsats, timeoutSecs, + idempotencyKey, ) } @@ -579,27 +603,27 @@ class LightsparkFuturesClient(config: ClientConfig) { /** * fetch outgoing payments for a given payment hash - * + * * @param paymentHash the payment hash of the invoice for which to fetch the outgoing payments * @param transactionStatuses the transaction statuses to filter the payments by. If null, all payments will be returned. */ @Throws(LightsparkException::class, LightsparkAuthenticationException::class) fun getOutgoingPaymentsForPaymentHash( paymentHash: String, - transactionStatuses: List? = null + transactionStatuses: List? = null, ): CompletableFuture> = coroutineScope.future { coroutinesClient.getOutgoingPaymentForPaymentHash(paymentHash, transactionStatuses) } /** * fetch invoice for a given payments hash - * + * * @param paymentHash the payment hash of the invoice for which to fetch the outgoing payments * @param transactionStatuses the transaction statuses to filter the payments by. If null, all payments will be returned. */ @Throws(LightsparkException::class, LightsparkAuthenticationException::class) fun getInvoiceForPaymentHash( - paymentHash: String + paymentHash: String, ): CompletableFuture = coroutineScope.future { coroutinesClient.getInvoiceForPaymentHash(paymentHash) } @@ -623,7 +647,7 @@ class LightsparkFuturesClient(config: ClientConfig) { @Throws(LightsparkException::class, LightsparkAuthenticationException::class) fun getIncomingPaymentsForInvoice( invoiceId: String, - transactionStatuses: List? = null + transactionStatuses: List? = null, ): CompletableFuture> = coroutineScope.future { coroutinesClient.getIncomingPaymentsForInvoice(invoiceId, transactionStatuses) } 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 6eaf367c..82b673f1 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkCoroutinesClient.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkCoroutinesClient.kt @@ -347,6 +347,8 @@ class LightsparkCoroutinesClient private constructor( * for a transaction between 10k sats and 100k sats, this would mean a fee limit of 15 to 150 sats. * @param amountMsats The amount to pay in milli-satoshis. Defaults to the full amount of the invoice. * @param timeoutSecs The number of seconds to wait for the payment to complete. Defaults to 60. + * @param idempotencyKey An optional key to ensure idempotency of the payment. If provided, the same result will be + * returned for the same idempotency key without triggering a new payment. * @return The payment details. */ @JvmOverloads @@ -356,6 +358,7 @@ class LightsparkCoroutinesClient private constructor( maxFeesMsats: Long, amountMsats: Long? = null, timeoutSecs: Int = 60, + idempotencyKey: String? = null, ): OutgoingPayment { requireValidAuth() return executeQuery( @@ -367,6 +370,7 @@ class LightsparkCoroutinesClient private constructor( add("timeout_secs", timeoutSecs) add("maximum_fees_msats", maxFeesMsats) amountMsats?.let { add("amount_msats", amountMsats) } + idempotencyKey?.let { add("idempotency_key", idempotencyKey) } }, signingNodeId = nodeId, ) { @@ -392,6 +396,8 @@ class LightsparkCoroutinesClient private constructor( * @param signingPrivateKey The sender's signing private key. Used to hash the sender identifier. * @param senderIdentifier Optional identifier of the sender. If provided, this will be hashed using a * monthly-rotated seed and used for anonymized analysis. + * @param idempotencyKey An optional key to ensure idempotency of the payment. If provided, the same result will be + * returned for the same idempotency key without triggering a new payment. * @return The payment details. */ @JvmOverloads @@ -404,6 +410,7 @@ class LightsparkCoroutinesClient private constructor( timeoutSecs: Int = 60, signingPrivateKey: ByteArray? = null, senderIdentifier: String? = null, + idempotencyKey: String? = null, ): OutgoingPayment { requireValidAuth() @@ -425,6 +432,7 @@ class LightsparkCoroutinesClient private constructor( add("maximum_fees_msats", maxFeesMsats) amountMsats?.let { add("amount_msats", amountMsats) } senderHash?.let { add("sender_hash", senderHash) } + idempotencyKey?.let { add("idempotency_key", idempotencyKey) } }, signingNodeId = nodeId, ) { @@ -767,12 +775,16 @@ class LightsparkCoroutinesClient private constructor( * @param amountSats The amount of funds to withdraw in SATOSHI. * @param bitcoinAddress The Bitcoin address to withdraw funds to. * @param mode The mode to use for the withdrawal. See `WithdrawalMode` for more information. + * @param idempotencyKey An optional key to ensure idempotency of the withdrawal. If provided, the same result will + * be returned for the same idempotency key without triggering a new withdrawal. */ + @JvmOverloads suspend fun requestWithdrawal( nodeId: String, amountSats: Long, bitcoinAddress: String, mode: WithdrawalMode, + idempotencyKey: String? = null, ): WithdrawalRequest { requireValidAuth() return executeQuery( @@ -783,6 +795,7 @@ class LightsparkCoroutinesClient private constructor( add("amount_sats", amountSats) add("bitcoin_address", bitcoinAddress) add("withdrawal_mode", serializerFormat.encodeToJsonElement(mode)) + idempotencyKey?.let { add("idempotency_key", idempotencyKey) } }, signingNodeId = nodeId, ) { @@ -805,15 +818,19 @@ class LightsparkCoroutinesClient private constructor( * As guidance, a maximum fee of 15 basis points should make almost all transactions succeed. For example, * for a transaction between 10k sats and 100k sats, this would mean a fee limit of 15 to 150 sats. * @param timeoutSecs The timeout in seconds that we will try to make the payment. + * @param idempotencyKey An optional key to ensure idempotency of the payment. If provided, the same result will be + * returned for the same idempotency key without triggering a new payment. * @return An `OutgoingPayment` object if the payment was successful, or throws if the payment failed. * @throws LightsparkException if the payment failed. */ + @JvmOverloads suspend fun sendPayment( payerNodeId: String, destinationPublicKey: String, amountMsats: Long, maxFeesMsats: Long, timeoutSecs: Int = 60, + idempotencyKey: String? = null, ): OutgoingPayment { requireValidAuth() return executeQuery( @@ -825,6 +842,7 @@ class LightsparkCoroutinesClient private constructor( add("amount_msats", amountMsats) add("timeout_secs", timeoutSecs) add("maximum_fees_msats", maxFeesMsats) + idempotencyKey?.let { add("idempotency_key", idempotencyKey) } }, signingNodeId = payerNodeId, ) { 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 bc80e00e..f074de9d 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkSyncClient.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkSyncClient.kt @@ -200,6 +200,8 @@ class LightsparkSyncClient constructor(config: ClientConfig) { * for a transaction between 10k sats and 100k sats, this would mean a fee limit of 15 to 150 sats. * @param amountMsats The amount to pay in milli-satoshis. Defaults to the full amount of the invoice. * @param timeoutSecs The number of seconds to wait for the payment to complete. Defaults to 60. + * @param idempotencyKey An optional key to ensure idempotency of the payment. If provided, the same result will be + * returned for the same idempotency key without triggering a new payment. * @return The payment details. */ @JvmOverloads @@ -209,8 +211,18 @@ class LightsparkSyncClient constructor(config: ClientConfig) { maxFeesMsats: Long, amountMsats: Long? = null, timeoutSecs: Int = 60, + idempotencyKey: String? = null, ): OutgoingPayment = - runBlocking { asyncClient.payInvoice(nodeId, encodedInvoice, maxFeesMsats, amountMsats, timeoutSecs) } + runBlocking { + asyncClient.payInvoice( + nodeId, + encodedInvoice, + maxFeesMsats, + amountMsats, + timeoutSecs, + idempotencyKey, + ) + } /** * [payUmaInvoice] sends an UMA payment to a node on the Lightning Network, based on the invoice (as defined by the @@ -227,6 +239,8 @@ class LightsparkSyncClient constructor(config: ClientConfig) { * @param signingPrivateKey The sender's signing private key. Used to hash the sender identifier. * @param senderIdentifier Optional identifier of the sender. If provided, this will be hashed using a * monthly-rotated seed and used for anonymized analysis. + * @param idempotencyKey An optional key to ensure idempotency of the payment. If provided, the same result will be + * returned for the same idempotency key without triggering a new payment. * @return The payment details. */ @JvmOverloads @@ -239,6 +253,7 @@ class LightsparkSyncClient constructor(config: ClientConfig) { timeoutSecs: Int = 60, signingPrivateKey: ByteArray? = null, senderIdentifier: String? = null, + idempotencyKey: String? = null, ): OutgoingPayment = runBlocking { asyncClient.payUmaInvoice( @@ -249,6 +264,7 @@ class LightsparkSyncClient constructor(config: ClientConfig) { timeoutSecs, signingPrivateKey, senderIdentifier, + idempotencyKey, ) } @@ -416,14 +432,19 @@ class LightsparkSyncClient constructor(config: ClientConfig) { * @param amountSats The amount of funds to withdraw in SATOSHI. * @param bitcoinAddress The Bitcoin address to withdraw funds to. * @param mode The mode to use for the withdrawal. See `WithdrawalMode` for more information. + * @param idempotencyKey An optional key to ensure idempotency of the withdrawal. If provided, the same result will + * be returned for the same idempotency key without triggering a new withdrawal. */ + @JvmOverloads @Throws(LightsparkException::class, LightsparkAuthenticationException::class, CancellationException::class) fun requestWithdrawal( nodeId: String, amountSats: Long, bitcoinAddress: String, mode: WithdrawalMode, - ): WithdrawalRequest = runBlocking { asyncClient.requestWithdrawal(nodeId, amountSats, bitcoinAddress, mode) } + idempotencyKey: String? = null, + ): WithdrawalRequest = + runBlocking { asyncClient.requestWithdrawal(nodeId, amountSats, bitcoinAddress, mode, idempotencyKey) } /** * Sends a payment directly to a node on the Lightning Network through the public key of the node without an invoice. @@ -435,6 +456,8 @@ class LightsparkSyncClient constructor(config: ClientConfig) { * As guidance, a maximum fee of 15 basis points should make almost all transactions succeed. For example, * for a transaction between 10k sats and 100k sats, this would mean a fee limit of 15 to 150 sats. * @param timeoutSecs The timeout in seconds that we will try to make the payment. + * @param idempotencyKey An optional key to ensure idempotency of the payment. If provided, the same result will be + * returned for the same idempotency key without triggering a new payment. * @return An `OutgoingPayment` object if the payment was successful, or throws if the payment failed. * @throws LightsparkException if the payment failed. */ @@ -445,6 +468,7 @@ class LightsparkSyncClient constructor(config: ClientConfig) { amountMsats: Long, maxFeesMsats: Long, timeoutSecs: Int = 60, + idempotencyKey: String? = null, ): OutgoingPayment = runBlocking { asyncClient.sendPayment( payerNodeId, @@ -452,6 +476,7 @@ class LightsparkSyncClient constructor(config: ClientConfig) { amountMsats, maxFeesMsats, timeoutSecs, + idempotencyKey, ) } @@ -579,14 +604,14 @@ class LightsparkSyncClient constructor(config: ClientConfig) { /** * fetch outgoing payments for a given payment hash - * + * * @param paymentHash the payment hash of the invoice for which to fetch the outgoing payments * @param transactionStatuses the transaction statuses to filter the payments by. If null, all payments will be returned. */ @Throws(LightsparkException::class, LightsparkAuthenticationException::class, CancellationException::class) fun getOutgoingPaymentsForPaymentHash( paymentHash: String, - transactionStatuses: List? = null + transactionStatuses: List? = null, ): List = runBlocking { asyncClient.getOutgoingPaymentForPaymentHash(paymentHash, transactionStatuses) } @@ -594,14 +619,14 @@ class LightsparkSyncClient constructor(config: ClientConfig) { @Throws(LightsparkException::class, LightsparkAuthenticationException::class, CancellationException::class) fun getIncomingPaymentsForInvoice( invoiceId: String, - transactionStatuses: List? = null + transactionStatuses: List? = null, ): List = runBlocking { asyncClient.getIncomingPaymentsForInvoice(invoiceId, transactionStatuses) } @Throws(LightsparkException::class, LightsparkAuthenticationException::class, CancellationException::class) fun getInvoiceForPaymentHash( - paymentHash: String + paymentHash: String, ): Invoice = runBlocking { asyncClient.getInvoiceForPaymentHash(paymentHash) } @@ -627,7 +652,12 @@ class LightsparkSyncClient constructor(config: ClientConfig) { * @param inviterRegionCode The region of the inviter. * @return The invitation that was created. */ - @Throws(LightsparkException::class, LightsparkAuthenticationException::class, CancellationException::class, IllegalArgumentException::class) + @Throws( + LightsparkException::class, + LightsparkAuthenticationException::class, + CancellationException::class, + IllegalArgumentException::class, + ) fun createUmaInvitationWithIncentives( inviterUma: String, inviterPhoneNumberE164: String, @@ -659,7 +689,12 @@ class LightsparkSyncClient constructor(config: ClientConfig) { * @param inviteeRegionCode The region of the invitee. * @returns The invitation that was claimed. */ - @Throws(LightsparkException::class, LightsparkAuthenticationException::class, CancellationException::class, IllegalArgumentException::class) + @Throws( + LightsparkException::class, + LightsparkAuthenticationException::class, + CancellationException::class, + IllegalArgumentException::class, + ) fun claimUmaInvitationWithIncentives( invitationCode: String, inviteeUma: String, diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/PayInvoice.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/PayInvoice.kt index 3a45eccc..91e752c2 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/PayInvoice.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/PayInvoice.kt @@ -9,6 +9,7 @@ const val PayInvoiceMutation = """ ${'$'}timeout_secs: Int! ${'$'}maximum_fees_msats: Long! ${'$'}amount_msats: Long + ${'$'}idempotency_key: String ) { pay_invoice( input: { @@ -17,6 +18,7 @@ const val PayInvoiceMutation = """ timeout_secs: ${'$'}timeout_secs amount_msats: ${'$'}amount_msats maximum_fees_msats: ${'$'}maximum_fees_msats + idempotency_key: ${'$'}idempotency_key } ) { payment { diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/PayUmaInvoice.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/PayUmaInvoice.kt index 22ae6948..3078bee9 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/PayUmaInvoice.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/PayUmaInvoice.kt @@ -10,6 +10,7 @@ const val PayUmaInvoiceMutation = """ ${'$'}maximum_fees_msats: Long! ${'$'}amount_msats: Long ${'$'}sender_hash: String + ${'$'}idempotency_key: String ) { pay_uma_invoice( input: { @@ -19,6 +20,7 @@ const val PayUmaInvoiceMutation = """ amount_msats: ${'$'}amount_msats maximum_fees_msats: ${'$'}maximum_fees_msats sender_hash: ${'$'}sender_hash + idempotency_key: ${'$'}idempotency_key } ) { payment { diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/RequestWithdrawal.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/RequestWithdrawal.kt index a90c1ece..24ddcd8f 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/RequestWithdrawal.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/RequestWithdrawal.kt @@ -8,12 +8,14 @@ const val RequestWithdrawalMutation = """ ${'$'}amount_sats: Long! ${'$'}bitcoin_address: String! ${'$'}withdrawal_mode: WithdrawalMode! + ${'$'}idempotency_key: String ) { request_withdrawal(input: { node_id: ${'$'}node_id amount_sats: ${'$'}amount_sats bitcoin_address: ${'$'}bitcoin_address withdrawal_mode: ${'$'}withdrawal_mode + idempotency_key: ${'$'}idempotency_key }) { request { ...WithdrawalRequestFragment diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/SendPayment.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/SendPayment.kt index 07a732aa..0211e9bd 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/SendPayment.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/SendPayment.kt @@ -9,6 +9,7 @@ const val SendPaymentMutation = """ ${'$'}timeout_secs: Int! ${'$'}amount_msats: Long! ${'$'}maximum_fees_msats: Long! + ${'$'}idempotency_key: String ) { send_payment( input: { @@ -17,6 +18,7 @@ const val SendPaymentMutation = """ timeout_secs: ${'$'}timeout_secs amount_msats: ${'$'}amount_msats maximum_fees_msats: ${'$'}maximum_fees_msats + idempotency_key: ${'$'}idempotency_key } ) { payment { From 993f94de78083fc83d5791022b55c2bf10bab9c0 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Wed, 12 Feb 2025 12:43:55 -0800 Subject: [PATCH 18/31] Add the outgoing_payment_for_idempotency_key query. (#213) --- .../lightspark/sdk/LightsparkFuturesClient.kt | 12 ++++++ .../sdk/LightsparkCoroutinesClient.kt | 39 ++++++++++++++++--- .../lightspark/sdk/LightsparkSyncClient.kt | 12 ++++++ .../OutgoingPaymentForIdempotencyKey.kt | 20 ++++++++++ 4 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/OutgoingPaymentForIdempotencyKey.kt 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 f32a2ecb..de40f052 100644 --- a/lightspark-sdk/src/commonJvmAndroidMain/kotlin/com/lightspark/sdk/LightsparkFuturesClient.kt +++ b/lightspark-sdk/src/commonJvmAndroidMain/kotlin/com/lightspark/sdk/LightsparkFuturesClient.kt @@ -615,6 +615,18 @@ class LightsparkFuturesClient(config: ClientConfig) { coroutinesClient.getOutgoingPaymentForPaymentHash(paymentHash, transactionStatuses) } + /** + * Fetch outgoing payment for a given idempotency key + * + * @param idempotencyKey The idempotency key used when creating the payment. + */ + @Throws(LightsparkException::class, LightsparkAuthenticationException::class) + fun getOutgoingPaymentForIdempotencyKey( + idempotencyKey: String, + ): CompletableFuture = coroutineScope.future { + coroutinesClient.getOutgoingPaymentForIdempotencyKey(idempotencyKey) + } + /** * fetch invoice for a given payments hash * 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 82b673f1..e8b3abe3 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkCoroutinesClient.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkCoroutinesClient.kt @@ -743,7 +743,7 @@ class LightsparkCoroutinesClient private constructor( suspend fun fundNode( nodeId: String, amountSats: Long?, - fundingAddress: String? = null + fundingAddress: String? = null, ): CurrencyAmount { requireValidAuth() return executeQuery( @@ -752,7 +752,7 @@ class LightsparkCoroutinesClient private constructor( { add("node_id", nodeId) amountSats?.let { add("amount_sats", it) } - fundingAddress?.let { add("funding_address", it)} + fundingAddress?.let { add("funding_address", it) } }, signingNodeId = nodeId, ) { @@ -1062,7 +1062,7 @@ class LightsparkCoroutinesClient private constructor( /** * fetch outgoing payments for a given payment hash - * + * * @param paymentHash the payment hash of the invoice for which to fetch the outgoing payments * @param transactionStatuses the transaction statuses to filter the payments by. If null, all payments will be returned. */ @@ -1090,13 +1090,40 @@ class LightsparkCoroutinesClient private constructor( ) } + /** + * Fetch outgoing payment for a given idempotency key + * + * @param idempotencyKey The idempotency key used when creating the payment. + */ + suspend fun getOutgoingPaymentForIdempotencyKey( + idempotencyKey: String, + ): OutgoingPayment? { + requireValidAuth() + return executeQuery( + Query( + OutgoingPaymentForIdempotencyKeyQuery, + { + add("idempotency_key", idempotencyKey) + }, + ) { + val outputJson = + requireNotNull(it["outgoing_payment_for_idempotency_key"]) { "No payment output found in response" } + val paymentJson = outputJson.jsonObject["payment"] + if (paymentJson == null) { + return@Query null + } + serializerFormat.decodeFromJsonElement(paymentJson) + }, + ) + } + /** * fetch invoice for a given payment hash - * + * * @param paymentHash the payment hash of the invoice for which to fetch the outgoing payments */ suspend fun getInvoiceForPaymentHash( - paymentHash: String + paymentHash: String, ): Invoice { requireValidAuth() return executeQuery( @@ -1140,7 +1167,7 @@ class LightsparkCoroutinesClient private constructor( val paymentsJson = requireNotNull(outputJson.jsonObject["payments"]) { "No payments found in response" } serializerFormat.decodeFromJsonElement(paymentsJson) - } + }, ) } 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 f074de9d..481cdecb 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkSyncClient.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkSyncClient.kt @@ -616,6 +616,18 @@ class LightsparkSyncClient constructor(config: ClientConfig) { asyncClient.getOutgoingPaymentForPaymentHash(paymentHash, transactionStatuses) } + /** + * Fetch outgoing payment for a given idempotency key + * + * @param idempotencyKey The idempotency key used when creating the payment. + */ + @Throws(LightsparkException::class, LightsparkAuthenticationException::class, CancellationException::class) + fun getOutgoingPaymentForIdempotencyKey( + idempotencyKey: String, + ): OutgoingPayment? = runBlocking { + asyncClient.getOutgoingPaymentForIdempotencyKey(idempotencyKey) + } + @Throws(LightsparkException::class, LightsparkAuthenticationException::class, CancellationException::class) fun getIncomingPaymentsForInvoice( invoiceId: String, 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 new file mode 100644 index 00000000..13e68ccd --- /dev/null +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/OutgoingPaymentForIdempotencyKey.kt @@ -0,0 +1,20 @@ +package com.lightspark.sdk.graphql + +import com.lightspark.sdk.model.OutgoingPayment + +const val OutgoingPaymentForIdempotencyKeyQuery = """ +query OutgoingPaymentForIdempotencyKey( + ${'$'}idempotency_key: String! +) { + outgoing_payment_for_idempotency_key(input: { + idempotency_key: ${'$'}idempotency_key, + statuses: ${'$'}transactionStatuses + }) { + payment { + ...OutgoingPaymentFragment + } + } +} + + ${OutgoingPayment.FRAGMENT} +""" From e0e32f74f07568dd263fc87d2cf91618dbcd5030 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 12 Feb 2025 12:50:56 -0800 Subject: [PATCH 19/31] Merge release/lightspark-sdk-v0.19.0 into develop (#215) This PR was automatically created by the release branch cut workflow.\nSDK=lightspark-sdk\nVERSION=0.19.0 Co-authored-by: runner --- lightspark-sdk/README.md | 4 ++-- lightspark-sdk/gradle.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lightspark-sdk/README.md b/lightspark-sdk/README.md index 3076690a..e2d84790 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.18.0" + implementation "com.lightspark:lightspark-sdk:0.19.0" } ``` or with **build.gradle.kts:** ```kotlin dependencies { - implementation("com.lightspark:lightspark-sdk:0.18.0") + implementation("com.lightspark:lightspark-sdk:0.19.0") } ``` diff --git a/lightspark-sdk/gradle.properties b/lightspark-sdk/gradle.properties index ec5ef4a8..27033c36 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.18.0 +VERSION_NAME=0.19.0 POM_DESCRIPTION=The Lightspark API SDK for Kotlin and Java. POM_INCEPTION_YEAR=2023 From bc4d70c522d090a75af86363654bd6621794a41b Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Wed, 12 Feb 2025 18:07:26 -0800 Subject: [PATCH 20/31] Remove an extra param for outgoing_payment_for_idempotency_key (#219) This was accidental copy-pasta. --- .../lightspark/sdk/graphql/OutgoingPaymentForIdempotencyKey.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 From 2a67dec8b582b6ffac06e8f2d2f39ac02976fd4f Mon Sep 17 00:00:00 2001 From: runner Date: Thu, 13 Feb 2025 21:01:09 +0000 Subject: [PATCH 21/31] Bump lightspark-sdk to version 0.19.1 --- lightspark-sdk/README.md | 4 ++-- lightspark-sdk/gradle.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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 From aca333f18cbc50c6f23f70e46dd9258e35adafc8 Mon Sep 17 00:00:00 2001 From: Shreya Vissamsetti Date: Tue, 18 Mar 2025 14:16:56 -0700 Subject: [PATCH 22/31] Fix non uma lnurl path (#224) --- umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt index 48db747c..5dcb26e2 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt @@ -494,7 +494,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)) } From 98a9dd890ea44790e56e394c7a87aa3e0d2c25c2 Mon Sep 17 00:00:00 2001 From: Shreya Vissamsetti Date: Fri, 21 Mar 2025 11:44:03 -0700 Subject: [PATCH 23/31] Bump sdk version, use standard errors in UMA demo vasp (#226) --- gradle/libs.versions.toml | 2 +- umaserverdemo/build.gradle.kts | 1 + .../kotlin/com/lightspark/ReceivingVasp.kt | 256 +++++++-------- .../main/kotlin/com/lightspark/SendingVasp.kt | 306 ++++++++---------- .../com/lightspark/SendingVaspRequestCache.kt | 2 +- .../kotlin/com/lightspark/plugins/Routing.kt | 53 ++- 6 files changed, 293 insertions(+), 327 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8bab457e..485c1f59 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ ktlint = "11.3.1" ktor = "2.3.7" lightsparkCore = "0.6.0" lightsparkCrypto = "0.6.0" -uma = "1.3.0" +uma = "1.5.0" mavenPublish = "0.25.2" mockitoCore = "5.5.0" taskTree = "2.1.1" 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 5dcb26e2..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()) @@ -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") From 6009d01aa40537bcadcc963689fdd966abc41315 Mon Sep 17 00:00:00 2001 From: Aaryaman Bhute Date: Tue, 25 Mar 2025 09:15:56 -0700 Subject: [PATCH 24/31] Added paymentHash and preimageNonce params to createInvoice, createLnurlInvoice, and createUmaInvoice and their respective mutations from all 3 clients. --- .../lightspark/sdk/LightsparkFuturesClient.kt | 28 ++++++++++++++++++- .../sdk/LightsparkCoroutinesClient.kt | 18 ++++++++++++ .../lightspark/sdk/LightsparkSyncClient.kt | 28 ++++++++++++++++++- .../lightspark/sdk/graphql/CreateInvoice.kt | 13 ++++++++- .../sdk/graphql/CreateLnurlInvoice.kt | 6 +++- .../sdk/graphql/CreateUmaInvoice.kt | 4 +++ 6 files changed, 93 insertions(+), 4 deletions(-) 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..5696b30e 100644 --- a/lightspark-sdk/src/commonJvmAndroidMain/kotlin/com/lightspark/sdk/LightsparkFuturesClient.kt +++ b/lightspark-sdk/src/commonJvmAndroidMain/kotlin/com/lightspark/sdk/LightsparkFuturesClient.kt @@ -126,6 +126,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 +136,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 +160,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 +169,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 +178,8 @@ class LightsparkFuturesClient(config: ClientConfig) { amountMsats, metadata, expirySecs, + paymentHash, + preimageNonce, ) } @@ -175,6 +195,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 +207,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 +218,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..1f7b7866 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkCoroutinesClient.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkCoroutinesClient.kt @@ -182,6 +182,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 +191,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 +204,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 +226,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 +251,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 +276,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 +287,8 @@ class LightsparkCoroutinesClient private constructor( expirySecs: Int? = null, signingPrivateKey: ByteArray? = null, receiverIdentifier: String? = null, + paymentHash: String? = null, + preimageNonce: String? = null, ): Invoice { requireValidAuth() @@ -296,6 +312,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..6f1a1782 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkSyncClient.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkSyncClient.kt @@ -108,6 +108,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 +118,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 +141,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 +150,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 +175,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 +187,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 +197,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 { From 01cf64112535c3b48af3be937602869a3ea478eb Mon Sep 17 00:00:00 2001 From: Aaryaman Bhute Date: Wed, 26 Mar 2025 12:35:50 -0700 Subject: [PATCH 25/31] Added releasePaymentPreimage functions to all 3 kotlin clients --- .../lightspark/sdk/LightsparkFuturesClient.kt | 18 ++++++++++++++ .../sdk/LightsparkCoroutinesClient.kt | 24 +++++++++++++++++++ .../lightspark/sdk/LightsparkSyncClient.kt | 16 +++++++++++++ 3 files changed, 58 insertions(+) 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..be6e599a 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. * 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..ea36225a 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. * 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..ccd60294 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. * From b0c071555d2cce9c251ffaa57c1e32f37f6cfef6 Mon Sep 17 00:00:00 2001 From: Alex Weil Date: Tue, 6 May 2025 15:51:56 -0700 Subject: [PATCH 26/31] Update Gradle version --- androidwalletdemo/build.gradle.kts | 1 - core/build.gradle.kts | 1 - crypto/build.gradle.kts | 1 - gradle/wrapper/gradle-wrapper.properties | 2 +- lightspark-sdk/build.gradle.kts | 1 - oauth/build.gradle.kts | 1 - wallet-sdk/build.gradle.kts | 1 - 7 files changed, 1 insertion(+), 7 deletions(-) diff --git a/androidwalletdemo/build.gradle.kts b/androidwalletdemo/build.gradle.kts index 709e113f..eb7fa5bb 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") diff --git a/core/build.gradle.kts b/core/build.gradle.kts index a8613771..e9508c05 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) 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/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/build.gradle.kts b/lightspark-sdk/build.gradle.kts index 64cda832..f9cc38f3 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) 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/wallet-sdk/build.gradle.kts b/wallet-sdk/build.gradle.kts index db59859f..15afdde5 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) From 0020f47488cb051d48bc3d864fb57405cbbce233 Mon Sep 17 00:00:00 2001 From: Joel Weinberger Date: Mon, 16 Jun 2025 22:10:42 -0700 Subject: [PATCH 27/31] Fix misnamed buckets ldev.web-dev -> lsdev.web-dev --- core/build.gradle.kts | 2 +- lightspark-sdk/build.gradle.kts | 2 +- wallet-sdk/build.gradle.kts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index e9508c05..db7eb36a 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -133,7 +133,7 @@ s3 { tasks.register("uploadDocsToS3") { group = "documentation" dependsOn("generateSdkDocs") - bucket = "ldev.web-dev" + bucket = "lsdev.web-dev" keyPrefix = "docs/kotlin" sourceDir = "docs/html" } diff --git a/lightspark-sdk/build.gradle.kts b/lightspark-sdk/build.gradle.kts index f9cc38f3..9c2e8d93 100644 --- a/lightspark-sdk/build.gradle.kts +++ b/lightspark-sdk/build.gradle.kts @@ -143,7 +143,7 @@ s3 { tasks.register("uploadDocsToS3") { group = "documentation" dependsOn("generateSdkDocs") - bucket = "ldev.web-dev" + bucket = "lsdev.web-dev" keyPrefix = "docs/kotlin" sourceDir = "docs/html" } diff --git a/wallet-sdk/build.gradle.kts b/wallet-sdk/build.gradle.kts index 15afdde5..535240af 100644 --- a/wallet-sdk/build.gradle.kts +++ b/wallet-sdk/build.gradle.kts @@ -152,7 +152,7 @@ s3 { tasks.register("uploadDocsToS3") { group = "documentation" dependsOn("generateSdkDocs") - bucket = "ldev.web-dev" + bucket = "lsdev.web-dev" keyPrefix = "docs/kotlin" sourceDir = "docs/html" } From a467a3c4e3772514a66513d7ae60f8db6bd9c214 Mon Sep 17 00:00:00 2001 From: Joel Weinberger Date: Mon, 16 Jun 2025 22:20:14 -0700 Subject: [PATCH 28/31] Rename to lightspark-dev-web.web-dev --- core/build.gradle.kts | 4 ++-- lightspark-sdk/build.gradle.kts | 4 ++-- wallet-sdk/build.gradle.kts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index db7eb36a..d2d14f1f 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -126,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 = "lsdev.web-dev" + bucket = "lightspark-dev-web" keyPrefix = "docs/kotlin" sourceDir = "docs/html" } diff --git a/lightspark-sdk/build.gradle.kts b/lightspark-sdk/build.gradle.kts index 9c2e8d93..942d3926 100644 --- a/lightspark-sdk/build.gradle.kts +++ b/lightspark-sdk/build.gradle.kts @@ -136,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 = "lsdev.web-dev" + bucket = "lightspark-dev-web" keyPrefix = "docs/kotlin" sourceDir = "docs/html" } diff --git a/wallet-sdk/build.gradle.kts b/wallet-sdk/build.gradle.kts index 535240af..d986cbb6 100644 --- a/wallet-sdk/build.gradle.kts +++ b/wallet-sdk/build.gradle.kts @@ -145,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 = "lsdev.web-dev" + bucket = "lightspark-dev-web" keyPrefix = "docs/kotlin" sourceDir = "docs/html" } From d25b3b49842e84c4abbb1bd5c664018deb4ac4e1 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Fri, 20 Jun 2025 19:16:56 -0700 Subject: [PATCH 29/31] Switch to CENTRAL_PORTAL for maven publishing (#235) --- androidwalletdemo/build.gradle.kts | 2 +- build.gradle.kts | 3 ++- gradle/libs.versions.toml | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/androidwalletdemo/build.gradle.kts b/androidwalletdemo/build.gradle.kts index eb7fa5bb..7df87504 100644 --- a/androidwalletdemo/build.gradle.kts +++ b/androidwalletdemo/build.gradle.kts @@ -19,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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 485c1f59..db5729a1 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,7 +12,7 @@ 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" @@ -22,7 +22,7 @@ ktor = "2.3.7" lightsparkCore = "0.6.0" lightsparkCrypto = "0.6.0" uma = "1.5.0" -mavenPublish = "0.25.2" +mavenPublish = "0.32.0" mockitoCore = "5.5.0" taskTree = "2.1.1" From b81bf8dabddf1d790f40744e4ddbda6d019ad042 Mon Sep 17 00:00:00 2001 From: runner Date: Sat, 21 Jun 2025 02:23:10 +0000 Subject: [PATCH 30/31] Bump core to version 0.6.1 --- core/gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index db5729a1..252f566f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ 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.5.0" mavenPublish = "0.32.0" From 090f80809be10bb7865bd439886dc5080c28e175 Mon Sep 17 00:00:00 2001 From: runner Date: Mon, 23 Jun 2025 16:48:21 +0000 Subject: [PATCH 31/31] Bump lightspark-sdk to version 0.20.0 --- lightspark-sdk/README.md | 4 ++-- lightspark-sdk/gradle.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lightspark-sdk/README.md b/lightspark-sdk/README.md index 14577555..308e846a 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.1" + implementation "com.lightspark:lightspark-sdk:0.20.0" } ``` or with **build.gradle.kts:** ```kotlin dependencies { - implementation("com.lightspark:lightspark-sdk:0.19.1") + implementation("com.lightspark:lightspark-sdk:0.20.0") } ``` diff --git a/lightspark-sdk/gradle.properties b/lightspark-sdk/gradle.properties index 399428d5..93bbdec8 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.1 +VERSION_NAME=0.20.0 POM_DESCRIPTION=The Lightspark API SDK for Kotlin and Java. POM_INCEPTION_YEAR=2023