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")