diff --git a/lightspark-sdk/README.md b/lightspark-sdk/README.md index cead931e..62e21216 100644 --- a/lightspark-sdk/README.md +++ b/lightspark-sdk/README.md @@ -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") } ``` diff --git a/lightspark-sdk/gradle.properties b/lightspark-sdk/gradle.properties index ee694ffb..89fcdee7 100644 --- a/lightspark-sdk/gradle.properties +++ b/lightspark-sdk/gradle.properties @@ -1,7 +1,7 @@ GROUP=com.lightspark POM_ARTIFACT_ID=lightspark-sdk # Don't bump this manually. Run `scripts/versions.main.kt ` 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 diff --git a/lightspark-sdk/src/commonJvmAndroidMain/kotlin/com/lightspark/sdk/LightsparkFuturesClient.kt b/lightspark-sdk/src/commonJvmAndroidMain/kotlin/com/lightspark/sdk/LightsparkFuturesClient.kt index 944a9c8e..9d76ef88 100644 --- a/lightspark-sdk/src/commonJvmAndroidMain/kotlin/com/lightspark/sdk/LightsparkFuturesClient.kt +++ b/lightspark-sdk/src/commonJvmAndroidMain/kotlin/com/lightspark/sdk/LightsparkFuturesClient.kt @@ -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 = - coroutineScope.future { coroutinesClient.fundNode(nodeId, amountSats) } + fun fundNode(nodeId: String, amountSats: Long?, fundingAddress: String? = null): CompletableFuture = + coroutineScope.future { coroutinesClient.fundNode(nodeId, amountSats, fundingAddress) } /** * Withdraws funds from the account and sends it to the requested bitcoin address. @@ -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? = null + ): CompletableFuture> = 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 = coroutineScope.future { + coroutinesClient.getInvoiceForPaymentHash(paymentHash) + } + /** * Fetch incoming payments for a given payment hash. * @@ -592,6 +620,14 @@ class LightsparkFuturesClient(config: ClientConfig) { coroutinesClient.getIncomingPaymentsForPaymentHash(paymentHash, transactionStatuses) } + @Throws(LightsparkException::class, LightsparkAuthenticationException::class) + fun getIncomingPaymentsForInvoice( + invoiceId: String, + transactionStatuses: List? = null + ): CompletableFuture> = coroutineScope.future { + coroutinesClient.getIncomingPaymentsForInvoice(invoiceId, transactionStatuses) + } + /** * Creates an UMA invitation. If you are part of the incentive program you should use * [createUmaInvitationWithIncentives]. diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkCoroutinesClient.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkCoroutinesClient.kt index 0d6fb427..6eaf367c 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkCoroutinesClient.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkCoroutinesClient.kt @@ -729,9 +729,14 @@ 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( @@ -739,6 +744,7 @@ class LightsparkCoroutinesClient private constructor( { add("node_id", nodeId) amountSats?.let { add("amount_sats", it) } + fundingAddress?.let { add("funding_address", it)} }, signingNodeId = nodeId, ) { @@ -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? = null, + ): List { + 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? = null, + ): List { + 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]. diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkSyncClient.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkSyncClient.kt index 2303447a..bc80e00e 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkSyncClient.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/LightsparkSyncClient.kt @@ -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. @@ -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? = null + ): List = runBlocking { + asyncClient.getOutgoingPaymentForPaymentHash(paymentHash, transactionStatuses) + } + + @Throws(LightsparkException::class, LightsparkAuthenticationException::class, CancellationException::class) + fun getIncomingPaymentsForInvoice( + invoiceId: String, + transactionStatuses: List? = null + ): List = 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]. diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/FundNode.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/FundNode.kt index b80d257e..4e30a832 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/FundNode.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/FundNode.kt @@ -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 } diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/IncomingPaymentsForInvoice.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/IncomingPaymentsForInvoice.kt new file mode 100644 index 00000000..a66b00d5 --- /dev/null +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/IncomingPaymentsForInvoice.kt @@ -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} +""" diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/IncomingPaymentsForPaymentHash.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/IncomingPaymentsForPaymentHash.kt index 94863d84..ee8c0f21 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/IncomingPaymentsForPaymentHash.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/IncomingPaymentsForPaymentHash.kt @@ -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 ) { diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/InvoiceForPaymentHash.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/InvoiceForPaymentHash.kt new file mode 100644 index 00000000..056edebb --- /dev/null +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/InvoiceForPaymentHash.kt @@ -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} +""" diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/OutgoingPaymentsForPaymentHash.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/OutgoingPaymentsForPaymentHash.kt new file mode 100644 index 00000000..37beead1 --- /dev/null +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/graphql/OutgoingPaymentsForPaymentHash.kt @@ -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} +""" \ No newline at end of file diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/model/FundNodeInput.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/model/FundNodeInput.kt index 1747bb60..e7e08c2e 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/model/FundNodeInput.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/model/FundNodeInput.kt @@ -14,6 +14,7 @@ import kotlinx.serialization.Serializable data class FundNodeInput( val nodeId: String, val amountSats: Long? = null, + val fundingAddress: String? = null, ) { companion object { } diff --git a/lightspark-sdk/src/commonTest/kotlin/com/lightspark/sdk/ClientIntegrationTests.kt b/lightspark-sdk/src/commonTest/kotlin/com/lightspark/sdk/ClientIntegrationTests.kt index 4e7fe551..ffbd1604 100644 --- a/lightspark-sdk/src/commonTest/kotlin/com/lightspark/sdk/ClientIntegrationTests.kt +++ b/lightspark-sdk/src/commonTest/kotlin/com/lightspark/sdk/ClientIntegrationTests.kt @@ -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() diff --git a/wallet-sdk/src/commonMain/kotlin/com/lightspark/sdk/wallet/model/FundWalletInput.kt b/wallet-sdk/src/commonMain/kotlin/com/lightspark/sdk/wallet/model/FundWalletInput.kt index 113f28a8..60a5b671 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/lightspark/sdk/wallet/model/FundWalletInput.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/lightspark/sdk/wallet/model/FundWalletInput.kt @@ -13,6 +13,7 @@ import kotlinx.serialization.Serializable @SerialName("FundWalletInput") data class FundWalletInput( val amountSats: Long? = null, + val fundingAddress: String? = null, ) { companion object { }