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
1 change: 1 addition & 0 deletions lightspark-sdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ kotlin {
implementation(kotlin("test-junit"))
implementation(libs.kotlinx.coroutines.test)
implementation(libs.kotest.assertions)
implementation(libs.mockito.core)
}
}
val commonJvmAndroidMain by creating {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ class LightsparkFuturesClient(config: ClientConfig) {
* @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.
* 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.
*/
@JvmOverloads
Expand All @@ -154,7 +154,14 @@ class LightsparkFuturesClient(config: ClientConfig) {
metadata: String,
expirySecs: Int? = null,
): CompletableFuture<Invoice> =
coroutineScope.future { coroutinesClient.createLnurlInvoice(nodeId, amountMsats, metadata, expirySecs) }
coroutineScope.future {
coroutinesClient.createLnurlInvoice(
nodeId,
amountMsats,
metadata,
expirySecs,
)
}

/**
* Creates a Lightning invoice for the given node. This should only be used for generating invoices for UMA, with
Expand All @@ -163,17 +170,32 @@ class LightsparkFuturesClient(config: ClientConfig) {
* @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.
* 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.
* @param signingPrivateKey The receiver's signing private key. Used to hash the receiver identifier.
* @param receiverIdentifier Optional identifier of the receiver. If provided, this will be hashed using a
* monthly-rotated seed and used for anonymized analysis.
*/
@JvmOverloads
@Throws(IllegalArgumentException::class)
fun createUmaInvoice(
nodeId: String,
amountMsats: Long,
metadata: String,
expirySecs: Int? = null,
signingPrivateKey: ByteArray? = null,
receiverIdentifier: String? = null,
): CompletableFuture<Invoice> =
coroutineScope.future { coroutinesClient.createUmaInvoice(nodeId, amountMsats, metadata, expirySecs) }
coroutineScope.future {
coroutinesClient.createUmaInvoice(
nodeId,
amountMsats,
metadata,
expirySecs,
signingPrivateKey,
receiverIdentifier,
)
}

/**
* Cancels an existing unpaid invoice and returns that invoice. Cancelled invoices cannot be paid.
Expand Down Expand Up @@ -232,15 +254,21 @@ class LightsparkFuturesClient(config: ClientConfig) {
* 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.
* @param signingPrivateKey The sender's signing private key. Used to hash the sender identifier.
* @param senderIdentifier Optional identifier of the sender. If provided, this will be hashed using a
* monthly-rotated seed and used for anonymized analysis.
* @return The payment details.
*/
@JvmOverloads
@Throws(IllegalArgumentException::class)
fun payUmaInvoice(
nodeId: String,
encodedInvoice: String,
maxFeesMsats: Long,
amountMsats: Long? = null,
timeoutSecs: Int = 60,
signingPrivateKey: ByteArray? = null,
senderIdentifier: String? = null,
): CompletableFuture<OutgoingPayment> =
coroutineScope.future {
coroutinesClient.payUmaInvoice(
Expand All @@ -249,6 +277,8 @@ class LightsparkFuturesClient(config: ClientConfig) {
maxFeesMsats,
amountMsats,
timeoutSecs,
signingPrivateKey,
senderIdentifier,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import com.lightspark.sdk.model.*
import com.lightspark.sdk.util.serializerFormat
import java.security.MessageDigest
import kotlinx.coroutines.flow.Flow
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.json.*

private const val SCHEMA_ENDPOINT = "graphql/server/2023-09-13"
Expand Down Expand Up @@ -214,7 +218,7 @@ class LightsparkCoroutinesClient private constructor(
* @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.
* 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 createLnurlInvoice(
Expand Down Expand Up @@ -255,20 +259,33 @@ class LightsparkCoroutinesClient private constructor(
* @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.
* 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.
* @param signingPrivateKey The receiver's signing private key. Used to hash the receiver identifier.
* @param receiverIdentifier Optional identifier of the receiver. If provided, this will be hashed using a
* monthly-rotated seed and used for anonymized analysis.
*/
@Throws(IllegalArgumentException::class)
suspend fun createUmaInvoice(
nodeId: String,
amountMsats: Long,
metadata: String,
expirySecs: Int? = null,
signingPrivateKey: ByteArray? = null,
receiverIdentifier: String? = 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()
val receiverHash = receiverIdentifier?.let {
if (signingPrivateKey == null) {
throw IllegalArgumentException("Receiver identifier provided without signing private key")
} else {
hashUmaIdentifier(receiverIdentifier, signingPrivateKey)
}
}

return executeQuery(
Query(
Expand All @@ -278,6 +295,7 @@ class LightsparkCoroutinesClient private constructor(
add("amountMsats", amountMsats)
add("metadataHash", metadataHash)
expirySecs?.let { add("expirySecs", expirySecs) }
receiverHash?.let { add("receiverHash", receiverHash) }
},
) {
val invoiceJson =
Expand Down Expand Up @@ -371,17 +389,32 @@ class LightsparkCoroutinesClient private constructor(
* 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.
* @param signingPrivateKey The sender's signing private key. Used to hash the sender identifier.
* @param senderIdentifier Optional identifier of the sender. If provided, this will be hashed using a
* monthly-rotated seed and used for anonymized analysis.
* @return The payment details.
*/
@JvmOverloads
@Throws(IllegalArgumentException::class)
suspend fun payUmaInvoice(
nodeId: String,
encodedInvoice: String,
maxFeesMsats: Long,
amountMsats: Long? = null,
timeoutSecs: Int = 60,
signingPrivateKey: ByteArray? = null,
senderIdentifier: String? = null,
): OutgoingPayment {
requireValidAuth()

val senderHash = senderIdentifier?.let {
if (signingPrivateKey == null) {
throw IllegalArgumentException("Receiver identifier provided without signing private key")
} else {
hashUmaIdentifier(senderIdentifier, signingPrivateKey)
}
}

return executeQuery(
Query(
PayUmaInvoiceMutation,
Expand All @@ -391,6 +424,7 @@ class LightsparkCoroutinesClient private constructor(
add("timeout_secs", timeoutSecs)
add("maximum_fees_msats", maxFeesMsats)
amountMsats?.let { add("amount_msats", amountMsats) }
senderHash?.let { add("sender_hash", senderHash) }
},
signingNodeId = nodeId,
) {
Expand Down Expand Up @@ -1137,6 +1171,18 @@ class LightsparkCoroutinesClient private constructor(
return digest.fold(StringBuilder()) { sb, it -> sb.append("%02x".format(it)) }.toString()
}

fun hashUmaIdentifier(identifier: String, signingPrivateKey: ByteArray): String {
val now = getUtcDateTime()
val input = identifier.toByteArray() + "${now.monthNumber}-${now.year}".toByteArray() + signingPrivateKey
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice :-)

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

fun getUtcDateTime(): LocalDateTime {
return Clock.System.now().toLocalDateTime(TimeZone.UTC)
}

fun setServerEnvironment(environment: ServerEnvironment, invalidateAuth: Boolean) {
serverUrl = environment.graphQLUrl
if (invalidateAuth) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,14 @@ class LightsparkSyncClient constructor(config: ClientConfig) {
amountMsats: Long,
metadata: String,
expirySecs: Int? = null,
): Invoice = runBlocking { asyncClient.createLnurlInvoice(nodeId, amountMsats, metadata, expirySecs) }
): Invoice = runBlocking {
asyncClient.createLnurlInvoice(
nodeId,
amountMsats,
metadata,
expirySecs,
)
}

/**
* Creates a Lightning invoice for the given node. This should only be used for generating invoices for UMA, with
Expand All @@ -145,14 +152,29 @@ class LightsparkSyncClient constructor(config: ClientConfig) {
* @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.
* @param signingPrivateKey The receiver's signing private key. Used to hash the receiver identifier.
* @param receiverIdentifier Optional identifier of the receiver. If provided, this will be hashed using a
* monthly-rotated seed and used for anonymized analysis.
*/
@JvmOverloads
@Throws(IllegalArgumentException::class)
fun createUmaInvoice(
nodeId: String,
amountMsats: Long,
metadata: String,
expirySecs: Int? = null,
): Invoice = runBlocking { asyncClient.createUmaInvoice(nodeId, amountMsats, metadata, expirySecs) }
signingPrivateKey: ByteArray? = null,
receiverIdentifier: String? = null,
): Invoice = runBlocking {
asyncClient.createUmaInvoice(
nodeId,
amountMsats,
metadata,
expirySecs,
signingPrivateKey,
receiverIdentifier,
)
}

/**
* Cancels an existing unpaid invoice and returns that invoice. Cancelled invoices cannot be paid.
Expand Down Expand Up @@ -202,17 +224,33 @@ class LightsparkSyncClient constructor(config: ClientConfig) {
* 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.
* @param signingPrivateKey The sender's signing private key. Used to hash the sender identifier.
* @param senderIdentifier Optional identifier of the sender. If provided, this will be hashed using a
* monthly-rotated seed and used for anonymized analysis.
* @return The payment details.
*/
@JvmOverloads
@Throws(IllegalArgumentException::class)
fun payUmaInvoice(
nodeId: String,
encodedInvoice: String,
maxFeesMsats: Long,
amountMsats: Long? = null,
timeoutSecs: Int = 60,
signingPrivateKey: ByteArray? = null,
senderIdentifier: String? = null,
): OutgoingPayment =
runBlocking { asyncClient.payUmaInvoice(nodeId, encodedInvoice, maxFeesMsats, amountMsats, timeoutSecs) }
runBlocking {
asyncClient.payUmaInvoice(
nodeId,
encodedInvoice,
maxFeesMsats,
amountMsats,
timeoutSecs,
signingPrivateKey,
senderIdentifier,
)
}

/**
* 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
Expand Up @@ -8,8 +8,17 @@ const val CreateLnurlInvoiceMutation = """
${'$'}amountMsats: Long!
${'$'}metadataHash: String!
${'$'}expirySecs: Int = null
${'$'}receiverHash: String = null
) {
create_lnurl_invoice(
input: {
node_id: ${'$'}nodeId
amount_msats: ${'$'}amountMsats
metadata_hash: ${'$'}metadataHash
expiry_secs: ${'$'}expirySecs
receiver_hash: ${'$'}receiverHash
}
) {
create_lnurl_invoice(input: { node_id: ${'$'}nodeId, amount_msats: ${'$'}amountMsats, metadata_hash: ${'$'}metadataHash, expiry_secs: ${'$'}expirySecs }) {
invoice {
...InvoiceFragment
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@ const val CreateUmaInvoiceMutation = """
${'$'}amountMsats: Long!
${'$'}metadataHash: String!
${'$'}expirySecs: Int = null
${'$'}receiverHash: String = null
) {
create_uma_invoice(
input: {
node_id: ${'$'}nodeId
amount_msats: ${'$'}amountMsats
metadata_hash: ${'$'}metadataHash
expiry_secs: ${'$'}expirySecs
receiver_hash: ${'$'}receiverHash
}
) {
create_uma_invoice(input: { node_id: ${'$'}nodeId, amount_msats: ${'$'}amountMsats, metadata_hash: ${'$'}metadataHash, expiry_secs: ${'$'}expirySecs }) {
invoice {
...InvoiceFragment
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const val PayUmaInvoiceMutation = """
${'$'}timeout_secs: Int!
${'$'}maximum_fees_msats: Long!
${'$'}amount_msats: Long
${'$'}sender_hash: String = null
) {
pay_uma_invoice(
input: {
Expand All @@ -17,6 +18,7 @@ const val PayUmaInvoiceMutation = """
timeout_secs: ${'$'}timeout_secs
amount_msats: ${'$'}amount_msats
maximum_fees_msats: ${'$'}maximum_fees_msats
sender_hash: ${'$'}sender_hash
}
) {
payment {
Expand Down
Loading