diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingCurrencies.kt b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingCurrencies.kt index 31c1f5b2..11610950 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingCurrencies.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/ReceivingCurrencies.kt @@ -1,34 +1,37 @@ package com.lightspark import me.uma.protocol.Currency -import me.uma.protocol.CurrencyConvertible +import me.uma.protocol.createCurrency // In real life, this would come from some actual exchange rate API. private const val MSATS_PER_USD_CENT = 22883.56 -val SATS_CURRENCY = Currency( - code = "SAT", - name = "Satoshis", - symbol = "SAT", - millisatoshiPerUnit = 1.0, - convertible = CurrencyConvertible( - min = 1, - max = 100_000_000_000, // 1 BTC - ), - decimals = 0, -) +fun getSatsCurrency(senderVersion: String): Currency { + return createCurrency( + code = "SAT", + name = "Satoshis", + symbol = "SAT", + millisatoshiPerUnit = 1.0, + minSendable = 1, + maxSendable = 100_000_000_000, // 1 BTC + decimals = 0, + senderUmaVersion = senderVersion, + ) +} -val RECEIVING_CURRENCIES = listOf( - Currency( - code = "USD", - name = "US Dollar", - symbol = "$", - millisatoshiPerUnit = MSATS_PER_USD_CENT, - convertible = CurrencyConvertible( - min = 1, - max = 1_000_000, +fun getReceivingCurrencies(senderUmaVersion: String): List { + val satsCurrency = getSatsCurrency(senderUmaVersion) + return listOf( + createCurrency( + code = "USD", + name = "US Dollar", + symbol = "$", + millisatoshiPerUnit = MSATS_PER_USD_CENT, + minSendable = 1, + maxSendable = 1_000_000, + decimals = 2, + senderUmaVersion = senderUmaVersion, ), - decimals = 2, - ), - SATS_CURRENCY, -) + satsCurrency, + ) +} diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/Vasp1.kt b/umaserverdemo/src/main/kotlin/com/lightspark/Vasp1.kt index daa70fde..19a5dcb2 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/Vasp1.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/Vasp1.kt @@ -30,6 +30,7 @@ import io.ktor.server.routing.post import kotlinx.coroutines.delay import kotlinx.datetime.Clock import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject @@ -40,15 +41,16 @@ import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonArray import me.uma.InMemoryNonceCache +import me.uma.UMA_VERSION_STRING import me.uma.UmaProtocolHelper import me.uma.protocol.CounterPartyDataOptions +import me.uma.protocol.CurrencySerializer import me.uma.protocol.KycStatus -import me.uma.protocol.LnurlpResponse -import me.uma.protocol.PayReqResponse import me.uma.protocol.PayRequest import me.uma.protocol.UtxoWithAmount import me.uma.protocol.createPayerData import me.uma.selectHighestSupportedVersion +import me.uma.utils.serialFormat class Vasp1( private val config: UmaConfig, @@ -66,6 +68,7 @@ class Vasp1( } private val requestDataCache = Vasp1RequestCache() private val nonceCache = InMemoryNonceCache(Clock.System.now().epochSeconds) + private lateinit var receiverUmaVersion: String suspend fun handleClientUmaLookup(call: ApplicationCall): String { val receiverAddress = call.parameters["receiver"] @@ -132,7 +135,7 @@ class Vasp1( } val lnurlpResponse = try { - response.body() + 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.") @@ -156,15 +159,17 @@ class Vasp1( call.respond(HttpStatusCode.BadRequest, "Failed to verify lnurlp response signature.") return "Failed to verify lnurlp response signature." } + + receiverUmaVersion = umaLnurlpResponse.umaVersion } val callbackUuid = requestDataCache.saveLnurlpResponseData(lnurlpResponse, receiverId, receiverVasp) - val receiverCurrencies = lnurlpResponse.currencies?.map { it.code } ?: listOf(SATS_CURRENCY) + val receiverCurrencies = lnurlpResponse.currencies ?: listOf(getSatsCurrency(UMA_VERSION_STRING)) call.respond( buildJsonObject { putJsonArray("receiverCurrencies") { - addAll(receiverCurrencies.map { Json.encodeToJsonElement(it) }) + addAll(receiverCurrencies.map { Json.encodeToJsonElement(CurrencySerializer, it) }) } put("minSendSats", lnurlpResponse.minSendable) put("maxSendSats", lnurlpResponse.maxSendable) @@ -193,10 +198,13 @@ class Vasp1( return "Amount invalid or not provided." } - val currencyCode = call.request.queryParameters["receivingCurrencyCode"] ?: "SAT" + val currencyCode = call.request.queryParameters["receivingCurrencyCode"] + // fallback to uma v0 + ?: call.request.queryParameters["currencyCode"] + ?: "SAT" val currencyValid = ( initialRequestData.lnurlpResponse.currencies - ?: listOf(SATS_CURRENCY) + ?: listOf(getSatsCurrency(UMA_VERSION_STRING)) ).any { it.code == currencyCode } if (!currencyValid) { call.respond(HttpStatusCode.BadRequest, "Receiving currency code not supported.") @@ -240,15 +248,18 @@ class Vasp1( payerName = payer.name, payerEmail = payer.email, comment = call.request.queryParameters["comment"], + receiverUmaVersion = receiverUmaVersion, ) } else { - PayRequest( - sendingCurrencyCode = if (isAmountInMsats) "SAT" else currencyCode, - receivingCurrencyCode = currencyCode.takeIf { it != "SAT" }, - amount = amount, - payerData = createPayerData(identifier = payer.identifier, name = payer.name, email = payer.email), - comment = call.request.queryParameters["comment"], + 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) @@ -259,12 +270,12 @@ class Vasp1( val response = if (isUma) { httpClient.post(initialRequestData.lnurlpResponse.callback) { contentType(ContentType.Application.Json) - setBody(payReq) + setBody(payReq.toJson()) } } else { httpClient.get(initialRequestData.lnurlpResponse.callback) { contentType(ContentType.Application.Json) - parametersOf(payReq.toQueryParamMap()) + parametersOf(payReq.toQueryParamMap() ?: emptyMap()) } } @@ -274,7 +285,7 @@ class Vasp1( } val payReqResponse = try { - response.body() + uma.parseAsPayReqResponse(response.body()) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, "Failed to parse payreq response.") return "Failed to parse payreq response." diff --git a/umaserverdemo/src/main/kotlin/com/lightspark/Vasp2.kt b/umaserverdemo/src/main/kotlin/com/lightspark/Vasp2.kt index f40218cf..760652ad 100644 --- a/umaserverdemo/src/main/kotlin/com/lightspark/Vasp2.kt +++ b/umaserverdemo/src/main/kotlin/com/lightspark/Vasp2.kt @@ -44,6 +44,7 @@ class Vasp2( ) { private val nonceCache = InMemoryNonceCache(Clock.System.now().epochSeconds) private val coroutineScope = CoroutineScope(Dispatchers.IO) + private lateinit var senderUmaVersion: String suspend fun handleLnurlp(call: ApplicationCall): String { val username = call.parameters["username"] @@ -85,7 +86,7 @@ class Vasp2( minSendable = 1, maxSendable = 100_000_000, metadata = getEncodedMetadata(), - currencies = RECEIVING_CURRENCIES, + currencies = getReceivingCurrencies(senderUmaVersion), requiredPayerData = createCounterPartyDataOptions( "name" to false, "email" to false, @@ -98,6 +99,8 @@ class Vasp2( return "OK" } + senderUmaVersion = request.umaVersion + val pubKeys = try { uma.fetchPublicKeysForVasp(request.vaspDomain) } catch (e: Exception) { @@ -127,7 +130,7 @@ class Vasp2( "compliance" to true, "identifier" to true, ), - currencyOptions = RECEIVING_CURRENCIES, + currencyOptions = getReceivingCurrencies(senderUmaVersion), receiverKycStatus = KycStatus.VERIFIED, ) } catch (e: Exception) { @@ -169,16 +172,17 @@ class Vasp2( } } - val receivingCurrency = RECEIVING_CURRENCIES.firstOrNull { it.code == payreq.receivingCurrencyCode } ?: run { - call.respond(HttpStatusCode.BadRequest, "Unsupported currency.") - return "Unsupported currency." - } + val receivingCurrency = getReceivingCurrencies(senderUmaVersion) + .firstOrNull { it.code == payreq.receivingCurrencyCode() } ?: run { + call.respond(HttpStatusCode.BadRequest, "Unsupported currency.") + return "Unsupported currency." + } val response = uma.getPayReqResponse( query = payreq, invoiceCreator = lnurlInvoiceCreator, metadata = getEncodedMetadata(), - receivingCurrencyCode = payreq.receivingCurrencyCode, + receivingCurrencyCode = payreq.receivingCurrencyCode(), receivingCurrencyDecimals = receivingCurrency.decimals, conversionRate = receivingCurrency.millisatoshiPerUnit, receiverFeesMillisats = 0, @@ -186,6 +190,7 @@ class Vasp2( receiverNodePubKey = null, utxoCallback = null, receivingVaspPrivateKey = null, + senderUmaVersion = senderUmaVersion, ) call.respond(response) @@ -206,7 +211,7 @@ class Vasp2( } val request = try { - call.receive() + uma.parseAsPayRequest(call.receive()) } catch (e: Exception) { call.respond(HttpStatusCode.BadRequest, "Invalid pay request. ${e.message}") return "Invalid pay request." @@ -231,10 +236,11 @@ class Vasp2( return "Invalid payreq signature." } - val receivingCurrency = RECEIVING_CURRENCIES.firstOrNull { it.code == request.receivingCurrencyCode } ?: run { - call.respond(HttpStatusCode.BadRequest, "Unsupported currency.") - return "Unsupported currency." - } + val receivingCurrency = getReceivingCurrencies(senderUmaVersion) + .firstOrNull { it.code == request.receivingCurrencyCode() } ?: run { + call.respond(HttpStatusCode.BadRequest, "Unsupported currency.") + return "Unsupported currency." + } val client = LightsparkCoroutinesClient( ClientConfig( @@ -243,7 +249,7 @@ class Vasp2( ), ) val expirySecs = 60 * 5 - val payeeProfile = getPayeeProfile(request.requestedPayeeData, call) + val payeeProfile = getPayeeProfile(request.requestedPayeeData(), call) val response = try { uma.getPayReqResponse( @@ -264,6 +270,7 @@ class Vasp2( name = payeeProfile.name, email = payeeProfile.email, ), + senderUmaVersion = senderUmaVersion, ) } catch (e: Exception) { call.application.environment.log.error("Failed to create payreq response.", e) @@ -271,7 +278,7 @@ class Vasp2( return "Failed to create payreq response." } - call.respond(response) + call.respond(response.toJson()) return "OK" }