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
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ ktlint = "11.3.1"
ktor = "2.3.7"
lightsparkCore = "0.6.0"
lightsparkCrypto = "0.6.0"
uma = "1.2.1"
uma = "1.3.0"
mavenPublish = "0.25.2"
mockitoCore = "5.5.0"
taskTree = "2.1.1"
Expand Down
138 changes: 136 additions & 2 deletions umaserverdemo/src/main/kotlin/com/lightspark/ReceivingVasp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@ import com.lightspark.sdk.ClientConfig
import com.lightspark.sdk.LightsparkCoroutinesClient
import com.lightspark.sdk.auth.AccountApiTokenAuthProvider
import com.lightspark.sdk.model.Node
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
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
Expand All @@ -31,12 +42,19 @@ import me.uma.UmaInvoiceCreator
import me.uma.UmaProtocolHelper
import me.uma.UnsupportedVersionException
import me.uma.protocol.CounterPartyDataOptions
import me.uma.protocol.InvoiceCurrency
import me.uma.protocol.KycStatus
import me.uma.protocol.LnurlpResponse
import me.uma.protocol.PayRequest
import me.uma.protocol.createCounterPartyDataOptions
import me.uma.protocol.createPayeeData
import me.uma.protocol.identifier
import java.util.UUID
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.plus
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive


class ReceivingVasp(
private val config: UmaConfig,
Expand All @@ -46,15 +64,131 @@ class ReceivingVasp(
private val nonceCache = InMemoryNonceCache(Clock.System.now().epochSeconds)
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private lateinit var senderUmaVersion: String
private val httpClient = HttpClient {
install(ContentNegotiation) {
json(
Json {
isLenient = true
},
)
}
}

suspend fun createInvoice(call: ApplicationCall): String {
val (status, data) = createUmaInvoice(call)
if (status != HttpStatusCode.OK) {
call.respond(status, data)
return data
} else {
call.respond(data)
}
return "OK"
}

suspend fun createAndSendInvoice(call: ApplicationCall): String {
val senderUma = call.parameters["senderUma"] ?: run {
call.respond(HttpStatusCode.BadRequest, "SenderUma not provided.")
return "SenderUma not provided."
}
val (status, data) = createUmaInvoice(call, senderUma)
if (status != HttpStatusCode.OK) {
call.respond(status, data)
return data
}
val senderComponents = senderUma.split("@")
val sendingVaspDomain = senderComponents.getOrNull(1) ?: run {
call.respond(HttpStatusCode.BadRequest, "Invalid senderUma.")
return "Invalid senderUma."
}
val wellKnownConfiguration = "http://$sendingVaspDomain/.well-known/uma-configuration"
val umaEndpoint = try {
val umaConfigResponse = httpClient.get(wellKnownConfiguration)
if (umaConfigResponse.status != HttpStatusCode.OK) {
call.respond(
HttpStatusCode.FailedDependency,
"failed to fetch request / pay endpoint at $wellKnownConfiguration",
)
return "failed to fetch request / pay endpoint at $wellKnownConfiguration"
}
Json.decodeFromString<JsonObject>(
umaConfigResponse.bodyAsText(),
)["uma_request_endpoint"]?.jsonPrimitive?.content
} catch (e: Exception) {
call.respond(
HttpStatusCode.FailedDependency,
"failed to fetch request / pay endpoint at $wellKnownConfiguration",
)
return "failed to fetch request / pay endpoint at $wellKnownConfiguration"
}
if (umaEndpoint == null) {
call.respond(HttpStatusCode.FailedDependency, "failed to fetch $wellKnownConfiguration")
return "failed to fetch $wellKnownConfiguration"
}
val response = try {
httpClient.post(umaEndpoint) {
contentType(ContentType.Application.Json)
setBody(parameter("invoice", data))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.FailedDependency, "failed to fetch $umaEndpoint")
return "failed to fetch $umaEndpoint"
}
if (response.status != HttpStatusCode.OK) {
call.respond(HttpStatusCode.InternalServerError, "Payreq to Sending Vasp: ${response.status}")
return "Payreq to sending vasp failed: ${response.status}"
}
call.respond(response.body())
return "OK"
}

private fun createUmaInvoice(
call: ApplicationCall, senderUma: String? = null
): Pair<HttpStatusCode, String> {
val amount = try {
call.parameters["amount"]?.toLong() ?: run {
return HttpStatusCode.BadRequest to "Amount not provided."
}
} catch (e: NumberFormatException) {
return HttpStatusCode.BadRequest to "Amount not parsable as number."
}

val currency = call.parameters["currencyCode"]?.let { currencyCode ->
// check if we support this currency code.
getReceivingCurrencies(UMA_VERSION_STRING).firstOrNull {
it.code == currencyCode
} ?: run {
return HttpStatusCode.BadRequest to "Unsupported CurrencyCode $currencyCode."
}
} ?: run {
return HttpStatusCode.BadRequest to "CurrencyCode not provided."
}

val expiresIn2Days = Clock.System.now().plus(2, DateTimeUnit.HOUR*24) //?

val receiverUma = "${config.username}:${getReceivingVaspDomain(call)}"

val invoice = uma.getInvoice(
receiverUma = receiverUma,
invoiceUUID = UUID.randomUUID().toString(),
amount = amount,
receivingCurrency = InvoiceCurrency(
currency.code, currency.name, currency.symbol, currency.decimals
),
expiration = expiresIn2Days.toEpochMilliseconds(),
isSubjectToTravelRule = true,
requiredPayerData = createCounterPartyDataOptions(
"name" to false,
"email" to false,
"compliance" to true,
"identifier" to true,
),
callback = getLnurlpCallback(call), // structured the same, going to /api/uma/payreq/{user_id}
privateSigningKey = config.umaSigningPrivKey,
senderUma = senderUma
)
return HttpStatusCode.OK to invoice.toBech32()
}

suspend fun handleLnurlp(call: ApplicationCall): String {
val username = call.parameters["username"]

Expand Down Expand Up @@ -355,11 +489,11 @@ fun Routing.registerReceivingVaspRoutes(receivingVasp: ReceivingVasp) {
call.debugLog(receivingVasp.handleUmaPayreq(call))
}

get("/api/uma/create_invoice") {
post("/api/uma/create_invoice") {
call.debugLog(receivingVasp.createInvoice(call));
}

get("/api/uma/create_and_send_invoice") {
post("/api/uma/create_and_send_invoice") {
call.debugLog(receivingVasp.createAndSendInvoice(call))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ fun Application.configureRouting(
call.respond(
HttpStatusCode.OK,
buildJsonObject {
put("uma_request_endpoint", "$scheme://$domain/api/uma/request_pay_invoice")
put("uma_request_endpoint", "$scheme://$domain/api/uma/request_invoice_payment")
put(
"uma_major_versions",
buildJsonArray {
Expand Down