diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/uma/LnurlpRequest.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/uma/LnurlpRequest.kt index e2e0523d..fa172b8d 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/uma/LnurlpRequest.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/uma/LnurlpRequest.kt @@ -54,13 +54,13 @@ data class LnurlpRequest( if (urlBuilder.protocol != URLProtocol.HTTP && urlBuilder.protocol != URLProtocol.HTTPS) { throw IllegalArgumentException("Invalid URL schema: $url") } - if (urlBuilder.pathSegments.size != 3 - || urlBuilder.pathSegments[0] != ".well-known" - || urlBuilder.pathSegments[1] != "lnurlp" + if (urlBuilder.pathSegments.size != 4 + || urlBuilder.pathSegments[1] != ".well-known" + || urlBuilder.pathSegments[2] != "lnurlp" ) { throw IllegalArgumentException("Invalid uma request path: $url") } - val receiverAddress = "${urlBuilder.host}@${urlBuilder.pathSegments[2]}" + val receiverAddress = "${urlBuilder.host}@${urlBuilder.pathSegments[3]}" val vaspDomain = urlBuilder.parameters["vaspDomain"] val nonce = urlBuilder.parameters["nonce"] val signature = urlBuilder.parameters["signature"] diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/uma/LnurlpResponse.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/uma/LnurlpResponse.kt index dd3b1196..fbcd38d1 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/uma/LnurlpResponse.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/uma/LnurlpResponse.kt @@ -5,7 +5,6 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.Json @@ -47,7 +46,7 @@ data class PayerDataOptions( // Custom serializer for PayerDataOptions class PayerDataOptionsSerializer : KSerializer { - override val descriptor = PrimitiveSerialDescriptor("PayerDataOptions", PrimitiveKind.STRING) + override val descriptor = PrimitiveSerialDescriptor("payerData", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: PayerDataOptions) { val jsonOutput = """{ @@ -60,28 +59,14 @@ class PayerDataOptionsSerializer : KSerializer { } override fun deserialize(decoder: Decoder): PayerDataOptions { - val compositeInput = decoder.beginStructure(descriptor) - var nameRequired = false - var emailRequired = false - var complianceRequired = false - loop@ while (true) { - when (val index = compositeInput.decodeElementIndex(descriptor)) { - CompositeDecoder.Companion.DECODE_DONE -> break@loop - 0 -> { - val jsonInput = compositeInput.decodeStringElement(descriptor, index) - val json = Json.parseToJsonElement(jsonInput) - val name = json.jsonObject["name"]?.jsonObject - val email = json.jsonObject["email"]?.jsonObject - val compliance = json.jsonObject["compliance"]?.jsonObject - nameRequired = name?.get("mandatory")?.jsonPrimitive?.boolean ?: false - emailRequired = email?.get("mandatory")?.jsonPrimitive?.boolean ?: false - complianceRequired = compliance?.get("mandatory")?.jsonPrimitive?.boolean ?: false - } - - else -> throw IllegalArgumentException("Invalid index $index") - } - } - compositeInput.endStructure(descriptor) + val jsonInput = decoder.decodeString() + val json = Json.parseToJsonElement(jsonInput) + val name = json.jsonObject["name"]?.jsonObject + val email = json.jsonObject["email"]?.jsonObject + val compliance = json.jsonObject["compliance"]?.jsonObject + val nameRequired = name?.get("mandatory")?.jsonPrimitive?.boolean ?: false + val emailRequired = email?.get("mandatory")?.jsonPrimitive?.boolean ?: false + val complianceRequired = compliance?.get("mandatory")?.jsonPrimitive?.boolean ?: false return PayerDataOptions(nameRequired, emailRequired, complianceRequired) } } diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/uma/UmaProtocolHelper.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/uma/UmaProtocolHelper.kt index e1a0b324..95ee8fd6 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/uma/UmaProtocolHelper.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/uma/UmaProtocolHelper.kt @@ -28,7 +28,8 @@ class UmaProtocolHelper( return cached } - val response = umaRequester.makeGetRequest("https://$vaspDomain/.well-known/uma-public-key") + val scheme = if (vaspDomain.startsWith("localhost:")) "http" else "https" + val response = umaRequester.makeGetRequest("$scheme://$vaspDomain/.well-known/lnurlpubkey") val pubKeyResponse = serializerFormat.decodeFromString(response) publicKeyCache.addPublicKeysForVasp(vaspDomain, pubKeyResponse) return pubKeyResponse diff --git a/umaserverdemo/build.gradle.kts b/umaserverdemo/build.gradle.kts index 42d521c1..cbafadd6 100644 --- a/umaserverdemo/build.gradle.kts +++ b/umaserverdemo/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation("io.ktor:ktor-server-default-headers-jvm") implementation("io.ktor:ktor-server-call-logging-jvm") implementation("io.ktor:ktor-server-content-negotiation-jvm") + implementation("io.ktor:ktor-client-content-negotiation-jvm") implementation("io.ktor:ktor-serialization-kotlinx-json-jvm") implementation("io.ktor:ktor-server-auth-jvm") implementation("io.ktor:ktor-server-compression-jvm") diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/Logging.kt b/umaserverdemo/src/main/kotlin/com/lightspark/Logging.kt new file mode 100644 index 00000000..2de840dc --- /dev/null +++ b/umaserverdemo/src/main/kotlin/com/lightspark/Logging.kt @@ -0,0 +1,7 @@ +package com.lightspark + +import io.ktor.server.application.ApplicationCall + +fun ApplicationCall.debugLog(message: String) { + application.environment.log.debug(message) +} diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/PubKeyHandler.kt b/umaserverdemo/src/main/kotlin/com/lightspark/PubKeyHandler.kt new file mode 100644 index 00000000..d4aac915 --- /dev/null +++ b/umaserverdemo/src/main/kotlin/com/lightspark/PubKeyHandler.kt @@ -0,0 +1,19 @@ +package com.lightspark + +import com.lightspark.sdk.uma.PubKeyResponse +import io.ktor.server.application.ApplicationCall +import io.ktor.server.response.respond + +suspend fun handlePubKeyRequest(call: ApplicationCall, config: UmaConfig): String { + val twoWeeksFromNowMs = System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 14 + + val response = PubKeyResponse( + signingPubKey = config.umaSigningPubKey, + encryptionPubKey = config.umaEncryptionPubKey, + expirationTimestamp = twoWeeksFromNowMs / 1000, + ) + + call.respond(response) + + return "OK" +} diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/Vasp1.kt b/umaserverdemo/src/main/kotlin/com/lightspark/Vasp1.kt new file mode 100644 index 00000000..a832b86f --- /dev/null +++ b/umaserverdemo/src/main/kotlin/com/lightspark/Vasp1.kt @@ -0,0 +1,330 @@ +package com.lightspark + +import com.lightspark.sdk.LightsparkCoroutinesClient +import com.lightspark.sdk.execute +import com.lightspark.sdk.model.OutgoingPayment +import com.lightspark.sdk.model.TransactionStatus +import com.lightspark.sdk.uma.LnurlpResponse +import com.lightspark.sdk.uma.PayReqResponse +import com.lightspark.sdk.uma.PayerDataOptions +import com.lightspark.sdk.uma.UmaProtocolHelper +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.post +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 +import io.ktor.server.request.host +import io.ktor.server.response.respond +import io.ktor.server.routing.Routing +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import kotlinx.coroutines.delay +import kotlinx.datetime.Clock +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray + +class Vasp1( + private val config: UmaConfig, + private val uma: UmaProtocolHelper, + private val lightsparkClient: LightsparkCoroutinesClient, +) { + private val httpClient = HttpClient { + install(ContentNegotiation) { + json( + Json { + isLenient = true + }, + ) + } + } + private val requestDataCache = Vasp1RequestCache() + + 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." + } + + val addressParts = receiverAddress.split("@") + if (addressParts.size != 2) { + call.respond(HttpStatusCode.BadRequest, "Invalid receiver address.") + return "Invalid receiver address." + } + val receiverId = addressParts[0] + val receiverVasp = addressParts[1] + val signingKey = config.umaSigningPrivKey + + val lnurlpRequest = uma.getSignedLnurlpRequestUrl( + signingPrivateKey = signingKey, + receiverAddress = receiverAddress, + // TODO: This should be configurable. + senderVaspDomain = "localhost:8080", + isSubjectToTravelRule = true, + ) + + val response = try { + httpClient.get(lnurlpRequest) + } catch (e: Exception) { + call.respond(HttpStatusCode.FailedDependency, "Failed to fetch lnurlp response.") + return "Failed to fetch lnurlp response." + } + + if (response.status != HttpStatusCode.OK) { + call.respond(HttpStatusCode.FailedDependency, "Failed to fetch lnurlp response. Status: ${response.status}") + return "Failed to fetch lnurlp response." + } + + val lnurlpResponse = try { + response.body() + } catch (e: Exception) { + call.application.environment.log.error("Failed to parse lnurlp response", e) + call.respond(HttpStatusCode.FailedDependency, "Failed to parse lnurlp response.") + return "Failed to parse lnurlp response." + } + + val vasp2PubKey = 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." + } + + try { + uma.verifyLnurlpResponseSignature(lnurlpResponse, vasp2PubKey) + } catch (e: Exception) { + call.respond(HttpStatusCode.BadRequest, "Failed to verify lnurlp response signature.") + return "Failed to verify lnurlp response signature." + } + + val callbackUuid = requestDataCache.saveLnurlpResponseData(lnurlpResponse, receiverId, receiverVasp) + + call.respond( + buildJsonObject { + putJsonArray("currencies") { + addAll(lnurlpResponse.currencies.map { Json.encodeToJsonElement(it) }) + } + put("minSendSats", lnurlpResponse.minSendable) + put("maxSendSats", lnurlpResponse.maxSendable) + put("callbackUuid", callbackUuid) + // You might not actually send this to a client in practice. + put("isReceiverKYCd", lnurlpResponse.compliance.isKYCd) + } + ) + + return "OK" + } + + 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." + } + val initialRequestData = requestDataCache.getLnurlpResponseData(callbackUuid) ?: run { + call.respond(HttpStatusCode.BadRequest, "Callback UUID not found.") + return "Callback UUID not found." + } + + 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." + } + + val currencyCode = call.request.queryParameters["currencyCode"] + if (currencyCode == null) { + call.respond(HttpStatusCode.BadRequest, "Currency code not provided.") + return "Currency code not provided." + } + val currencyValid = initialRequestData.lnurlpResponse.currencies.any { it.code == currencyCode } + if (!currencyValid) { + call.respond(HttpStatusCode.BadRequest, "Currency code not supported.") + return "Currency code not supported." + } + + val vasp2PubKeys = try { + uma.fetchPublicKeysForVasp(initialRequestData.vasp2Domain) + } 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." + } + + val payer = getPayerProfile(initialRequestData.lnurlpResponse.requiredPayerData) + 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 { + uma.getPayRequest( + receiverEncryptionPubKey = vasp2PubKeys.encryptionPubKey, + sendingVaspPrivateKey = config.umaSigningPrivKey, + currencyCode = currencyCode, + amount = amount, + payerIdentifier = payer.identifier, + isPayerKYCd = true, + utxoCallback = getUtxoCallback(call, "1234abc"), + travelRuleInfo = trInfo, + payerUtxos = payerUtxos, + payerName = payer.name, + payerEmail = payer.email, + ) + } 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 response = httpClient.post(initialRequestData.lnurlpResponse.callback) { + contentType(ContentType.Application.Json) + setBody(payReq) + } + + if (response.status != HttpStatusCode.OK) { + call.respond(HttpStatusCode.InternalServerError, "Payreq to vasp2 failed: ${response.status}") + return "Payreq to vasp2 failed: ${response.status}" + } + + val payReqResponse = try { + response.body() + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, "Failed to parse payreq response.") + return "Failed to parse payreq response." + } + + // TODO(Yun): Pre-screen the UTXOs from payreqResponse.compliance.ytxos + + 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, + ) + + call.respond( + buildJsonObject { + put("encodedInvoice", payReqResponse.encodedInvoice) + put("callbackUuid", newCallbackId) + put("amount", Json.encodeToJsonElement(invoice.amount)) + put("conversionRate", payReqResponse.paymentInfo.multiplier) + put("currencyCode", payReqResponse.paymentInfo.currencyCode) + }, + ) + + return "OK" + } + + /** + * NOTE: In a real application, you'd want to use the authentication context to pull out this information. It's not + * actually always Alice sending the money ;-). + */ + private fun getPayerProfile(requiredPayerData: PayerDataOptions) = PayerProfile( + name = if (requiredPayerData.nameRequired) "Alice FakeName" else null, + email = if (requiredPayerData.emailRequired) "alice@vasp1.com" else null, + identifier = "alice", + ) + + private fun getUtxoCallback(call: ApplicationCall, txId: String): String { + val protocol = call.request.origin.scheme + val host = call.request.host() + val path = "/api/uma/utxoCallback?txId=${config.userID}" + return "$protocol://$host$path" + } + + 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." + } + val payReqData = requestDataCache.getPayReqData(callbackUuid) ?: run { + call.respond(HttpStatusCode.BadRequest, "Callback UUID not found.") + return "Callback UUID not found." + } + + if (payReqData.invoiceData.expiresAt < Clock.System.now()) { + call.respond(HttpStatusCode.BadRequest, "Invoice expired.") + return "Invoice expired." + } + + if (payReqData.invoiceData.amount.originalValue <= 0) { + call.respond(HttpStatusCode.BadRequest, "Invoice amount invalid. Uma requires positive amounts.") + return "Invoice amount invalid." + } + + val payment = try { + val pendingPayment = lightsparkClient.payInvoice( + config.nodeID, + payReqData.encodedInvoice, + maxFeesMsats = 1_000_000L, + ) + 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." + } + + call.respond( + buildJsonObject { + put("didSucceed", (payment.status == TransactionStatus.SUCCESS)) + put("paymentId", payment.id) + }, + ) + + return "OK" + } + + // TODO(Jeremy): Expose payInvoiceAndAwaitCompletion in the lightspark-sdk instead. + private suspend fun waitForPaymentCompletion(pendingPayment: OutgoingPayment): OutgoingPayment { + var attemptsLeft = 40 + var payment = pendingPayment + while (payment.status == TransactionStatus.PENDING && attemptsLeft-- > 0) { + delay(250) + payment = OutgoingPayment.getOutgoingPaymentQuery(payment.id).execute(lightsparkClient) + ?: throw Exception("Payment not found.") + } + if (payment.status == TransactionStatus.PENDING) { + throw Exception("Payment timed out.") + } + return payment + } +} + +fun Routing.registerVasp1Routes(vasp1: Vasp1) { + get("/api/umalookup/{receiver}") { + call.debugLog(vasp1.handleClientUmaLookup(call)) + } + + get("/api/umapayreq/{callbackUuid}") { + call.debugLog(vasp1.handleClientUmaPayReq(call)) + } + + post("/api/sendpayment/{callbackUuid}") { + call.debugLog(vasp1.handleClientSendPayment(call)) + } +} + +private data class PayerProfile( + val name: String?, + val email: String?, + val identifier: String, +) diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/Vasp1RequestCache.kt b/umaserverdemo/src/main/kotlin/com/lightspark/Vasp1RequestCache.kt new file mode 100644 index 00000000..7e1f0e78 --- /dev/null +++ b/umaserverdemo/src/main/kotlin/com/lightspark/Vasp1RequestCache.kt @@ -0,0 +1,67 @@ +package com.lightspark + +import com.lightspark.sdk.model.InvoiceData +import com.lightspark.sdk.uma.LnurlpResponse +import java.util.UUID + +/** + * 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 { + /** + * 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() + + /** + * 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() + + fun getLnurlpResponseData(uuid: String): Vasp1InitialRequestData? { + return lnurlpRequestCache[uuid] + } + + fun getPayReqData(uuid: String): Vasp1PayReqData? { + return payReqCache[uuid] + } + + fun saveLnurlpResponseData(lnurlpResponse: LnurlpResponse, receiverId: String, vasp2Domain: String): String { + val uuid = UUID.randomUUID().toString() + lnurlpRequestCache[uuid] = Vasp1InitialRequestData(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) + return uuid + } + + fun removeLnurlpResponseData(uuid: String) { + lnurlpRequestCache.remove(uuid) + } + + fun removePayReqData(uuid: String) { + payReqCache.remove(uuid) + } +} + +data class Vasp1InitialRequestData( + val lnurlpResponse: LnurlpResponse, + val receiverId: String, + val vasp2Domain: String, +) + +data class Vasp1PayReqData( + val encodedInvoice: String, + val utxoCallback: String, + val invoiceData: InvoiceData, +) + diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/Vasp2.kt b/umaserverdemo/src/main/kotlin/com/lightspark/Vasp2.kt index 39ea22a1..de673177 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/Vasp2.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/Vasp2.kt @@ -2,21 +2,25 @@ package com.lightspark import com.lightspark.sdk.ClientConfig import com.lightspark.sdk.LightsparkCoroutinesClient +import com.lightspark.sdk.auth.AccountApiTokenAuthProvider import com.lightspark.sdk.model.Invoice import com.lightspark.sdk.uma.Currency import com.lightspark.sdk.uma.LnurlInvoiceCreator import com.lightspark.sdk.uma.PayRequest import com.lightspark.sdk.uma.PayerDataOptions -import com.lightspark.sdk.uma.PubKeyResponse import com.lightspark.sdk.uma.UmaProtocolHelper import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.call import io.ktor.server.plugins.origin import io.ktor.server.request.ApplicationRequest import io.ktor.server.request.host import io.ktor.server.request.receive import io.ktor.server.request.uri import io.ktor.server.response.respond +import io.ktor.server.routing.Routing +import io.ktor.server.routing.get +import io.ktor.server.routing.post import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -41,7 +45,8 @@ class Vasp2( if (uma.isUmaLnurlpQuery(requestUrl)) { return handleUmaLnurlp(call) } - return "Hello World!" + + return "OK" } private suspend fun handleUmaLnurlp(call: ApplicationCall): String { @@ -120,7 +125,10 @@ class Vasp2( } val client = LightsparkCoroutinesClient( - ClientConfig(serverUrl = config.clientBaseURL ?: "api.lightspark.com"), + ClientConfig( + serverUrl = config.clientBaseURL ?: "api.lightspark.com", + authProvider = AccountApiTokenAuthProvider(config.apiClientID, config.apiClientSecret) + ), ) val invoice = try { client.createLnurlInvoice(config.nodeID, amountMsats, getEncodedMetadata()) @@ -161,7 +169,10 @@ class Vasp2( val conversionRate = 34_150L // In real life, this would come from some actual exchange rate API. val client = LightsparkCoroutinesClient( - ClientConfig(serverUrl = config.clientBaseURL ?: "api.lightspark.com"), + ClientConfig( + serverUrl = config.clientBaseURL ?: "api.lightspark.com", + authProvider = AccountApiTokenAuthProvider(config.apiClientID, config.apiClientSecret) + ), ) val response = try { @@ -181,6 +192,7 @@ class Vasp2( utxoCallback = getUtxoCallback(call, "1234"), ) } 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." } @@ -190,20 +202,6 @@ class Vasp2( return "OK" } - suspend fun handlePubKeyRequest(call: ApplicationCall): String { - val twoWeeksFromNowMs = System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 14 - - val response = PubKeyResponse( - signingPubKey = config.umaSigningPubKey, - encryptionPubKey = config.umaEncryptionPubKey, - expirationTimestamp = twoWeeksFromNowMs / 1000, - ) - - call.respond(response) - - return "OK" - } - private fun getEncodedMetadata(): String { val metadata = mapOf( "text/plain" to "Pay to ${config.username}@vasp2.com", @@ -214,9 +212,11 @@ class Vasp2( private fun getLnurlpCallback(call: ApplicationCall): String { val protocol = call.request.origin.scheme + val port = call.request.origin.localPort + val portString = if (port == 80 || port == 443) "" else ":$port" val host = call.request.host() val path = "/api/uma/payreq/${config.userID}" - return "$protocol://$host$path" + return "$protocol://$host$portString$path" } private fun getUtxoCallback(call: ApplicationCall, txId: String): String { @@ -233,3 +233,17 @@ class Vasp2( return "$protocol://$host$path" } } + +fun Routing.registerVasp2Routes(vasp2: Vasp2) { + get("/.well-known/lnurlp/{username}") { + call.debugLog(vasp2.handleLnurlp(call)) + } + + get("/api/uma/payreq/{uuid}") { + call.debugLog(vasp2.handleLnurlPayreq(call)) + } + + post("/api/uma/payreq/{uuid}") { + call.debugLog(vasp2.handleUmaPayreq(call)) + } +} diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt b/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt index b048b0c5..56c296cf 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt @@ -1,60 +1,40 @@ package com.lightspark.plugins import com.lightspark.UmaConfig +import com.lightspark.Vasp1 import com.lightspark.Vasp2 +import com.lightspark.debugLog +import com.lightspark.handlePubKeyRequest +import com.lightspark.registerVasp1Routes +import com.lightspark.registerVasp2Routes +import com.lightspark.sdk.ClientConfig +import com.lightspark.sdk.LightsparkCoroutinesClient +import com.lightspark.sdk.auth.AccountApiTokenAuthProvider import com.lightspark.sdk.uma.InMemoryPublicKeyCache import com.lightspark.sdk.uma.UmaProtocolHelper import io.ktor.server.application.Application -import io.ktor.server.application.ApplicationCall import io.ktor.server.application.call -import io.ktor.server.response.respondText import io.ktor.server.routing.get -import io.ktor.server.routing.post import io.ktor.server.routing.routing fun Application.configureRouting(config: UmaConfig) { val pubKeyCache = InMemoryPublicKeyCache() val uma = UmaProtocolHelper(pubKeyCache) + val client = LightsparkCoroutinesClient( + ClientConfig( + serverUrl = config.clientBaseURL ?: "api.lightspark.com", + authProvider = AccountApiTokenAuthProvider(config.apiClientID, config.apiClientSecret) + ), + ) + val vasp1 = Vasp1(config, uma, client) val vasp2 = Vasp2(config, uma) routing { - get("/api/umalookup/:receiver") { - call.respondText("Hello World!") - } - - get("/api/umapayreq/:callbackUuid") { - call.respondText("Hello World!") - } - - post("/api/sendpayment/:callbackUuid") { - call.respondText("Hello World!") - } - - // End VASP1 Routes - - // VASP2 Routes: - get("/.well-known/lnurlp/:username") { - call.debugLog(vasp2.handleLnurlp(call)) - } - - get("/api/uma/payreq/:uuid") { - call.debugLog(vasp2.handleLnurlPayreq(call)) - } - - post("/api/uma/payreq/:uuid") { - call.debugLog(vasp2.handleUmaPayreq(call)) - } - // End VASP2 Routes - - // Shared: + registerVasp1Routes(vasp1) + registerVasp2Routes(vasp2) get("/.well-known/lnurlpubkey") { - // It doesn't matter which vasp protocol handles this since they share a config and cache. - call.debugLog(vasp2.handlePubKeyRequest(call)) + call.debugLog(handlePubKeyRequest(call, config)) } } } - -private fun ApplicationCall.debugLog(message: String) { - application.environment.log.debug(message) -}