Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 28 additions & 25 deletions umaserverdemo/src/main/kotlin/com/lightspark/ReceivingCurrencies.kt
Original file line number Diff line number Diff line change
@@ -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<Currency> {
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,
)
}
43 changes: 27 additions & 16 deletions umaserverdemo/src/main/kotlin/com/lightspark/Vasp1.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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"]
Expand Down Expand Up @@ -132,7 +135,7 @@ class Vasp1(
}

val lnurlpResponse = try {
response.body<LnurlpResponse>()
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.")
Expand All @@ -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)
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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)
Expand All @@ -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())
}
}

Expand All @@ -274,7 +285,7 @@ class Vasp1(
}

val payReqResponse = try {
response.body<PayReqResponse>()
uma.parseAsPayReqResponse(response.body())
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, "Failed to parse payreq response.")
return "Failed to parse payreq response."
Expand Down
35 changes: 21 additions & 14 deletions umaserverdemo/src/main/kotlin/com/lightspark/Vasp2.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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,
Expand All @@ -98,6 +99,8 @@ class Vasp2(
return "OK"
}

senderUmaVersion = request.umaVersion

val pubKeys = try {
uma.fetchPublicKeysForVasp(request.vaspDomain)
} catch (e: Exception) {
Expand Down Expand Up @@ -127,7 +130,7 @@ class Vasp2(
"compliance" to true,
"identifier" to true,
),
currencyOptions = RECEIVING_CURRENCIES,
currencyOptions = getReceivingCurrencies(senderUmaVersion),
receiverKycStatus = KycStatus.VERIFIED,
)
} catch (e: Exception) {
Expand Down Expand Up @@ -169,23 +172,25 @@ 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,
receiverChannelUtxos = null,
receiverNodePubKey = null,
utxoCallback = null,
receivingVaspPrivateKey = null,
senderUmaVersion = senderUmaVersion,
)

call.respond(response)
Expand All @@ -206,7 +211,7 @@ class Vasp2(
}

val request = try {
call.receive<PayRequest>()
uma.parseAsPayRequest(call.receive<String>())
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, "Invalid pay request. ${e.message}")
return "Invalid pay request."
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -264,14 +270,15 @@ class Vasp2(
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."
}

call.respond(response)
call.respond(response.toJson())

return "OK"
}
Expand Down