Skip to content

Commit 14b284f

Browse files
committed
feat(gl-sdk): add list_invoices, list_pays, and unified list_payments API
Three new Node methods for querying payment history: - list_invoices(): wraps CLN ListInvoices with full native filtering (label, invstring, payment_hash, offer_id) and pagination (index, start, limit) - list_pays(): wraps CLN ListPays with native filtering (bolt11, payment_hash, status) and pagination - list_payments(): merges both into a unified Payment timeline sorted newest-first, defaults to COMPLETE status filter New types: Invoice, InvoiceStatus, Pay, ListInvoicesResponse, ListPaysResponse, Payment, PaymentDirection, PaymentStatus, ListPaymentsResponse, ListIndex. Payment includes optional invoice/pay fields for drill-down access to full CLN-specific details. Kotlin extensions provide default null parameters for idiomatic API. Includes Python integration tests and Android instrumented tests.
1 parent 5cb9e0a commit 14b284f

7 files changed

Lines changed: 785 additions & 4 deletions

File tree

docs/superpowers/plans/2026-04-01-list-payments-api.md

Whitespace-only changes.

docs/superpowers/specs/2026-04-01-list-payments-api-design.md

Whitespace-only changes.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Instrumented tests for list_invoices, list_pays, and list_payments.
2+
// Tests type construction, empty lists on fresh nodes, invoice creation
3+
// visibility, and status filtering.
4+
5+
package com.blockstream.glsdk
6+
7+
import android.system.Os
8+
import androidx.test.ext.junit.runners.AndroidJUnit4
9+
import org.junit.Assert.*
10+
import org.junit.Before
11+
import org.junit.Test
12+
import org.junit.runner.RunWith
13+
import kotlin.uuid.ExperimentalUuidApi
14+
import kotlin.uuid.Uuid
15+
16+
@RunWith(AndroidJUnit4::class)
17+
class ListPaymentTest {
18+
19+
@Before
20+
fun setup() {
21+
Os.setenv("RUST_LOG", "trace", true)
22+
}
23+
24+
// BIP39 test vector — not a real wallet
25+
private val testMnemonic =
26+
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"
27+
28+
// ============================================================
29+
// Type construction
30+
// ============================================================
31+
32+
@Test
33+
fun invoice_status_enum_values() {
34+
assertNotNull(InvoiceStatus.UNPAID)
35+
assertNotNull(InvoiceStatus.PAID)
36+
assertNotNull(InvoiceStatus.EXPIRED)
37+
}
38+
39+
@Test
40+
fun payment_direction_enum_values() {
41+
assertNotNull(PaymentDirection.SENT)
42+
assertNotNull(PaymentDirection.RECEIVED)
43+
}
44+
45+
@Test
46+
fun payment_status_enum_values() {
47+
assertNotNull(PaymentStatus.PENDING)
48+
assertNotNull(PaymentStatus.COMPLETE)
49+
assertNotNull(PaymentStatus.FAILED)
50+
assertNotNull(PaymentStatus.EXPIRED)
51+
}
52+
53+
// ============================================================
54+
// Invoice appears in list_invoices and list_payments
55+
// ============================================================
56+
57+
@OptIn(ExperimentalUuidApi::class)
58+
@Test
59+
fun created_invoice_appears_in_list_invoices() {
60+
val config = Config()
61+
registerOrRecover(testMnemonic, null, config).use { node ->
62+
val label = Uuid.random().toString()
63+
node.receive(label = label, description = "Coffee", amountMsat = 10_000_000uL)
64+
65+
val result = node.listInvoices()
66+
val matching = result.invoices.filter { it.label == label }
67+
assertEquals("Should find exactly one invoice", 1, matching.size)
68+
assertEquals(InvoiceStatus.UNPAID, matching[0].status)
69+
assertEquals("Coffee", matching[0].description)
70+
assertNotNull(matching[0].bolt11)
71+
}
72+
}
73+
74+
@OptIn(ExperimentalUuidApi::class)
75+
@Test
76+
fun default_filter_excludes_unpaid() {
77+
val config = Config()
78+
registerOrRecover(testMnemonic, null, config).use { node ->
79+
val label = Uuid.random().toString()
80+
node.receive(label = label, description = "Tea", amountMsat = 5_000_000uL)
81+
82+
// Default (null) filters to COMPLETE, so unpaid invoice should not appear
83+
val result = node.listPayments()
84+
val matching = result.payments.filter { it.label == label }
85+
assertTrue("Default filter should exclude UNPAID invoice", matching.isEmpty())
86+
}
87+
}
88+
89+
@OptIn(ExperimentalUuidApi::class)
90+
@Test
91+
fun pending_filter_includes_unpaid_invoice() {
92+
val config = Config()
93+
registerOrRecover(testMnemonic, null, config).use { node ->
94+
val label = Uuid.random().toString()
95+
node.receive(label = label, description = "Tea", amountMsat = 5_000_000uL)
96+
97+
val result = node.listPayments(PaymentStatus.PENDING)
98+
val matching = result.payments.filter { it.label == label }
99+
assertEquals("PENDING filter should include UNPAID invoice", 1, matching.size)
100+
assertEquals(PaymentDirection.RECEIVED, matching[0].direction)
101+
assertEquals(PaymentStatus.PENDING, matching[0].status)
102+
assertEquals(InvoiceStatus.UNPAID, matching[0].invoiceStatus)
103+
assertNull(matching[0].payStatus)
104+
assertNotNull(matching[0].invoice)
105+
assertEquals(label, matching[0].invoice?.label)
106+
assertNull(matching[0].pay)
107+
}
108+
}
109+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Kotlin extension functions for Node that provide default parameter values.
2+
// The UniFFI-generated bindings require all parameters explicitly;
3+
// these extensions give Kotlin consumers idiomatic optional-parameter APIs.
4+
5+
package com.blockstream.glsdk
6+
7+
@Throws(Exception::class)
8+
public fun Node.listInvoices(
9+
label: String? = null,
10+
invstring: String? = null,
11+
paymentHash: ByteArray? = null,
12+
offerId: String? = null,
13+
index: ListIndex? = null,
14+
start: ULong? = null,
15+
limit: UInt? = null,
16+
): ListInvoicesResponse = listInvoices(label, invstring, paymentHash, offerId, index, start, limit)
17+
18+
@Throws(Exception::class)
19+
public fun Node.listPays(
20+
bolt11: String? = null,
21+
paymentHash: ByteArray? = null,
22+
status: PayStatus? = null,
23+
index: ListIndex? = null,
24+
start: ULong? = null,
25+
limit: UInt? = null,
26+
): ListPaysResponse = listPays(bolt11, paymentHash, status, index, start, limit)
27+
28+
@Throws(Exception::class)
29+
public fun Node.listPayments(
30+
status: PaymentStatus? = null,
31+
): ListPaymentsResponse = listPayments(status)

libs/gl-sdk/src/lib.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ pub use crate::{
3535
config::Config,
3636
credentials::{Credentials, DeveloperCert},
3737
node::{
38-
ChannelState, FundChannel, FundOutput, GetInfoResponse, InvoicePaidEvent,
39-
ListFundsResponse, ListPeerChannelsResponse, ListPeersResponse, Node, NodeEvent,
40-
NodeEventStream, OnchainReceiveResponse, OnchainSendResponse, OutputStatus, PayStatus,
41-
Peer, PeerChannel, ReceiveResponse, SendResponse,
38+
ChannelState, FundChannel, FundOutput, GetInfoResponse, Invoice, InvoicePaidEvent,
39+
InvoiceStatus, ListFundsResponse, ListIndex, ListInvoicesResponse, ListPaymentsResponse,
40+
ListPeerChannelsResponse, ListPaysResponse, ListPeersResponse, Node, NodeEvent,
41+
NodeEventStream, OnchainReceiveResponse, OnchainSendResponse, OutputStatus, Pay,
42+
PayStatus, Payment, PaymentDirection, PaymentStatus, Peer, PeerChannel, ReceiveResponse,
43+
SendResponse,
4244
},
4345
scheduler::Scheduler,
4446
signer::{Handle, Signer},

0 commit comments

Comments
 (0)