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
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,47 @@ class LightsparkCoroutinesClient private constructor(
)
}

/**
* Creates a Lightning invoice for the given node. This should only be used for generating invoices for UMA, with
* [LightsparkCoroutinesClient.createInvoice] preferred in the general case.
*
* @param nodeId The ID of the node for which to create the invoice.
* @param amountMsats The amount of the invoice in milli-satoshis.
* @param metadata The LNURL metadata payload field from the initial payreq response. This will be hashed and
* present in the h-tag (SHA256 purpose of payment) of the resulting Bolt 11 invoice.
* @param expirySecs The number of seconds until the invoice expires. Defaults to 1 day.
*/
suspend fun createUmaInvoice(
nodeId: String,
amountMsats: Long,
metadata: String,
expirySecs: Int? = null,
): Invoice {
requireValidAuth()

val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(metadata.toByteArray())
val metadataHash = digest.fold(StringBuilder()) { sb, it -> sb.append("%02x".format(it)) }.toString()

return executeQuery(
Query(
CreateUmaInvoiceMutation,
{
add("nodeId", nodeId)
add("amountMsats", amountMsats)
add("metadataHash", metadataHash)
expirySecs?.let { add("expirySecs", expirySecs) }
},
) {
val invoiceJson =
requireNotNull(
it["create_uma_invoice"]?.jsonObject?.get("invoice"),
) { "No invoice found in response" }
serializerFormat.decodeFromJsonElement(invoiceJson)
},
)
}

/**
* Pay a lightning invoice for the given node.
*
Expand Down Expand Up @@ -296,6 +337,48 @@ class LightsparkCoroutinesClient private constructor(
)
}

/**
* [payUmaInvoice] sends an UMA payment to a node on the Lightning Network, based on the invoice (as defined by the
* BOLT11 specification) that you provide. This should only be used for paying UMA invoices, with [payInvoice]
* preferred in the general case.
*
* @param nodeId The ID of the node which will pay the invoice.
* @param encodedInvoice An encoded string representation of the invoice to pay.
* @param maxFeesMsats The maximum fees to pay in milli-satoshis. You must pass a value.
* As guidance, a maximum fee of 15 basis points should make almost all transactions succeed. For example,
* for a transaction between 10k sats and 100k sats, this would mean a fee limit of 15 to 150 sats.
* @param amountMsats The amount to pay in milli-satoshis. Defaults to the full amount of the invoice.
* @param timeoutSecs The number of seconds to wait for the payment to complete. Defaults to 60.
* @return The payment details.
*/
@JvmOverloads
suspend fun payUmaInvoice(
nodeId: String,
encodedInvoice: String,
maxFeesMsats: Long,
amountMsats: Long? = null,
timeoutSecs: Int = 60,
): OutgoingPayment {
requireValidAuth()
return executeQuery(
Query(
PayUmaInvoiceMutation,
{
add("node_id", nodeId)
add("encoded_invoice", encodedInvoice)
add("timeout_secs", timeoutSecs)
add("maximum_fees_msats", maxFeesMsats)
amountMsats?.let { add("amount_msats", amountMsats) }
},
signingNodeId = nodeId,
) {
val paymentJson =
requireNotNull(it["pay_uma_invoice"]?.jsonObject?.get("payment")) { "No payment found in response" }
serializerFormat.decodeFromJsonElement(paymentJson)
},
)
}

/**
* Decode a lightning invoice to get its details included payment amount, destination, etc.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.lightspark.sdk.graphql

import com.lightspark.sdk.model.Invoice

const val CreateUmaInvoiceMutation = """
mutation CreateUmaInvoice(
${'$'}nodeId: ID!
${'$'}amountMsats: Long!
${'$'}metadataHash: String!
${'$'}expirySecs: Int = null
) {
create_uma_invoice(input: { node_id: ${'$'}nodeId, amount_msats: ${'$'}amountMsats, metadata_hash: ${'$'}metadataHash, expiry_secs: ${'$'}expirySecs }) {
invoice {
...InvoiceFragment
}
}
}

${Invoice.FRAGMENT}
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.lightspark.sdk.graphql

import com.lightspark.sdk.model.OutgoingPayment

const val PayUmaInvoiceMutation = """
mutation PayUmaInvoice(
${'$'}node_id: ID!
${'$'}encoded_invoice: String!
${'$'}timeout_secs: Int!
${'$'}maximum_fees_msats: Long!
${'$'}amount_msats: Long
) {
pay_uma_invoice(
input: {
node_id: ${'$'}node_id
encoded_invoice: ${'$'}encoded_invoice
timeout_secs: ${'$'}timeout_secs
amount_msats: ${'$'}amount_msats
maximum_fees_msats: ${'$'}maximum_fees_msats
}
) {
payment {
...OutgoingPaymentFragment
}
}
}

${OutgoingPayment.FRAGMENT}
"""
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.lightspark.sdk.uma

import com.lightspark.sdk.LightsparkCoroutinesClient
import com.lightspark.sdk.model.Invoice
import com.lightspark.sdk.util.serializerFormat
import java.security.MessageDigest
Expand Down Expand Up @@ -285,7 +286,7 @@ class UmaProtocolHelper(
* Creates an uma pay request response with an encoded invoice.
*
* @param query The [PayRequest] sent by the sender.
* @param invoiceCreator The [LnurlInvoiceCreator] that will be used to create the invoice.
* @param invoiceCreator The [UmaInvoiceCreator] that will be used to create the invoice.
* @param nodeId The node ID of the receiver.
* @param metadata The metadata that will be added to the invoice's metadata hash field.
* @param currencyCode The code of the currency that the receiver will receive for this payment.
Expand All @@ -302,7 +303,7 @@ class UmaProtocolHelper(
*/
suspend fun getPayReqResponse(
query: PayRequest,
invoiceCreator: LnurlInvoiceCreator,
invoiceCreator: UmaInvoiceCreator,
metadata: String,
currencyCode: String,
conversionRate: Long,
Expand All @@ -312,7 +313,7 @@ class UmaProtocolHelper(
): PayReqResponse {
val encodedPayerData = serializerFormat.encodeToString(query.payerData)
val metadataWithPayerData = "$metadata$encodedPayerData"
val invoice = invoiceCreator.createLnurlInvoice(
val invoice = invoiceCreator.createUmaInvoice(
amountMsats = query.amount * conversionRate,
metadata = metadataWithPayerData,
)
Expand Down Expand Up @@ -353,7 +354,16 @@ class UmaProtocolHelper(
}
}

interface LnurlInvoiceCreator {
interface UmaInvoiceCreator {
// TODO: Figure out the async story here. Do we need a different implementation for each client type?
suspend fun createLnurlInvoice(amountMsats: Long, metadata: String): Invoice
suspend fun createUmaInvoice(amountMsats: Long, metadata: String): Invoice
}

class LightsparkClientUmaInvoiceCreator(
private val client: LightsparkCoroutinesClient,
private val nodeId: String,
) : UmaInvoiceCreator {
override suspend fun createUmaInvoice(amountMsats: Long, metadata: String): Invoice {
return client.createUmaInvoice(nodeId, amountMsats, metadata)
}
}
9 changes: 3 additions & 6 deletions umaserverdemo/src/main/kotlin/com/lightspark/Vasp2.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import com.lightspark.sdk.auth.AccountApiTokenAuthProvider
import com.lightspark.sdk.model.Invoice
import com.lightspark.sdk.uma.Currency
import com.lightspark.sdk.uma.KycStatus
import com.lightspark.sdk.uma.LnurlInvoiceCreator
import com.lightspark.sdk.uma.LightsparkClientUmaInvoiceCreator
import com.lightspark.sdk.uma.UmaInvoiceCreator
import com.lightspark.sdk.uma.PayRequest
import com.lightspark.sdk.uma.PayerDataOptions
import com.lightspark.sdk.uma.UmaProtocolHelper
Expand Down Expand Up @@ -193,11 +194,7 @@ class Vasp2(
val response = try {
uma.getPayReqResponse(
query = request,
invoiceCreator = object : LnurlInvoiceCreator {
override suspend fun createLnurlInvoice(amountMsats: Long, metadata: String): Invoice {
return client.createLnurlInvoice(config.nodeID, amountMsats, metadata)
}
},
invoiceCreator = LightsparkClientUmaInvoiceCreator(client, config.nodeID),
metadata = getEncodedMetadata(),
currencyCode = "USD",
conversionRate = conversionRate,
Expand Down