From adbbca98bb9ee6ded8a08f3e6c0a686aea716c54 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 9 Sep 2024 11:25:23 -0700 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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 {