Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge release/lightspark-sdk-v0.17.0 into main #200

Merged
merged 15 commits into from
Aug 15, 2024
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
4 changes: 2 additions & 2 deletions lightspark-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ Start by installing the SDK from maven:
**build.gradle:**
```groovy
dependencies {
implementation "com.lightspark:lightspark-sdk:0.16.0"
implementation "com.lightspark:lightspark-sdk:0.17.0"
}
```

or with **build.gradle.kts:**
```kotlin
dependencies {
implementation("com.lightspark:lightspark-sdk:0.16.0")
implementation("com.lightspark:lightspark-sdk:0.17.0")
}
```

Expand Down
2 changes: 1 addition & 1 deletion lightspark-sdk/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
GROUP=com.lightspark
POM_ARTIFACT_ID=lightspark-sdk
# Don't bump this manually. Run `scripts/versions.main.kt <new_version>` to bump the version instead.
VERSION_NAME=0.16.0
VERSION_NAME=0.17.0

POM_DESCRIPTION=The Lightspark API SDK for Kotlin and Java.
POM_INCEPTION_YEAR=2023
Original file line number Diff line number Diff line change
Expand Up @@ -414,10 +414,11 @@ class LightsparkFuturesClient(config: ClientConfig) {
*
* @param nodeId The ID of the node to fund. Must be a REGTEST node.
* @param amountSats The amount of funds to add to the node. Defaults to 10,000,000 SATOSHI.
* @param fundingAddress: L1 address owned by funded node. If null, automatically create new funding address
* @return The amount of funds added to the node.
*/
fun fundNode(nodeId: String, amountSats: Long?): CompletableFuture<CurrencyAmount> =
coroutineScope.future { coroutinesClient.fundNode(nodeId, amountSats) }
fun fundNode(nodeId: String, amountSats: Long?, fundingAddress: String? = null): CompletableFuture<CurrencyAmount> =
coroutineScope.future { coroutinesClient.fundNode(nodeId, amountSats, fundingAddress) }

/**
* Withdraws funds from the account and sends it to the requested bitcoin address.
Expand Down Expand Up @@ -576,6 +577,33 @@ class LightsparkFuturesClient(config: ClientConfig) {
coroutinesClient.getOutgoingPaymentsForInvoice(encodedInvoice, transactionStatuses)
}

/**
* fetch outgoing payments for a given payment hash
*
* @param paymentHash the payment hash of the invoice for which to fetch the outgoing payments
* @param transactionStatuses the transaction statuses to filter the payments by. If null, all payments will be returned.
*/
@Throws(LightsparkException::class, LightsparkAuthenticationException::class)
fun getOutgoingPaymentsForPaymentHash(
paymentHash: String,
transactionStatuses: List<TransactionStatus>? = null
): CompletableFuture<List<OutgoingPayment>> = coroutineScope.future {
coroutinesClient.getOutgoingPaymentForPaymentHash(paymentHash, transactionStatuses)
}

/**
* fetch invoice for a given payments hash
*
* @param paymentHash the payment hash of the invoice for which to fetch the outgoing payments
* @param transactionStatuses the transaction statuses to filter the payments by. If null, all payments will be returned.
*/
@Throws(LightsparkException::class, LightsparkAuthenticationException::class)
fun getInvoiceForPaymentHash(
paymentHash: String
): CompletableFuture<Invoice> = coroutineScope.future {
coroutinesClient.getInvoiceForPaymentHash(paymentHash)
}

/**
* Fetch incoming payments for a given payment hash.
*
Expand All @@ -592,6 +620,14 @@ class LightsparkFuturesClient(config: ClientConfig) {
coroutinesClient.getIncomingPaymentsForPaymentHash(paymentHash, transactionStatuses)
}

@Throws(LightsparkException::class, LightsparkAuthenticationException::class)
fun getIncomingPaymentsForInvoice(
invoiceId: String,
transactionStatuses: List<TransactionStatus>? = null
): CompletableFuture<List<IncomingPayment>> = coroutineScope.future {
coroutinesClient.getIncomingPaymentsForInvoice(invoiceId, transactionStatuses)
}

/**
* Creates an UMA invitation. If you are part of the incentive program you should use
* [createUmaInvitationWithIncentives].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -729,16 +729,22 @@ class LightsparkCoroutinesClient private constructor(
*
* @param nodeId The ID of the node to fund. Must be a REGTEST node.
* @param amountSats The amount of funds to add to the node. Defaults to 10,000,000 SATOSHI.
* @param fundingAddress: L1 address owned by funded node. If null, automatically create new funding address
* @return The amount of funds added to the node.
*/
suspend fun fundNode(nodeId: String, amountSats: Long?): CurrencyAmount {
suspend fun fundNode(
nodeId: String,
amountSats: Long?,
fundingAddress: String? = null
): CurrencyAmount {
requireValidAuth()
return executeQuery(
Query(
FundNodeMutation,
{
add("node_id", nodeId)
amountSats?.let { add("amount_sats", it) }
fundingAddress?.let { add("funding_address", it)}
},
signingNodeId = nodeId,
) {
Expand Down Expand Up @@ -1036,6 +1042,90 @@ class LightsparkCoroutinesClient private constructor(
)
}

/**
* fetch outgoing payments for a given payment hash
*
* @param paymentHash the payment hash of the invoice for which to fetch the outgoing payments
* @param transactionStatuses the transaction statuses to filter the payments by. If null, all payments will be returned.
*/
suspend fun getOutgoingPaymentForPaymentHash(
paymentHash: String,
transactionStatuses: List<TransactionStatus>? = null,
): List<OutgoingPayment> {
requireValidAuth()
return executeQuery(
Query(
OutgoingPaymentsForPaymentHashQuery,
{
add("paymentHash", paymentHash)
transactionStatuses?.let {
add("transactionStatuses", serializerFormat.encodeToJsonElement(it))
}
},
) {
val outputJson =
requireNotNull(it["outgoing_payments_for_payment_hash"]) { "No payment output found in response" }
val paymentsJson =
requireNotNull(outputJson.jsonObject["payments"]) { "No payments found in response" }
serializerFormat.decodeFromJsonElement(paymentsJson)
},
)
}

/**
* fetch invoice for a given payment hash
*
* @param paymentHash the payment hash of the invoice for which to fetch the outgoing payments
*/
suspend fun getInvoiceForPaymentHash(
paymentHash: String
): Invoice {
requireValidAuth()
return executeQuery(
Query(
InvoiceForPaymentHashQuery,
{
add("paymentHash", paymentHash)
},
) {
val outputJson =
requireNotNull(it["invoice_for_payment_hash"]) { "No invoice found in response" }
val invoiceJson =
requireNotNull(outputJson.jsonObject["invoice"]) { "No invoice found in response" }
serializerFormat.decodeFromJsonElement(invoiceJson)
},
)
}

/**
* fetch invoice for a given invoice id
*
* @param invoiceId the id of the invoice for which to fetch the outgoing payments
* @param transactionStatuses the transaction statuses to filter the payments by. If null, all payments will be returned.
*/
suspend fun getIncomingPaymentsForInvoice(
invoiceId: String,
transactionStatuses: List<TransactionStatus>? = null,
): List<IncomingPayment> {
return executeQuery(
Query(
IncomingPaymentsForInvoiceQuery,
{
add("invoiceId", invoiceId)
transactionStatuses?.let {
add("transactionStatuses", serializerFormat.encodeToJsonElement(it))
}
},
) {
val outputJson =
requireNotNull(it["incoming_payments_for_invoice"]) { "No payment output found in response" }
val paymentsJson =
requireNotNull(outputJson.jsonObject["payments"]) { "No payments found in response" }
serializerFormat.decodeFromJsonElement(paymentsJson)
}
)
}

/**
* Creates an UMA invitation. If you are part of the incentive program you should use
* [createUmaInvitationWithIncentives].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -397,11 +397,12 @@ class LightsparkSyncClient constructor(config: ClientConfig) {
*
* @param nodeId The ID of the node to fund. Must be a REGTEST node.
* @param amountSats The amount of funds to add to the node. Defaults to 10,000,000 SATOSHI.
* @param fundingAddress: L1 address owned by funded node. If null, automatically create new funding address
* @return The amount of funds added to the node.
*/
@Throws(LightsparkException::class, LightsparkAuthenticationException::class, CancellationException::class)
fun fundNode(nodeId: String, amountSats: Long?): CurrencyAmount =
runBlocking { asyncClient.fundNode(nodeId, amountSats) }
fun fundNode(nodeId: String, amountSats: Long?, fundingAddress: String? = null): CurrencyAmount =
runBlocking { asyncClient.fundNode(nodeId, amountSats, fundingAddress) }

/**
* Withdraws funds from the account and sends it to the requested bitcoin address.
Expand Down Expand Up @@ -576,6 +577,35 @@ class LightsparkSyncClient constructor(config: ClientConfig) {
asyncClient.getIncomingPaymentsForPaymentHash(paymentHash, transactionStatuses)
}

/**
* fetch outgoing payments for a given payment hash
*
* @param paymentHash the payment hash of the invoice for which to fetch the outgoing payments
* @param transactionStatuses the transaction statuses to filter the payments by. If null, all payments will be returned.
*/
@Throws(LightsparkException::class, LightsparkAuthenticationException::class, CancellationException::class)
fun getOutgoingPaymentsForPaymentHash(
paymentHash: String,
transactionStatuses: List<TransactionStatus>? = null
): List<OutgoingPayment> = runBlocking {
asyncClient.getOutgoingPaymentForPaymentHash(paymentHash, transactionStatuses)
}

@Throws(LightsparkException::class, LightsparkAuthenticationException::class, CancellationException::class)
fun getIncomingPaymentsForInvoice(
invoiceId: String,
transactionStatuses: List<TransactionStatus>? = null
): List<IncomingPayment> = runBlocking {
asyncClient.getIncomingPaymentsForInvoice(invoiceId, transactionStatuses)
}

@Throws(LightsparkException::class, LightsparkAuthenticationException::class, CancellationException::class)
fun getInvoiceForPaymentHash(
paymentHash: String
): Invoice = runBlocking {
asyncClient.getInvoiceForPaymentHash(paymentHash)
}

/**
* Creates an UMA invitation. If you are part of the incentive program you should use
* [createUmaInvitationWithIncentives].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ const val FundNodeMutation = """
mutation FundNode(
${'$'}node_id: ID!,
${'$'}amount_sats: Long
${'$'}funding_address: String
) {
fund_node(input: { node_id: ${'$'}node_id, amount_sats: ${'$'}amount_sats }) {
fund_node(input: {
node_id: ${'$'}node_id,
amount_sats: ${'$'}amount_sats,
funding_address: ${'$'}funding_address
}) {
amount {
...CurrencyAmountFragment
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.lightspark.sdk.graphql

import com.lightspark.sdk.model.IncomingPayment

const val IncomingPaymentsForInvoiceQuery = """
query IncomingPaymentsForInvoice(
${'$'}invoiceId: Hash32!,
${'$'}transactionStatuses: [TransactionStatus!] = null
) {
incoming_payments_for_invoice_query(input: {
invoice_id: ${'$'}invoiceId,
statuses: ${'$'}transactionStatuses
}) {
payments {
...IncomingPaymentFragment
}
}
}

${IncomingPayment.FRAGMENT}
"""
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.lightspark.sdk.graphql
import com.lightspark.sdk.model.IncomingPayment

const val IncomingPaymentsForPaymentHashQuery = """
query OutgoingPaymentsForInvoice(
query IncomingPaymentsForPaymentHash(
${'$'}paymentHash: Hash32!,
${'$'}transactionStatuses: [TransactionStatus!] = null
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.lightspark.sdk.graphql

import com.lightspark.sdk.model.Invoice

const val InvoiceForPaymentHashQuery = """
query InvoiceForPaymentHash(
${'$'}paymentHash: Hash32!,
) {
invoice_for_payment_hash(input: {
payment_hash: ${'$'}paymentHash,
}) {
invoice {
...InvoiceFragment
}
}
}

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

import com.lightspark.sdk.model.OutgoingPayment

const val OutgoingPaymentsForPaymentHashQuery = """
query OutgoingPaymentsForPaymentHash(
${'$'}paymentHash: Hash32!,
${'$'}transactionStatuses: [TransactionStatus!] = null
) {
outgoing_payments_for_payment_hash(input: {
payment_hash: ${'$'}paymentHash,
statuses: ${'$'}transactionStatuses
}) {
payments {
...OutgoingPaymentFragment
}
}
}

${OutgoingPayment.FRAGMENT}
"""
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import kotlinx.serialization.Serializable
data class FundNodeInput(
val nodeId: String,
val amountSats: Long? = null,
val fundingAddress: String? = null,
) {
companion object {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,45 @@ class ClientIntegrationTests {
payments[0].id.shouldBe(payment?.id)
}

@Test
fun `test getOutgoingPaymentsForPaymentHash`() = runTest {
val node = getFirstOskNode()
client.loadNodeSigningKey(node.id, PasswordRecoverySigningKeyLoader(node.id, NODE_PASSWORD))
val invoice = client.createTestModeInvoice(node.id, 100_000, "test invoice")
var outgoingPayment: OutgoingPayment? = client.payInvoice(node.id, invoice, maxFeesMsats = 100_000)
outgoingPayment.shouldNotBeNull()
while (outgoingPayment?.status == TransactionStatus.PENDING) {
delay(500)
outgoingPayment = OutgoingPayment.getOutgoingPaymentQuery(outgoingPayment.id).execute(client)
println("Payment status: ${outgoingPayment?.status}")
}
outgoingPayment?.status.shouldBe(TransactionStatus.SUCCESS)
val payments = client.getOutgoingPaymentForPaymentHash(
client.decodeInvoice(invoice).paymentHash
)
payments.shouldNotBeNull()
payments.shouldHaveSize(1)
payments[0].id.shouldBe(outgoingPayment?.id!!)
}

@Test
fun `test getInvoiceForPaymentHash`() = runTest {
val node = getFirstOskNode()
client.loadNodeSigningKey(node.id, PasswordRecoverySigningKeyLoader(node.id, NODE_PASSWORD))
val testInvoice = client.createInvoice(node.id, 100_000, "test invoice")
var payment: IncomingPayment? = client.createTestModePayment(node.id, testInvoice.data.encodedPaymentRequest)
payment.shouldNotBeNull()
while (payment?.status == TransactionStatus.PENDING) {
delay(500)
payment = IncomingPayment.getIncomingPaymentQuery(payment.id).execute(client)
println("Payment status: ${payment?.status}")
}

val invoice = client.getInvoiceForPaymentHash(testInvoice.data.paymentHash)
invoice.shouldNotBeNull()
invoice.id.shouldBe(testInvoice.id)
}

@Test
fun `test uma identifier hashing`() = runTest {
val privKeyBytes = "xyz".toByteArray()
Expand Down
Loading
Loading