diff --git a/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/NodeOperationsTest.kt b/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/NodeOperationsTest.kt index 234eb10c5..597c3d6aa 100644 --- a/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/NodeOperationsTest.kt +++ b/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/NodeOperationsTest.kt @@ -2,6 +2,8 @@ package com.blockstream.glsdk import android.system.Os import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -41,4 +43,22 @@ class NodeOperationsTest { println("Lightning Invoice: ${invoice.toString()}") } } + + @Test + fun test_node_state_returns_valid_snapshot() { + val config = Config() + val node = registerOrRecover(mnemonic = testMnemonic, inviteCode = null, config = config) + node.use { n -> + val state = n.nodeState() + assertTrue(state.id.isNotEmpty()) + assertTrue(state.blockHeight > 0u) + assertEquals("bitcoin", state.network) + assertTrue(state.version.isNotEmpty()) + assertEquals(0uL, state.channelsBalanceMsat) + assertEquals(0uL, state.maxPayableMsat) + assertEquals(0uL, state.totalChannelCapacityMsat) + assertEquals(0uL, state.onchainBalanceMsat) + assertEquals(0uL, state.totalInboundLiquidityMsat) + } + } } diff --git a/libs/gl-sdk-android/lib/src/commonMain/kotlin/com/blockstream/glsdk/NodeExtensions.kt b/libs/gl-sdk-android/lib/src/commonMain/kotlin/com/blockstream/glsdk/NodeExtensions.kt index f304eabbf..14e5b4449 100644 --- a/libs/gl-sdk-android/lib/src/commonMain/kotlin/com/blockstream/glsdk/NodeExtensions.kt +++ b/libs/gl-sdk-android/lib/src/commonMain/kotlin/com/blockstream/glsdk/NodeExtensions.kt @@ -30,7 +30,7 @@ public fun Node.listPayments( filters: List? = null, fromTimestamp: ULong? = null, toTimestamp: ULong? = null, - includeFailures: Boolean? = null, + includeFailures: Boolean = false, offset: UInt? = null, limit: UInt? = null, ): List = listPayments(ListPaymentsRequest(filters, fromTimestamp, toTimestamp, includeFailures, offset, limit)) diff --git a/libs/gl-sdk-cli/src/output.rs b/libs/gl-sdk-cli/src/output.rs index 635342afa..d1fd72ba5 100644 --- a/libs/gl-sdk-cli/src/output.rs +++ b/libs/gl-sdk-cli/src/output.rs @@ -27,9 +27,9 @@ pub struct GetInfoOutput { impl From for GetInfoOutput { fn from(r: glsdk::GetInfoResponse) -> Self { Self { - id: hex::encode(&r.id), + id: r.id, alias: r.alias, - color: hex::encode(&r.color), + color: r.color, num_peers: r.num_peers, num_pending_channels: r.num_pending_channels, num_active_channels: r.num_active_channels, @@ -73,7 +73,7 @@ impl From for ListPeersOutput { impl From for PeerOutput { fn from(p: glsdk::Peer) -> Self { Self { - id: hex::encode(&p.id), + id: p.id, connected: p.connected, num_channels: p.num_channels, netaddr: p.netaddr, @@ -130,18 +130,19 @@ fn channel_state_str(s: &glsdk::ChannelState) -> &'static str { glsdk::ChannelState::DualopendAwaitingLockin => "DUALOPEND_AWAITING_LOCKIN", glsdk::ChannelState::DualopendOpenCommitted => "DUALOPEND_OPEN_COMMITTED", glsdk::ChannelState::DualopendOpenCommitReady => "DUALOPEND_OPEN_COMMIT_READY", + glsdk::ChannelState::Unknown => "UNKNOWN", } } impl From for PeerChannelOutput { fn from(c: glsdk::PeerChannel) -> Self { Self { - peer_id: hex::encode(&c.peer_id), + peer_id: c.peer_id, peer_connected: c.peer_connected, state: channel_state_str(&c.state).to_string(), short_channel_id: c.short_channel_id, - channel_id: c.channel_id.map(|v| hex::encode(&v)), - funding_txid: c.funding_txid.map(|v| hex::encode(&v)), + channel_id: c.channel_id, + funding_txid: c.funding_txid, funding_outnum: c.funding_outnum, to_us_msat: c.to_us_msat, total_msat: c.total_msat, @@ -205,7 +206,7 @@ impl From for ListFundsOutput { impl From for FundOutputOutput { fn from(o: glsdk::FundOutput) -> Self { Self { - txid: hex::encode(&o.txid), + txid: o.txid, output: o.output, amount_msat: o.amount_msat, status: output_status_str(&o.status).to_string(), @@ -218,15 +219,15 @@ impl From for FundOutputOutput { impl From for FundChannelOutput { fn from(c: glsdk::FundChannel) -> Self { Self { - peer_id: hex::encode(&c.peer_id), + peer_id: c.peer_id, our_amount_msat: c.our_amount_msat, amount_msat: c.amount_msat, - funding_txid: hex::encode(&c.funding_txid), + funding_txid: c.funding_txid, funding_output: c.funding_output, connected: c.connected, state: channel_state_str(&c.state).to_string(), short_channel_id: c.short_channel_id, - channel_id: c.channel_id.map(|v| hex::encode(&v)), + channel_id: c.channel_id, } } } @@ -267,7 +268,7 @@ impl From for SendOutput { fn from(r: glsdk::SendResponse) -> Self { Self { status: pay_status_str(&r.status).to_string(), - preimage: hex::encode(&r.preimage), + preimage: r.preimage, amount_msat: r.amount_msat, amount_sent_msat: r.amount_sent_msat, parts: r.parts, @@ -301,7 +302,7 @@ impl From for OnchainSendOutput { fn from(r: glsdk::OnchainSendResponse) -> Self { Self { tx: hex::encode(&r.tx), - txid: hex::encode(&r.txid), + txid: r.txid, psbt: r.psbt, } } @@ -330,9 +331,9 @@ impl From for NodeEventOutput { fn from(e: glsdk::NodeEvent) -> Self { match e { glsdk::NodeEvent::InvoicePaid { details } => NodeEventOutput::InvoicePaid { - payment_hash: hex::encode(&details.payment_hash), + payment_hash: details.payment_hash, bolt11: details.bolt11, - preimage: hex::encode(&details.preimage), + preimage: details.preimage, label: details.label, amount_msat: details.amount_msat, }, diff --git a/libs/gl-sdk-napi/package-lock.json b/libs/gl-sdk-napi/package-lock.json index a64414f10..573bf5939 100644 --- a/libs/gl-sdk-napi/package-lock.json +++ b/libs/gl-sdk-napi/package-lock.json @@ -1,12 +1,12 @@ { "name": "@greenlightcln/glsdk", - "version": "0.0.3", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@greenlightcln/glsdk", - "version": "0.0.3", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@jest/globals": "^30.2.0", diff --git a/libs/gl-sdk-napi/src/lib.rs b/libs/gl-sdk-napi/src/lib.rs index ecec2f323..9a61e0447 100644 --- a/libs/gl-sdk-napi/src/lib.rs +++ b/libs/gl-sdk-napi/src/lib.rs @@ -31,9 +31,12 @@ pub struct ReceiveResponse { #[napi(object)] pub struct SendResponse { pub status: u32, - pub preimage: Buffer, - pub payment_hash: Buffer, - pub destination_pubkey: Option, + /// Preimage as lowercase hex (64 chars) + pub preimage: String, + /// Payment hash as lowercase hex (64 chars) + pub payment_hash: String, + /// Recipient node pubkey as lowercase hex (66 chars), if known + pub destination_pubkey: Option, /// Amount in millisatoshis (as i64 for JS compatibility) pub amount_msat: i64, /// Amount sent in millisatoshis (as i64 for JS compatibility) @@ -44,7 +47,8 @@ pub struct SendResponse { #[napi(object)] pub struct OnchainSendResponse { pub tx: Buffer, - pub txid: Buffer, + /// Transaction id as lowercase hex (64 chars) + pub txid: String, pub psbt: String, } @@ -60,9 +64,11 @@ pub struct OnchainReceiveResponse { #[napi(object)] pub struct InvoicePaidEvent { - pub payment_hash: Buffer, + /// Payment hash as lowercase hex (64 chars) + pub payment_hash: String, pub bolt11: String, - pub preimage: Buffer, + /// Preimage as lowercase hex (64 chars) + pub preimage: String, pub label: String, /// Amount in millisatoshis (as i64 for JS compatibility) pub amount_msat: i64, @@ -82,9 +88,11 @@ pub struct NodeEvent { #[napi(object)] pub struct GetInfoResponse { - pub id: Buffer, + /// Node public key as lowercase hex (66 chars) + pub id: String, pub alias: Option, - pub color: Buffer, + /// 3-byte RGB color as lowercase hex (6 chars) + pub color: String, pub num_peers: u32, pub num_pending_channels: u32, pub num_active_channels: u32, @@ -97,6 +105,51 @@ pub struct GetInfoResponse { pub fees_collected_msat: i64, } +#[napi(object)] +pub struct NodeState { + /// Node public key as lowercase hex (66 chars) + pub id: String, + pub block_height: u32, + pub network: String, + pub version: String, + pub alias: Option, + /// RGB color as lowercase hex (6 chars) + pub color: String, + pub num_active_channels: u32, + pub num_pending_channels: u32, + pub num_inactive_channels: u32, + /// Channel balance in millisatoshis (as i64 for JS compatibility) + pub channels_balance_msat: i64, + /// Max payable across all channels in millisatoshis (as i64 for JS compatibility) + pub max_payable_msat: i64, + /// Total channel capacity in millisatoshis (as i64 for JS compatibility) + pub total_channel_capacity_msat: i64, + /// Protocol channel reserve locked across channels in millisatoshis (as i64 for JS compatibility) + pub max_chan_reserve_msat: i64, + /// Confirmed on-chain balance in millisatoshis (as i64 for JS compatibility) + pub onchain_balance_msat: i64, + /// Unconfirmed on-chain balance in millisatoshis (as i64 for JS compatibility) + pub unconfirmed_onchain_balance_msat: i64, + /// Immature (timelocked coinbase) on-chain balance in millisatoshis (as i64 for JS compatibility) + pub immature_onchain_balance_msat: i64, + /// Balance from closing channels in millisatoshis (as i64 for JS compatibility) + pub pending_onchain_balance_msat: i64, + /// Largest receivable in a single payment in millisatoshis (as i64 for JS compatibility) + pub max_receivable_single_payment_msat: i64, + /// Total inbound liquidity in millisatoshis (as i64 for JS compatibility) + pub total_inbound_liquidity_msat: i64, + /// Peers we have a channel with and are currently connected to (lowercase hex pubkeys) + pub connected_channel_peers: Vec, + /// Unspent on-chain outputs (excludes spent) + pub utxos: Vec, + /// Confirmed + unconfirmed + immature on-chain in millisatoshis (as i64 for JS compatibility) + pub total_onchain_msat: i64, + /// All user funds summed in millisatoshis (as i64 for JS compatibility) + pub total_balance_msat: i64, + /// What the user can spend right now in millisatoshis (as i64 for JS compatibility) + pub spendable_balance_msat: i64, +} + // ============================================================================ // ListPeers Response Types // ============================================================================ @@ -108,7 +161,8 @@ pub struct ListPeersResponse { #[napi(object)] pub struct Peer { - pub id: Buffer, + /// Peer node public key as lowercase hex (66 chars) + pub id: String, pub connected: bool, pub num_channels: Option, pub netaddr: Vec, @@ -127,13 +181,16 @@ pub struct ListPeerChannelsResponse { #[napi(object)] pub struct PeerChannel { - pub peer_id: Buffer, + /// Peer node public key as lowercase hex (66 chars) + pub peer_id: String, pub peer_connected: bool, /// Channel state as string (e.g., "CHANNELD_NORMAL", "OPENINGD") pub state: String, pub short_channel_id: Option, - pub channel_id: Option, - pub funding_txid: Option, + /// Channel id as lowercase hex (64 chars) + pub channel_id: Option, + /// Funding transaction id as lowercase hex (64 chars) + pub funding_txid: Option, pub funding_outnum: Option, /// Balance to us in millisatoshis (as i64 for JS compatibility) pub to_us_msat: Option, @@ -157,7 +214,8 @@ pub struct ListFundsResponse { #[napi(object)] pub struct FundOutput { - pub txid: Buffer, + /// Transaction id as lowercase hex (64 chars) + pub txid: String, pub output: u32, /// Amount in millisatoshis (as i64 for JS compatibility) pub amount_msat: i64, @@ -165,22 +223,27 @@ pub struct FundOutput { pub status: String, pub address: Option, pub blockheight: Option, + /// True when this UTXO is reserved by an in-flight PSBT + pub reserved: bool, } #[napi(object)] pub struct FundChannel { - pub peer_id: Buffer, + /// Peer node public key as lowercase hex (66 chars) + pub peer_id: String, /// Our amount in millisatoshis (as i64 for JS compatibility) pub our_amount_msat: i64, /// Total amount in millisatoshis (as i64 for JS compatibility) pub amount_msat: i64, - pub funding_txid: Buffer, + /// Funding transaction id as lowercase hex (64 chars) + pub funding_txid: String, pub funding_output: u32, pub connected: bool, /// Channel state as string (e.g., "CHANNELD_NORMAL", "OPENINGD") pub state: String, pub short_channel_id: Option, - pub channel_id: Option, + /// Channel id as lowercase hex (64 chars) + pub channel_id: Option, } // ============================================================================ @@ -513,9 +576,9 @@ impl Node { Ok(SendResponse { status: response.status as u32, - preimage: Buffer::from(response.preimage), - payment_hash: Buffer::from(response.payment_hash), - destination_pubkey: response.destination_pubkey.map(Buffer::from), + preimage: response.preimage, + payment_hash: response.payment_hash, + destination_pubkey: response.destination_pubkey, amount_msat: response.amount_msat as i64, amount_sent_msat: response.amount_sent_msat as i64, parts: response.parts, @@ -544,7 +607,7 @@ impl Node { Ok(OnchainSendResponse { tx: Buffer::from(response.tx), - txid: Buffer::from(response.txid), + txid: response.txid, psbt: response.psbt, }) } @@ -606,9 +669,9 @@ impl Node { .map_err(|e| Error::from_reason(e.to_string()))??; Ok(GetInfoResponse { - id: Buffer::from(response.id), + id: response.id, alias: response.alias, - color: Buffer::from(response.color), + color: response.color, num_peers: response.num_peers, num_pending_channels: response.num_pending_channels, num_active_channels: response.num_active_channels, @@ -621,6 +684,57 @@ impl Node { }) } + #[napi] + pub async fn node_state(&self) -> Result { + let inner = self.inner.clone(); + let response = tokio::task::spawn_blocking(move || { + inner + .node_state() + .map_err(|e| Error::from_reason(e.to_string())) + }) + .await + .map_err(|e| Error::from_reason(e.to_string()))??; + + Ok(NodeState { + id: response.id, + block_height: response.block_height, + network: response.network, + version: response.version, + alias: response.alias, + color: response.color, + num_active_channels: response.num_active_channels, + num_pending_channels: response.num_pending_channels, + num_inactive_channels: response.num_inactive_channels, + channels_balance_msat: response.channels_balance_msat as i64, + max_payable_msat: response.max_payable_msat as i64, + total_channel_capacity_msat: response.total_channel_capacity_msat as i64, + max_chan_reserve_msat: response.max_chan_reserve_msat as i64, + onchain_balance_msat: response.onchain_balance_msat as i64, + unconfirmed_onchain_balance_msat: response.unconfirmed_onchain_balance_msat as i64, + immature_onchain_balance_msat: response.immature_onchain_balance_msat as i64, + pending_onchain_balance_msat: response.pending_onchain_balance_msat as i64, + max_receivable_single_payment_msat: response.max_receivable_single_payment_msat as i64, + total_inbound_liquidity_msat: response.total_inbound_liquidity_msat as i64, + connected_channel_peers: response.connected_channel_peers, + utxos: response + .utxos + .into_iter() + .map(|o| FundOutput { + txid: o.txid, + output: o.output, + amount_msat: o.amount_msat as i64, + status: output_status_to_string(&o.status), + address: o.address, + blockheight: o.blockheight, + reserved: o.reserved, + }) + .collect(), + total_onchain_msat: response.total_onchain_msat as i64, + total_balance_msat: response.total_balance_msat as i64, + spendable_balance_msat: response.spendable_balance_msat as i64, + }) + } + /// List all peers connected to this node /// /// Returns information about all peers including their connection status. @@ -640,7 +754,7 @@ impl Node { .peers .into_iter() .map(|p| Peer { - id: Buffer::from(p.id), + id: p.id, connected: p.connected, num_channels: p.num_channels, netaddr: p.netaddr, @@ -671,12 +785,12 @@ impl Node { .channels .into_iter() .map(|c| PeerChannel { - peer_id: Buffer::from(c.peer_id), + peer_id: c.peer_id, peer_connected: c.peer_connected, state: channel_state_to_string(&c.state), short_channel_id: c.short_channel_id, - channel_id: c.channel_id.map(Buffer::from), - funding_txid: c.funding_txid.map(Buffer::from), + channel_id: c.channel_id, + funding_txid: c.funding_txid, funding_outnum: c.funding_outnum, to_us_msat: c.to_us_msat.map(|v| v as i64), total_msat: c.total_msat.map(|v| v as i64), @@ -707,27 +821,28 @@ impl Node { .outputs .into_iter() .map(|o| FundOutput { - txid: Buffer::from(o.txid), + txid: o.txid, output: o.output, amount_msat: o.amount_msat as i64, status: output_status_to_string(&o.status), address: o.address, blockheight: o.blockheight, + reserved: o.reserved, }) .collect(), channels: response .channels .into_iter() .map(|c| FundChannel { - peer_id: Buffer::from(c.peer_id), + peer_id: c.peer_id, our_amount_msat: c.our_amount_msat as i64, amount_msat: c.amount_msat as i64, - funding_txid: Buffer::from(c.funding_txid), + funding_txid: c.funding_txid, funding_output: c.funding_output, connected: c.connected, state: channel_state_to_string(&c.state), short_channel_id: c.short_channel_id, - channel_id: c.channel_id.map(Buffer::from), + channel_id: c.channel_id, }) .collect(), }) @@ -744,9 +859,9 @@ fn napi_node_event_from_gl(event: GlNodeEvent) -> NodeEvent { GlNodeEvent::InvoicePaid { details } => NodeEvent { event_type: "invoice_paid".to_string(), invoice_paid: Some(InvoicePaidEvent { - payment_hash: Buffer::from(details.payment_hash), + payment_hash: details.payment_hash, bolt11: details.bolt11, - preimage: Buffer::from(details.preimage), + preimage: details.preimage, label: details.label, amount_msat: details.amount_msat as i64, }), @@ -773,6 +888,7 @@ fn channel_state_to_string(state: &GlChannelState) -> String { GlChannelState::DualopendAwaitingLockin => "DUALOPEND_AWAITING_LOCKIN".to_string(), GlChannelState::DualopendOpenCommitted => "DUALOPEND_OPEN_COMMITTED".to_string(), GlChannelState::DualopendOpenCommitReady => "DUALOPEND_OPEN_COMMIT_READY".to_string(), + GlChannelState::Unknown => "UNKNOWN".to_string(), } } diff --git a/libs/gl-sdk/glsdk/glsdk.py b/libs/gl-sdk/glsdk/glsdk.py index f7e91e884..c0ccc4694 100644 --- a/libs/gl-sdk/glsdk/glsdk.py +++ b/libs/gl-sdk/glsdk/glsdk.py @@ -486,7 +486,7 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_node_list_funds() != 21692: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_glsdk_checksum_method_node_list_invoices() != 6677: + if lib.uniffi_glsdk_checksum_method_node_list_invoices() != 44803: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_node_list_payments() != 43756: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") @@ -496,6 +496,8 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_node_list_peers() != 29567: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_method_node_node_state() != 41833: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_node_onchain_receive() != 46432: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_node_onchain_send() != 12590: @@ -793,6 +795,11 @@ class _UniffiForeignFutureStructVoid(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_glsdk_fn_method_node_list_peers.restype = _UniffiRustBuffer +_UniffiLib.uniffi_glsdk_fn_method_node_node_state.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_method_node_node_state.restype = _UniffiRustBuffer _UniffiLib.uniffi_glsdk_fn_method_node_onchain_receive.argtypes = ( ctypes.c_void_p, ctypes.POINTER(_UniffiRustCallStatus), @@ -1269,6 +1276,9 @@ class _UniffiForeignFutureStructVoid(ctypes.Structure): _UniffiLib.uniffi_glsdk_checksum_method_node_list_peers.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_method_node_list_peers.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_method_node_node_state.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_method_node_node_state.restype = ctypes.c_uint16 _UniffiLib.uniffi_glsdk_checksum_method_node_onchain_receive.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_method_node_onchain_receive.restype = ctypes.c_uint16 @@ -1456,16 +1466,28 @@ def write(value, buf): class FundChannel: - peer_id: "bytes" + peer_id: "str" + """ + Peer node public key as lowercase hex (66 chars). + """ + our_amount_msat: "int" amount_msat: "int" - funding_txid: "bytes" + funding_txid: "str" + """ + Funding transaction id as lowercase hex (64 chars). + """ + funding_output: "int" connected: "bool" state: "ChannelState" short_channel_id: "typing.Optional[str]" - channel_id: "typing.Optional[bytes]" - def __init__(self, *, peer_id: "bytes", our_amount_msat: "int", amount_msat: "int", funding_txid: "bytes", funding_output: "int", connected: "bool", state: "ChannelState", short_channel_id: "typing.Optional[str]", channel_id: "typing.Optional[bytes]"): + channel_id: "typing.Optional[str]" + """ + Channel id as lowercase hex (64 chars). + """ + + def __init__(self, *, peer_id: "str", our_amount_msat: "int", amount_msat: "int", funding_txid: "str", funding_output: "int", connected: "bool", state: "ChannelState", short_channel_id: "typing.Optional[str]", channel_id: "typing.Optional[str]"): self.peer_id = peer_id self.our_amount_msat = our_amount_msat self.amount_msat = amount_msat @@ -1504,59 +1526,72 @@ class _UniffiConverterTypeFundChannel(_UniffiConverterRustBuffer): @staticmethod def read(buf): return FundChannel( - peer_id=_UniffiConverterBytes.read(buf), + peer_id=_UniffiConverterString.read(buf), our_amount_msat=_UniffiConverterUInt64.read(buf), amount_msat=_UniffiConverterUInt64.read(buf), - funding_txid=_UniffiConverterBytes.read(buf), + funding_txid=_UniffiConverterString.read(buf), funding_output=_UniffiConverterUInt32.read(buf), connected=_UniffiConverterBool.read(buf), state=_UniffiConverterTypeChannelState.read(buf), short_channel_id=_UniffiConverterOptionalString.read(buf), - channel_id=_UniffiConverterOptionalBytes.read(buf), + channel_id=_UniffiConverterOptionalString.read(buf), ) @staticmethod def check_lower(value): - _UniffiConverterBytes.check_lower(value.peer_id) + _UniffiConverterString.check_lower(value.peer_id) _UniffiConverterUInt64.check_lower(value.our_amount_msat) _UniffiConverterUInt64.check_lower(value.amount_msat) - _UniffiConverterBytes.check_lower(value.funding_txid) + _UniffiConverterString.check_lower(value.funding_txid) _UniffiConverterUInt32.check_lower(value.funding_output) _UniffiConverterBool.check_lower(value.connected) _UniffiConverterTypeChannelState.check_lower(value.state) _UniffiConverterOptionalString.check_lower(value.short_channel_id) - _UniffiConverterOptionalBytes.check_lower(value.channel_id) + _UniffiConverterOptionalString.check_lower(value.channel_id) @staticmethod def write(value, buf): - _UniffiConverterBytes.write(value.peer_id, buf) + _UniffiConverterString.write(value.peer_id, buf) _UniffiConverterUInt64.write(value.our_amount_msat, buf) _UniffiConverterUInt64.write(value.amount_msat, buf) - _UniffiConverterBytes.write(value.funding_txid, buf) + _UniffiConverterString.write(value.funding_txid, buf) _UniffiConverterUInt32.write(value.funding_output, buf) _UniffiConverterBool.write(value.connected, buf) _UniffiConverterTypeChannelState.write(value.state, buf) _UniffiConverterOptionalString.write(value.short_channel_id, buf) - _UniffiConverterOptionalBytes.write(value.channel_id, buf) + _UniffiConverterOptionalString.write(value.channel_id, buf) class FundOutput: - txid: "bytes" + txid: "str" + """ + Transaction id as lowercase hex (64 chars). + """ + output: "int" amount_msat: "int" status: "OutputStatus" address: "typing.Optional[str]" blockheight: "typing.Optional[int]" - def __init__(self, *, txid: "bytes", output: "int", amount_msat: "int", status: "OutputStatus", address: "typing.Optional[str]", blockheight: "typing.Optional[int]"): + reserved: "bool" + """ + True when this UTXO is currently reserved by an in-flight PSBT + (e.g. a channel-open or fund-send that has not been broadcast or + abandoned). Reserved UTXOs are not spendable and must be excluded + from the wallet's spendable balance. + """ + + def __init__(self, *, txid: "str", output: "int", amount_msat: "int", status: "OutputStatus", address: "typing.Optional[str]", blockheight: "typing.Optional[int]", reserved: "bool"): self.txid = txid self.output = output self.amount_msat = amount_msat self.status = status self.address = address self.blockheight = blockheight + self.reserved = reserved def __str__(self): - return "FundOutput(txid={}, output={}, amount_msat={}, status={}, address={}, blockheight={})".format(self.txid, self.output, self.amount_msat, self.status, self.address, self.blockheight) + return "FundOutput(txid={}, output={}, amount_msat={}, status={}, address={}, blockheight={}, reserved={})".format(self.txid, self.output, self.amount_msat, self.status, self.address, self.blockheight, self.reserved) def __eq__(self, other): if self.txid != other.txid: @@ -1571,43 +1606,56 @@ def __eq__(self, other): return False if self.blockheight != other.blockheight: return False + if self.reserved != other.reserved: + return False return True class _UniffiConverterTypeFundOutput(_UniffiConverterRustBuffer): @staticmethod def read(buf): return FundOutput( - txid=_UniffiConverterBytes.read(buf), + txid=_UniffiConverterString.read(buf), output=_UniffiConverterUInt32.read(buf), amount_msat=_UniffiConverterUInt64.read(buf), status=_UniffiConverterTypeOutputStatus.read(buf), address=_UniffiConverterOptionalString.read(buf), blockheight=_UniffiConverterOptionalUInt32.read(buf), + reserved=_UniffiConverterBool.read(buf), ) @staticmethod def check_lower(value): - _UniffiConverterBytes.check_lower(value.txid) + _UniffiConverterString.check_lower(value.txid) _UniffiConverterUInt32.check_lower(value.output) _UniffiConverterUInt64.check_lower(value.amount_msat) _UniffiConverterTypeOutputStatus.check_lower(value.status) _UniffiConverterOptionalString.check_lower(value.address) _UniffiConverterOptionalUInt32.check_lower(value.blockheight) + _UniffiConverterBool.check_lower(value.reserved) @staticmethod def write(value, buf): - _UniffiConverterBytes.write(value.txid, buf) + _UniffiConverterString.write(value.txid, buf) _UniffiConverterUInt32.write(value.output, buf) _UniffiConverterUInt64.write(value.amount_msat, buf) _UniffiConverterTypeOutputStatus.write(value.status, buf) _UniffiConverterOptionalString.write(value.address, buf) _UniffiConverterOptionalUInt32.write(value.blockheight, buf) + _UniffiConverterBool.write(value.reserved, buf) class GetInfoResponse: - id: "bytes" + id: "str" + """ + Node public key as lowercase hex (66 chars). + """ + alias: "typing.Optional[str]" - color: "bytes" + color: "str" + """ + 3-byte RGB color as lowercase hex (6 chars). + """ + num_peers: "int" num_pending_channels: "int" num_active_channels: "int" @@ -1617,7 +1665,7 @@ class GetInfoResponse: blockheight: "int" network: "str" fees_collected_msat: "int" - def __init__(self, *, id: "bytes", alias: "typing.Optional[str]", color: "bytes", num_peers: "int", num_pending_channels: "int", num_active_channels: "int", num_inactive_channels: "int", version: "str", lightning_dir: "str", blockheight: "int", network: "str", fees_collected_msat: "int"): + def __init__(self, *, id: "str", alias: "typing.Optional[str]", color: "str", num_peers: "int", num_pending_channels: "int", num_active_channels: "int", num_inactive_channels: "int", version: "str", lightning_dir: "str", blockheight: "int", network: "str", fees_collected_msat: "int"): self.id = id self.alias = alias self.color = color @@ -1665,9 +1713,9 @@ class _UniffiConverterTypeGetInfoResponse(_UniffiConverterRustBuffer): @staticmethod def read(buf): return GetInfoResponse( - id=_UniffiConverterBytes.read(buf), + id=_UniffiConverterString.read(buf), alias=_UniffiConverterOptionalString.read(buf), - color=_UniffiConverterBytes.read(buf), + color=_UniffiConverterString.read(buf), num_peers=_UniffiConverterUInt32.read(buf), num_pending_channels=_UniffiConverterUInt32.read(buf), num_active_channels=_UniffiConverterUInt32.read(buf), @@ -1681,9 +1729,9 @@ def read(buf): @staticmethod def check_lower(value): - _UniffiConverterBytes.check_lower(value.id) + _UniffiConverterString.check_lower(value.id) _UniffiConverterOptionalString.check_lower(value.alias) - _UniffiConverterBytes.check_lower(value.color) + _UniffiConverterString.check_lower(value.color) _UniffiConverterUInt32.check_lower(value.num_peers) _UniffiConverterUInt32.check_lower(value.num_pending_channels) _UniffiConverterUInt32.check_lower(value.num_active_channels) @@ -1696,9 +1744,9 @@ def check_lower(value): @staticmethod def write(value, buf): - _UniffiConverterBytes.write(value.id, buf) + _UniffiConverterString.write(value.id, buf) _UniffiConverterOptionalString.write(value.alias, buf) - _UniffiConverterBytes.write(value.color, buf) + _UniffiConverterString.write(value.color, buf) _UniffiConverterUInt32.write(value.num_peers, buf) _UniffiConverterUInt32.write(value.num_pending_channels, buf) _UniffiConverterUInt32.write(value.num_active_channels, buf) @@ -1713,7 +1761,11 @@ def write(value, buf): class Invoice: label: "str" description: "str" - payment_hash: "bytes" + payment_hash: "str" + """ + Payment hash as lowercase hex (64 chars). + """ + status: "InvoiceStatus" amount_msat: "typing.Optional[int]" amount_received_msat: "typing.Optional[int]" @@ -1721,9 +1773,17 @@ class Invoice: bolt12: "typing.Optional[str]" paid_at: "typing.Optional[int]" expires_at: "int" - payment_preimage: "typing.Optional[bytes]" - destination_pubkey: "typing.Optional[bytes]" - def __init__(self, *, label: "str", description: "str", payment_hash: "bytes", status: "InvoiceStatus", amount_msat: "typing.Optional[int]", amount_received_msat: "typing.Optional[int]", bolt11: "typing.Optional[str]", bolt12: "typing.Optional[str]", paid_at: "typing.Optional[int]", expires_at: "int", payment_preimage: "typing.Optional[bytes]", destination_pubkey: "typing.Optional[bytes]"): + payment_preimage: "typing.Optional[str]" + """ + Payment preimage as lowercase hex (64 chars), if the invoice has been paid. + """ + + destination_pubkey: "typing.Optional[str]" + """ + Recipient node pubkey as lowercase hex (66 chars), recovered from the bolt11. + """ + + def __init__(self, *, label: "str", description: "str", payment_hash: "str", status: "InvoiceStatus", amount_msat: "typing.Optional[int]", amount_received_msat: "typing.Optional[int]", bolt11: "typing.Optional[str]", bolt12: "typing.Optional[str]", paid_at: "typing.Optional[int]", expires_at: "int", payment_preimage: "typing.Optional[str]", destination_pubkey: "typing.Optional[str]"): self.label = label self.description = description self.payment_hash = payment_hash @@ -1773,7 +1833,7 @@ def read(buf): return Invoice( label=_UniffiConverterString.read(buf), description=_UniffiConverterString.read(buf), - payment_hash=_UniffiConverterBytes.read(buf), + payment_hash=_UniffiConverterString.read(buf), status=_UniffiConverterTypeInvoiceStatus.read(buf), amount_msat=_UniffiConverterOptionalUInt64.read(buf), amount_received_msat=_UniffiConverterOptionalUInt64.read(buf), @@ -1781,15 +1841,15 @@ def read(buf): bolt12=_UniffiConverterOptionalString.read(buf), paid_at=_UniffiConverterOptionalUInt64.read(buf), expires_at=_UniffiConverterUInt64.read(buf), - payment_preimage=_UniffiConverterOptionalBytes.read(buf), - destination_pubkey=_UniffiConverterOptionalBytes.read(buf), + payment_preimage=_UniffiConverterOptionalString.read(buf), + destination_pubkey=_UniffiConverterOptionalString.read(buf), ) @staticmethod def check_lower(value): _UniffiConverterString.check_lower(value.label) _UniffiConverterString.check_lower(value.description) - _UniffiConverterBytes.check_lower(value.payment_hash) + _UniffiConverterString.check_lower(value.payment_hash) _UniffiConverterTypeInvoiceStatus.check_lower(value.status) _UniffiConverterOptionalUInt64.check_lower(value.amount_msat) _UniffiConverterOptionalUInt64.check_lower(value.amount_received_msat) @@ -1797,14 +1857,14 @@ def check_lower(value): _UniffiConverterOptionalString.check_lower(value.bolt12) _UniffiConverterOptionalUInt64.check_lower(value.paid_at) _UniffiConverterUInt64.check_lower(value.expires_at) - _UniffiConverterOptionalBytes.check_lower(value.payment_preimage) - _UniffiConverterOptionalBytes.check_lower(value.destination_pubkey) + _UniffiConverterOptionalString.check_lower(value.payment_preimage) + _UniffiConverterOptionalString.check_lower(value.destination_pubkey) @staticmethod def write(value, buf): _UniffiConverterString.write(value.label, buf) _UniffiConverterString.write(value.description, buf) - _UniffiConverterBytes.write(value.payment_hash, buf) + _UniffiConverterString.write(value.payment_hash, buf) _UniffiConverterTypeInvoiceStatus.write(value.status, buf) _UniffiConverterOptionalUInt64.write(value.amount_msat, buf) _UniffiConverterOptionalUInt64.write(value.amount_received_msat, buf) @@ -1812,8 +1872,8 @@ def write(value, buf): _UniffiConverterOptionalString.write(value.bolt12, buf) _UniffiConverterOptionalUInt64.write(value.paid_at, buf) _UniffiConverterUInt64.write(value.expires_at, buf) - _UniffiConverterOptionalBytes.write(value.payment_preimage, buf) - _UniffiConverterOptionalBytes.write(value.destination_pubkey, buf) + _UniffiConverterOptionalString.write(value.payment_preimage, buf) + _UniffiConverterOptionalString.write(value.destination_pubkey, buf) class InvoicePaidEvent: @@ -1821,9 +1881,9 @@ class InvoicePaidEvent: Details of a paid invoice. """ - payment_hash: "bytes" + payment_hash: "str" """ - The payment hash of the paid invoice. + Payment hash of the paid invoice as lowercase hex (64 chars). """ bolt11: "str" @@ -1831,9 +1891,9 @@ class InvoicePaidEvent: The bolt11 invoice string. """ - preimage: "bytes" + preimage: "str" """ - The preimage that proves payment. + Preimage that proves payment as lowercase hex (64 chars). """ label: "str" @@ -1846,7 +1906,7 @@ class InvoicePaidEvent: Amount received in millisatoshis. """ - def __init__(self, *, payment_hash: "bytes", bolt11: "str", preimage: "bytes", label: "str", amount_msat: "int"): + def __init__(self, *, payment_hash: "str", bolt11: "str", preimage: "str", label: "str", amount_msat: "int"): self.payment_hash = payment_hash self.bolt11 = bolt11 self.preimage = preimage @@ -1873,26 +1933,26 @@ class _UniffiConverterTypeInvoicePaidEvent(_UniffiConverterRustBuffer): @staticmethod def read(buf): return InvoicePaidEvent( - payment_hash=_UniffiConverterBytes.read(buf), + payment_hash=_UniffiConverterString.read(buf), bolt11=_UniffiConverterString.read(buf), - preimage=_UniffiConverterBytes.read(buf), + preimage=_UniffiConverterString.read(buf), label=_UniffiConverterString.read(buf), amount_msat=_UniffiConverterUInt64.read(buf), ) @staticmethod def check_lower(value): - _UniffiConverterBytes.check_lower(value.payment_hash) + _UniffiConverterString.check_lower(value.payment_hash) _UniffiConverterString.check_lower(value.bolt11) - _UniffiConverterBytes.check_lower(value.preimage) + _UniffiConverterString.check_lower(value.preimage) _UniffiConverterString.check_lower(value.label) _UniffiConverterUInt64.check_lower(value.amount_msat) @staticmethod def write(value, buf): - _UniffiConverterBytes.write(value.payment_hash, buf) + _UniffiConverterString.write(value.payment_hash, buf) _UniffiConverterString.write(value.bolt11, buf) - _UniffiConverterBytes.write(value.preimage, buf) + _UniffiConverterString.write(value.preimage, buf) _UniffiConverterString.write(value.label, buf) _UniffiConverterUInt64.write(value.amount_msat, buf) @@ -2137,6 +2197,347 @@ def write(value, buf): _UniffiConverterSequenceTypePeer.write(value.peers, buf) +class NodeState: + """ + A point-in-time snapshot of the node's balances, capacity, and + connectivity. Returned by `node_state()`. + + All amounts are in millisatoshis (1 sat = 1000 msat). + """ + + id: "str" + """ + The node's public key as a lowercase hex string (66 chars). + """ + + block_height: "int" + """ + Latest block height the node has synced to. + """ + + network: "str" + """ + The Bitcoin network this node is running on (e.g. "bitcoin", "regtest"). + """ + + version: "str" + """ + CLN version string (e.g. "v24.11"). + """ + + alias: "typing.Optional[str]" + """ + Human-readable node alias, if set. + """ + + color: "str" + """ + 3-byte RGB color of the node, as a lowercase hex string (6 chars). + """ + + num_active_channels: "int" + """ + Number of channels that are open and operational. These are the + channels that contribute to `channels_balance_msat`, + `max_payable_msat`, `total_channel_capacity_msat`, and + `total_inbound_liquidity_msat`. + """ + + num_pending_channels: "int" + """ + Number of channels that are being opened but not yet confirmed. + Pending channels do not contribute to any balance or capacity + field on this snapshot; their funds show up only after they + transition to active. + """ + + num_inactive_channels: "int" + """ + Number of channels that are open but the peer is offline. + Inactive channels hold balance but cannot be used for payments + until the peer reconnects; they do not contribute to + `max_payable_msat` or `total_inbound_liquidity_msat` (those are + computed from the live `spendable_msat` / `receivable_msat` + reported by CLN, which goes to zero when the peer is offline). + """ + + channels_balance_msat: "int" + """ + Total our-side balance across all open channels, including amounts + that protocol reserves make unspendable. + + This is the field a wallet's home screen should show as the + user's "Lightning balance" — it reflects what they own off-chain, + matching what they'd expect to see at a glance. + + Do **not** use this to gate a send button: some of it is locked + in channel reserves. Use `max_payable_msat` for that. + """ + + max_payable_msat: "int" + """ + Aggregate spendable amount across all open channels. Equal to + `channels_balance_msat - max_chan_reserve_msat`. + + This is the field a send screen should gate against — it is what + the user can actually move right now over Lightning in total. + + Caveat: a single Lightning payment is additionally bounded by + the largest channel's own `spendable_msat`. Reaching this full + aggregate amount in one payment requires multi-path-payment + support from the recipient and a working route. + """ + + total_channel_capacity_msat: "int" + """ + Sum of all open channel capacities (your side + remote side). + """ + + max_chan_reserve_msat: "int" + """ + Amount locked in protocol channel reserves, computed as + `channels_balance_msat - max_payable_msat`. These sats are yours + on paper but cannot be spent until the channel closes. + """ + + onchain_balance_msat: "int" + """ + Confirmed on-chain balance available for spending or opening channels. + """ + + unconfirmed_onchain_balance_msat: "int" + """ + On-chain balance from transactions that have not yet been confirmed. + """ + + immature_onchain_balance_msat: "int" + """ + On-chain balance confirmed but not yet spendable (e.g. coinbase + outputs inside the 100-block maturation window). + """ + + pending_onchain_balance_msat: "int" + """ + On-chain balance locked in channels that are being closed. + These funds will become available once the close is confirmed. + """ + + max_receivable_single_payment_msat: "int" + """ + Largest single Lightning payment the node can receive without + splitting across channels. Bounded by the inbound capacity of + the largest open channel. + """ + + total_inbound_liquidity_msat: "int" + """ + Total amount you can receive across all open channels combined. + """ + + connected_channel_peers: "typing.List[str]" + """ + Lowercase hex public keys of peers we have at least one channel + with and are currently connected to. Peers we're connected to but + have no channel with are not represented here; for routing-node + use cases, query `list_peers()` directly. + """ + + utxos: "typing.List[FundOutput]" + """ + Unspent on-chain outputs owned by the node's wallet. Excludes + spent outputs; includes confirmed, unconfirmed, immature, and + reserved UTXOs (callers can filter by `status` and `reserved`). + """ + + total_onchain_msat: "int" + """ + All non-pending on-chain balance buckets summed: + `onchain_balance_msat + unconfirmed_onchain_balance_msat + immature_onchain_balance_msat`. + Excludes funds locked in closing channels (`pending_onchain_balance_msat`) + since those are not yet on-chain UTXOs. + """ + + total_balance_msat: "int" + """ + Everything the user owns, summed: channel balance (including + protocol reserves) + all on-chain buckets + funds locked in + closing channels. The "total holdings" number a wallet home + screen typically shows. + """ + + spendable_balance_msat: "int" + """ + What the user can spend *right now*: + `max_payable_msat + onchain_balance_msat`. Excludes reserves, + unconfirmed, immature, and pending amounts. The number a + send-money screen should gate against. + """ + + def __init__(self, *, id: "str", block_height: "int", network: "str", version: "str", alias: "typing.Optional[str]", color: "str", num_active_channels: "int", num_pending_channels: "int", num_inactive_channels: "int", channels_balance_msat: "int", max_payable_msat: "int", total_channel_capacity_msat: "int", max_chan_reserve_msat: "int", onchain_balance_msat: "int", unconfirmed_onchain_balance_msat: "int", immature_onchain_balance_msat: "int", pending_onchain_balance_msat: "int", max_receivable_single_payment_msat: "int", total_inbound_liquidity_msat: "int", connected_channel_peers: "typing.List[str]", utxos: "typing.List[FundOutput]", total_onchain_msat: "int", total_balance_msat: "int", spendable_balance_msat: "int"): + self.id = id + self.block_height = block_height + self.network = network + self.version = version + self.alias = alias + self.color = color + self.num_active_channels = num_active_channels + self.num_pending_channels = num_pending_channels + self.num_inactive_channels = num_inactive_channels + self.channels_balance_msat = channels_balance_msat + self.max_payable_msat = max_payable_msat + self.total_channel_capacity_msat = total_channel_capacity_msat + self.max_chan_reserve_msat = max_chan_reserve_msat + self.onchain_balance_msat = onchain_balance_msat + self.unconfirmed_onchain_balance_msat = unconfirmed_onchain_balance_msat + self.immature_onchain_balance_msat = immature_onchain_balance_msat + self.pending_onchain_balance_msat = pending_onchain_balance_msat + self.max_receivable_single_payment_msat = max_receivable_single_payment_msat + self.total_inbound_liquidity_msat = total_inbound_liquidity_msat + self.connected_channel_peers = connected_channel_peers + self.utxos = utxos + self.total_onchain_msat = total_onchain_msat + self.total_balance_msat = total_balance_msat + self.spendable_balance_msat = spendable_balance_msat + + def __str__(self): + return "NodeState(id={}, block_height={}, network={}, version={}, alias={}, color={}, num_active_channels={}, num_pending_channels={}, num_inactive_channels={}, channels_balance_msat={}, max_payable_msat={}, total_channel_capacity_msat={}, max_chan_reserve_msat={}, onchain_balance_msat={}, unconfirmed_onchain_balance_msat={}, immature_onchain_balance_msat={}, pending_onchain_balance_msat={}, max_receivable_single_payment_msat={}, total_inbound_liquidity_msat={}, connected_channel_peers={}, utxos={}, total_onchain_msat={}, total_balance_msat={}, spendable_balance_msat={})".format(self.id, self.block_height, self.network, self.version, self.alias, self.color, self.num_active_channels, self.num_pending_channels, self.num_inactive_channels, self.channels_balance_msat, self.max_payable_msat, self.total_channel_capacity_msat, self.max_chan_reserve_msat, self.onchain_balance_msat, self.unconfirmed_onchain_balance_msat, self.immature_onchain_balance_msat, self.pending_onchain_balance_msat, self.max_receivable_single_payment_msat, self.total_inbound_liquidity_msat, self.connected_channel_peers, self.utxos, self.total_onchain_msat, self.total_balance_msat, self.spendable_balance_msat) + + def __eq__(self, other): + if self.id != other.id: + return False + if self.block_height != other.block_height: + return False + if self.network != other.network: + return False + if self.version != other.version: + return False + if self.alias != other.alias: + return False + if self.color != other.color: + return False + if self.num_active_channels != other.num_active_channels: + return False + if self.num_pending_channels != other.num_pending_channels: + return False + if self.num_inactive_channels != other.num_inactive_channels: + return False + if self.channels_balance_msat != other.channels_balance_msat: + return False + if self.max_payable_msat != other.max_payable_msat: + return False + if self.total_channel_capacity_msat != other.total_channel_capacity_msat: + return False + if self.max_chan_reserve_msat != other.max_chan_reserve_msat: + return False + if self.onchain_balance_msat != other.onchain_balance_msat: + return False + if self.unconfirmed_onchain_balance_msat != other.unconfirmed_onchain_balance_msat: + return False + if self.immature_onchain_balance_msat != other.immature_onchain_balance_msat: + return False + if self.pending_onchain_balance_msat != other.pending_onchain_balance_msat: + return False + if self.max_receivable_single_payment_msat != other.max_receivable_single_payment_msat: + return False + if self.total_inbound_liquidity_msat != other.total_inbound_liquidity_msat: + return False + if self.connected_channel_peers != other.connected_channel_peers: + return False + if self.utxos != other.utxos: + return False + if self.total_onchain_msat != other.total_onchain_msat: + return False + if self.total_balance_msat != other.total_balance_msat: + return False + if self.spendable_balance_msat != other.spendable_balance_msat: + return False + return True + +class _UniffiConverterTypeNodeState(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return NodeState( + id=_UniffiConverterString.read(buf), + block_height=_UniffiConverterUInt32.read(buf), + network=_UniffiConverterString.read(buf), + version=_UniffiConverterString.read(buf), + alias=_UniffiConverterOptionalString.read(buf), + color=_UniffiConverterString.read(buf), + num_active_channels=_UniffiConverterUInt32.read(buf), + num_pending_channels=_UniffiConverterUInt32.read(buf), + num_inactive_channels=_UniffiConverterUInt32.read(buf), + channels_balance_msat=_UniffiConverterUInt64.read(buf), + max_payable_msat=_UniffiConverterUInt64.read(buf), + total_channel_capacity_msat=_UniffiConverterUInt64.read(buf), + max_chan_reserve_msat=_UniffiConverterUInt64.read(buf), + onchain_balance_msat=_UniffiConverterUInt64.read(buf), + unconfirmed_onchain_balance_msat=_UniffiConverterUInt64.read(buf), + immature_onchain_balance_msat=_UniffiConverterUInt64.read(buf), + pending_onchain_balance_msat=_UniffiConverterUInt64.read(buf), + max_receivable_single_payment_msat=_UniffiConverterUInt64.read(buf), + total_inbound_liquidity_msat=_UniffiConverterUInt64.read(buf), + connected_channel_peers=_UniffiConverterSequenceString.read(buf), + utxos=_UniffiConverterSequenceTypeFundOutput.read(buf), + total_onchain_msat=_UniffiConverterUInt64.read(buf), + total_balance_msat=_UniffiConverterUInt64.read(buf), + spendable_balance_msat=_UniffiConverterUInt64.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterString.check_lower(value.id) + _UniffiConverterUInt32.check_lower(value.block_height) + _UniffiConverterString.check_lower(value.network) + _UniffiConverterString.check_lower(value.version) + _UniffiConverterOptionalString.check_lower(value.alias) + _UniffiConverterString.check_lower(value.color) + _UniffiConverterUInt32.check_lower(value.num_active_channels) + _UniffiConverterUInt32.check_lower(value.num_pending_channels) + _UniffiConverterUInt32.check_lower(value.num_inactive_channels) + _UniffiConverterUInt64.check_lower(value.channels_balance_msat) + _UniffiConverterUInt64.check_lower(value.max_payable_msat) + _UniffiConverterUInt64.check_lower(value.total_channel_capacity_msat) + _UniffiConverterUInt64.check_lower(value.max_chan_reserve_msat) + _UniffiConverterUInt64.check_lower(value.onchain_balance_msat) + _UniffiConverterUInt64.check_lower(value.unconfirmed_onchain_balance_msat) + _UniffiConverterUInt64.check_lower(value.immature_onchain_balance_msat) + _UniffiConverterUInt64.check_lower(value.pending_onchain_balance_msat) + _UniffiConverterUInt64.check_lower(value.max_receivable_single_payment_msat) + _UniffiConverterUInt64.check_lower(value.total_inbound_liquidity_msat) + _UniffiConverterSequenceString.check_lower(value.connected_channel_peers) + _UniffiConverterSequenceTypeFundOutput.check_lower(value.utxos) + _UniffiConverterUInt64.check_lower(value.total_onchain_msat) + _UniffiConverterUInt64.check_lower(value.total_balance_msat) + _UniffiConverterUInt64.check_lower(value.spendable_balance_msat) + + @staticmethod + def write(value, buf): + _UniffiConverterString.write(value.id, buf) + _UniffiConverterUInt32.write(value.block_height, buf) + _UniffiConverterString.write(value.network, buf) + _UniffiConverterString.write(value.version, buf) + _UniffiConverterOptionalString.write(value.alias, buf) + _UniffiConverterString.write(value.color, buf) + _UniffiConverterUInt32.write(value.num_active_channels, buf) + _UniffiConverterUInt32.write(value.num_pending_channels, buf) + _UniffiConverterUInt32.write(value.num_inactive_channels, buf) + _UniffiConverterUInt64.write(value.channels_balance_msat, buf) + _UniffiConverterUInt64.write(value.max_payable_msat, buf) + _UniffiConverterUInt64.write(value.total_channel_capacity_msat, buf) + _UniffiConverterUInt64.write(value.max_chan_reserve_msat, buf) + _UniffiConverterUInt64.write(value.onchain_balance_msat, buf) + _UniffiConverterUInt64.write(value.unconfirmed_onchain_balance_msat, buf) + _UniffiConverterUInt64.write(value.immature_onchain_balance_msat, buf) + _UniffiConverterUInt64.write(value.pending_onchain_balance_msat, buf) + _UniffiConverterUInt64.write(value.max_receivable_single_payment_msat, buf) + _UniffiConverterUInt64.write(value.total_inbound_liquidity_msat, buf) + _UniffiConverterSequenceString.write(value.connected_channel_peers, buf) + _UniffiConverterSequenceTypeFundOutput.write(value.utxos, buf) + _UniffiConverterUInt64.write(value.total_onchain_msat, buf) + _UniffiConverterUInt64.write(value.total_balance_msat, buf) + _UniffiConverterUInt64.write(value.spendable_balance_msat, buf) + + class OnchainReceiveResponse: """ A pair of on-chain addresses for receiving funds. @@ -2195,9 +2596,9 @@ class OnchainSendResponse: The raw signed transaction bytes. """ - txid: "bytes" + txid: "str" """ - The transaction ID (32 bytes, reversed byte order as is standard). + The transaction id as lowercase hex (64 chars). """ psbt: "str" @@ -2205,7 +2606,7 @@ class OnchainSendResponse: The transaction as a Partially Signed Bitcoin Transaction string. """ - def __init__(self, *, tx: "bytes", txid: "bytes", psbt: "str"): + def __init__(self, *, tx: "bytes", txid: "str", psbt: "str"): self.tx = tx self.txid = txid self.psbt = psbt @@ -2227,20 +2628,20 @@ class _UniffiConverterTypeOnchainSendResponse(_UniffiConverterRustBuffer): def read(buf): return OnchainSendResponse( tx=_UniffiConverterBytes.read(buf), - txid=_UniffiConverterBytes.read(buf), + txid=_UniffiConverterString.read(buf), psbt=_UniffiConverterString.read(buf), ) @staticmethod def check_lower(value): _UniffiConverterBytes.check_lower(value.tx) - _UniffiConverterBytes.check_lower(value.txid) + _UniffiConverterString.check_lower(value.txid) _UniffiConverterString.check_lower(value.psbt) @staticmethod def write(value, buf): _UniffiConverterBytes.write(value.tx, buf) - _UniffiConverterBytes.write(value.txid, buf) + _UniffiConverterString.write(value.txid, buf) _UniffiConverterString.write(value.psbt, buf) @@ -2254,14 +2655,14 @@ class ParsedInvoice: The original invoice string. """ - payee_pubkey: "typing.Optional[bytes]" + payee_pubkey: "typing.Optional[str]" """ - 33-byte recipient public key, recovered from the invoice signature. + Recipient public key as lowercase hex (66 chars), recovered from the invoice signature. """ - payment_hash: "bytes" + payment_hash: "str" """ - 32-byte payment hash identifying this payment. + Payment hash as lowercase hex (64 chars) identifying this payment. """ description: "typing.Optional[str]" @@ -2284,7 +2685,7 @@ class ParsedInvoice: Unix timestamp (seconds) when the invoice was created. """ - def __init__(self, *, bolt11: "str", payee_pubkey: "typing.Optional[bytes]", payment_hash: "bytes", description: "typing.Optional[str]", amount_msat: "typing.Optional[int]", expiry: "int", timestamp: "int"): + def __init__(self, *, bolt11: "str", payee_pubkey: "typing.Optional[str]", payment_hash: "str", description: "typing.Optional[str]", amount_msat: "typing.Optional[int]", expiry: "int", timestamp: "int"): self.bolt11 = bolt11 self.payee_pubkey = payee_pubkey self.payment_hash = payment_hash @@ -2318,8 +2719,8 @@ class _UniffiConverterTypeParsedInvoice(_UniffiConverterRustBuffer): def read(buf): return ParsedInvoice( bolt11=_UniffiConverterString.read(buf), - payee_pubkey=_UniffiConverterOptionalBytes.read(buf), - payment_hash=_UniffiConverterBytes.read(buf), + payee_pubkey=_UniffiConverterOptionalString.read(buf), + payment_hash=_UniffiConverterString.read(buf), description=_UniffiConverterOptionalString.read(buf), amount_msat=_UniffiConverterOptionalUInt64.read(buf), expiry=_UniffiConverterUInt64.read(buf), @@ -2329,8 +2730,8 @@ def read(buf): @staticmethod def check_lower(value): _UniffiConverterString.check_lower(value.bolt11) - _UniffiConverterOptionalBytes.check_lower(value.payee_pubkey) - _UniffiConverterBytes.check_lower(value.payment_hash) + _UniffiConverterOptionalString.check_lower(value.payee_pubkey) + _UniffiConverterString.check_lower(value.payment_hash) _UniffiConverterOptionalString.check_lower(value.description) _UniffiConverterOptionalUInt64.check_lower(value.amount_msat) _UniffiConverterUInt64.check_lower(value.expiry) @@ -2339,8 +2740,8 @@ def check_lower(value): @staticmethod def write(value, buf): _UniffiConverterString.write(value.bolt11, buf) - _UniffiConverterOptionalBytes.write(value.payee_pubkey, buf) - _UniffiConverterBytes.write(value.payment_hash, buf) + _UniffiConverterOptionalString.write(value.payee_pubkey, buf) + _UniffiConverterString.write(value.payment_hash, buf) _UniffiConverterOptionalString.write(value.description, buf) _UniffiConverterOptionalUInt64.write(value.amount_msat, buf) _UniffiConverterUInt64.write(value.expiry, buf) @@ -2348,20 +2749,32 @@ def write(value, buf): class Pay: - payment_hash: "bytes" + payment_hash: "str" + """ + Payment hash as lowercase hex (64 chars). + """ + status: "PayStatus" - destination_pubkey: "typing.Optional[bytes]" + destination_pubkey: "typing.Optional[str]" + """ + Recipient node pubkey as lowercase hex (66 chars), if known. + """ + amount_msat: "typing.Optional[int]" amount_sent_msat: "typing.Optional[int]" label: "typing.Optional[str]" bolt11: "typing.Optional[str]" description: "typing.Optional[str]" bolt12: "typing.Optional[str]" - preimage: "typing.Optional[bytes]" + preimage: "typing.Optional[str]" + """ + Payment preimage as lowercase hex (64 chars), if the payment completed. + """ + created_at: "int" completed_at: "typing.Optional[int]" number_of_parts: "typing.Optional[int]" - def __init__(self, *, payment_hash: "bytes", status: "PayStatus", destination_pubkey: "typing.Optional[bytes]", amount_msat: "typing.Optional[int]", amount_sent_msat: "typing.Optional[int]", label: "typing.Optional[str]", bolt11: "typing.Optional[str]", description: "typing.Optional[str]", bolt12: "typing.Optional[str]", preimage: "typing.Optional[bytes]", created_at: "int", completed_at: "typing.Optional[int]", number_of_parts: "typing.Optional[int]"): + def __init__(self, *, payment_hash: "str", status: "PayStatus", destination_pubkey: "typing.Optional[str]", amount_msat: "typing.Optional[int]", amount_sent_msat: "typing.Optional[int]", label: "typing.Optional[str]", bolt11: "typing.Optional[str]", description: "typing.Optional[str]", bolt12: "typing.Optional[str]", preimage: "typing.Optional[str]", created_at: "int", completed_at: "typing.Optional[int]", number_of_parts: "typing.Optional[int]"): self.payment_hash = payment_hash self.status = status self.destination_pubkey = destination_pubkey @@ -2412,16 +2825,16 @@ class _UniffiConverterTypePay(_UniffiConverterRustBuffer): @staticmethod def read(buf): return Pay( - payment_hash=_UniffiConverterBytes.read(buf), + payment_hash=_UniffiConverterString.read(buf), status=_UniffiConverterTypePayStatus.read(buf), - destination_pubkey=_UniffiConverterOptionalBytes.read(buf), + destination_pubkey=_UniffiConverterOptionalString.read(buf), amount_msat=_UniffiConverterOptionalUInt64.read(buf), amount_sent_msat=_UniffiConverterOptionalUInt64.read(buf), label=_UniffiConverterOptionalString.read(buf), bolt11=_UniffiConverterOptionalString.read(buf), description=_UniffiConverterOptionalString.read(buf), bolt12=_UniffiConverterOptionalString.read(buf), - preimage=_UniffiConverterOptionalBytes.read(buf), + preimage=_UniffiConverterOptionalString.read(buf), created_at=_UniffiConverterUInt64.read(buf), completed_at=_UniffiConverterOptionalUInt64.read(buf), number_of_parts=_UniffiConverterOptionalUInt64.read(buf), @@ -2429,32 +2842,32 @@ def read(buf): @staticmethod def check_lower(value): - _UniffiConverterBytes.check_lower(value.payment_hash) + _UniffiConverterString.check_lower(value.payment_hash) _UniffiConverterTypePayStatus.check_lower(value.status) - _UniffiConverterOptionalBytes.check_lower(value.destination_pubkey) + _UniffiConverterOptionalString.check_lower(value.destination_pubkey) _UniffiConverterOptionalUInt64.check_lower(value.amount_msat) _UniffiConverterOptionalUInt64.check_lower(value.amount_sent_msat) _UniffiConverterOptionalString.check_lower(value.label) _UniffiConverterOptionalString.check_lower(value.bolt11) _UniffiConverterOptionalString.check_lower(value.description) _UniffiConverterOptionalString.check_lower(value.bolt12) - _UniffiConverterOptionalBytes.check_lower(value.preimage) + _UniffiConverterOptionalString.check_lower(value.preimage) _UniffiConverterUInt64.check_lower(value.created_at) _UniffiConverterOptionalUInt64.check_lower(value.completed_at) _UniffiConverterOptionalUInt64.check_lower(value.number_of_parts) @staticmethod def write(value, buf): - _UniffiConverterBytes.write(value.payment_hash, buf) + _UniffiConverterString.write(value.payment_hash, buf) _UniffiConverterTypePayStatus.write(value.status, buf) - _UniffiConverterOptionalBytes.write(value.destination_pubkey, buf) + _UniffiConverterOptionalString.write(value.destination_pubkey, buf) _UniffiConverterOptionalUInt64.write(value.amount_msat, buf) _UniffiConverterOptionalUInt64.write(value.amount_sent_msat, buf) _UniffiConverterOptionalString.write(value.label, buf) _UniffiConverterOptionalString.write(value.bolt11, buf) _UniffiConverterOptionalString.write(value.description, buf) _UniffiConverterOptionalString.write(value.bolt12, buf) - _UniffiConverterOptionalBytes.write(value.preimage, buf) + _UniffiConverterOptionalString.write(value.preimage, buf) _UniffiConverterUInt64.write(value.created_at, buf) _UniffiConverterOptionalUInt64.write(value.completed_at, buf) _UniffiConverterOptionalUInt64.write(value.number_of_parts, buf) @@ -2469,9 +2882,17 @@ class Payment: status: "PaymentStatus" description: "typing.Optional[str]" bolt11: "typing.Optional[str]" - preimage: "typing.Optional[bytes]" - destination: "typing.Optional[bytes]" - def __init__(self, *, id: "str", payment_type: "PaymentType", payment_time: "int", amount_msat: "int", fee_msat: "int", status: "PaymentStatus", description: "typing.Optional[str]", bolt11: "typing.Optional[str]", preimage: "typing.Optional[bytes]", destination: "typing.Optional[bytes]"): + preimage: "typing.Optional[str]" + """ + Payment preimage as lowercase hex (64 chars), when the payment is known. + """ + + destination: "typing.Optional[str]" + """ + Counterparty node pubkey as lowercase hex (66 chars), when known. + """ + + def __init__(self, *, id: "str", payment_type: "PaymentType", payment_time: "int", amount_msat: "int", fee_msat: "int", status: "PaymentStatus", description: "typing.Optional[str]", bolt11: "typing.Optional[str]", preimage: "typing.Optional[str]", destination: "typing.Optional[str]"): self.id = id self.payment_type = payment_type self.payment_time = payment_time @@ -2521,8 +2942,8 @@ def read(buf): status=_UniffiConverterTypePaymentStatus.read(buf), description=_UniffiConverterOptionalString.read(buf), bolt11=_UniffiConverterOptionalString.read(buf), - preimage=_UniffiConverterOptionalBytes.read(buf), - destination=_UniffiConverterOptionalBytes.read(buf), + preimage=_UniffiConverterOptionalString.read(buf), + destination=_UniffiConverterOptionalString.read(buf), ) @staticmethod @@ -2535,8 +2956,8 @@ def check_lower(value): _UniffiConverterTypePaymentStatus.check_lower(value.status) _UniffiConverterOptionalString.check_lower(value.description) _UniffiConverterOptionalString.check_lower(value.bolt11) - _UniffiConverterOptionalBytes.check_lower(value.preimage) - _UniffiConverterOptionalBytes.check_lower(value.destination) + _UniffiConverterOptionalString.check_lower(value.preimage) + _UniffiConverterOptionalString.check_lower(value.destination) @staticmethod def write(value, buf): @@ -2548,18 +2969,22 @@ def write(value, buf): _UniffiConverterTypePaymentStatus.write(value.status, buf) _UniffiConverterOptionalString.write(value.description, buf) _UniffiConverterOptionalString.write(value.bolt11, buf) - _UniffiConverterOptionalBytes.write(value.preimage, buf) - _UniffiConverterOptionalBytes.write(value.destination, buf) + _UniffiConverterOptionalString.write(value.preimage, buf) + _UniffiConverterOptionalString.write(value.destination, buf) class Peer: - id: "bytes" + id: "str" + """ + Peer node public key as lowercase hex (66 chars). + """ + connected: "bool" num_channels: "typing.Optional[int]" netaddr: "typing.List[str]" remote_addr: "typing.Optional[str]" features: "typing.Optional[bytes]" - def __init__(self, *, id: "bytes", connected: "bool", num_channels: "typing.Optional[int]", netaddr: "typing.List[str]", remote_addr: "typing.Optional[str]", features: "typing.Optional[bytes]"): + def __init__(self, *, id: "str", connected: "bool", num_channels: "typing.Optional[int]", netaddr: "typing.List[str]", remote_addr: "typing.Optional[str]", features: "typing.Optional[bytes]"): self.id = id self.connected = connected self.num_channels = num_channels @@ -2589,7 +3014,7 @@ class _UniffiConverterTypePeer(_UniffiConverterRustBuffer): @staticmethod def read(buf): return Peer( - id=_UniffiConverterBytes.read(buf), + id=_UniffiConverterString.read(buf), connected=_UniffiConverterBool.read(buf), num_channels=_UniffiConverterOptionalUInt32.read(buf), netaddr=_UniffiConverterSequenceString.read(buf), @@ -2599,7 +3024,7 @@ def read(buf): @staticmethod def check_lower(value): - _UniffiConverterBytes.check_lower(value.id) + _UniffiConverterString.check_lower(value.id) _UniffiConverterBool.check_lower(value.connected) _UniffiConverterOptionalUInt32.check_lower(value.num_channels) _UniffiConverterSequenceString.check_lower(value.netaddr) @@ -2608,7 +3033,7 @@ def check_lower(value): @staticmethod def write(value, buf): - _UniffiConverterBytes.write(value.id, buf) + _UniffiConverterString.write(value.id, buf) _UniffiConverterBool.write(value.connected, buf) _UniffiConverterOptionalUInt32.write(value.num_channels, buf) _UniffiConverterSequenceString.write(value.netaddr, buf) @@ -2617,18 +3042,43 @@ def write(value, buf): class PeerChannel: - peer_id: "bytes" + peer_id: "str" + """ + Peer node public key as lowercase hex (66 chars). + """ + peer_connected: "bool" state: "ChannelState" short_channel_id: "typing.Optional[str]" - channel_id: "typing.Optional[bytes]" - funding_txid: "typing.Optional[bytes]" + channel_id: "typing.Optional[str]" + """ + Channel id as lowercase hex (64 chars). + """ + + funding_txid: "typing.Optional[str]" + """ + Funding transaction id as lowercase hex (64 chars). + """ + funding_outnum: "typing.Optional[int]" to_us_msat: "typing.Optional[int]" total_msat: "typing.Optional[int]" spendable_msat: "typing.Optional[int]" receivable_msat: "typing.Optional[int]" - def __init__(self, *, peer_id: "bytes", peer_connected: "bool", state: "ChannelState", short_channel_id: "typing.Optional[str]", channel_id: "typing.Optional[bytes]", funding_txid: "typing.Optional[bytes]", funding_outnum: "typing.Optional[int]", to_us_msat: "typing.Optional[int]", total_msat: "typing.Optional[int]", spendable_msat: "typing.Optional[int]", receivable_msat: "typing.Optional[int]"): + closer: "typing.Optional[ChannelSide]" + """ + Which side initiated the close, if the channel is closing or closed. + """ + + status: "typing.List[str]" + """ + Human-readable status strings from CLN, ordered oldest to newest. + For a channel in `Onchain` state, the last entry indicates whether + our payout is still timelocked (`DELAYED_OUTPUT_TO_US`) or already + available in the on-chain balance. + """ + + def __init__(self, *, peer_id: "str", peer_connected: "bool", state: "ChannelState", short_channel_id: "typing.Optional[str]", channel_id: "typing.Optional[str]", funding_txid: "typing.Optional[str]", funding_outnum: "typing.Optional[int]", to_us_msat: "typing.Optional[int]", total_msat: "typing.Optional[int]", spendable_msat: "typing.Optional[int]", receivable_msat: "typing.Optional[int]", closer: "typing.Optional[ChannelSide]", status: "typing.List[str]"): self.peer_id = peer_id self.peer_connected = peer_connected self.state = state @@ -2640,9 +3090,11 @@ def __init__(self, *, peer_id: "bytes", peer_connected: "bool", state: "ChannelS self.total_msat = total_msat self.spendable_msat = spendable_msat self.receivable_msat = receivable_msat + self.closer = closer + self.status = status def __str__(self): - return "PeerChannel(peer_id={}, peer_connected={}, state={}, short_channel_id={}, channel_id={}, funding_txid={}, funding_outnum={}, to_us_msat={}, total_msat={}, spendable_msat={}, receivable_msat={})".format(self.peer_id, self.peer_connected, self.state, self.short_channel_id, self.channel_id, self.funding_txid, self.funding_outnum, self.to_us_msat, self.total_msat, self.spendable_msat, self.receivable_msat) + return "PeerChannel(peer_id={}, peer_connected={}, state={}, short_channel_id={}, channel_id={}, funding_txid={}, funding_outnum={}, to_us_msat={}, total_msat={}, spendable_msat={}, receivable_msat={}, closer={}, status={})".format(self.peer_id, self.peer_connected, self.state, self.short_channel_id, self.channel_id, self.funding_txid, self.funding_outnum, self.to_us_msat, self.total_msat, self.spendable_msat, self.receivable_msat, self.closer, self.status) def __eq__(self, other): if self.peer_id != other.peer_id: @@ -2667,52 +3119,62 @@ def __eq__(self, other): return False if self.receivable_msat != other.receivable_msat: return False + if self.closer != other.closer: + return False + if self.status != other.status: + return False return True class _UniffiConverterTypePeerChannel(_UniffiConverterRustBuffer): @staticmethod def read(buf): return PeerChannel( - peer_id=_UniffiConverterBytes.read(buf), + peer_id=_UniffiConverterString.read(buf), peer_connected=_UniffiConverterBool.read(buf), state=_UniffiConverterTypeChannelState.read(buf), short_channel_id=_UniffiConverterOptionalString.read(buf), - channel_id=_UniffiConverterOptionalBytes.read(buf), - funding_txid=_UniffiConverterOptionalBytes.read(buf), + channel_id=_UniffiConverterOptionalString.read(buf), + funding_txid=_UniffiConverterOptionalString.read(buf), funding_outnum=_UniffiConverterOptionalUInt32.read(buf), to_us_msat=_UniffiConverterOptionalUInt64.read(buf), total_msat=_UniffiConverterOptionalUInt64.read(buf), spendable_msat=_UniffiConverterOptionalUInt64.read(buf), receivable_msat=_UniffiConverterOptionalUInt64.read(buf), + closer=_UniffiConverterOptionalTypeChannelSide.read(buf), + status=_UniffiConverterSequenceString.read(buf), ) @staticmethod def check_lower(value): - _UniffiConverterBytes.check_lower(value.peer_id) + _UniffiConverterString.check_lower(value.peer_id) _UniffiConverterBool.check_lower(value.peer_connected) _UniffiConverterTypeChannelState.check_lower(value.state) _UniffiConverterOptionalString.check_lower(value.short_channel_id) - _UniffiConverterOptionalBytes.check_lower(value.channel_id) - _UniffiConverterOptionalBytes.check_lower(value.funding_txid) + _UniffiConverterOptionalString.check_lower(value.channel_id) + _UniffiConverterOptionalString.check_lower(value.funding_txid) _UniffiConverterOptionalUInt32.check_lower(value.funding_outnum) _UniffiConverterOptionalUInt64.check_lower(value.to_us_msat) _UniffiConverterOptionalUInt64.check_lower(value.total_msat) _UniffiConverterOptionalUInt64.check_lower(value.spendable_msat) _UniffiConverterOptionalUInt64.check_lower(value.receivable_msat) + _UniffiConverterOptionalTypeChannelSide.check_lower(value.closer) + _UniffiConverterSequenceString.check_lower(value.status) @staticmethod def write(value, buf): - _UniffiConverterBytes.write(value.peer_id, buf) + _UniffiConverterString.write(value.peer_id, buf) _UniffiConverterBool.write(value.peer_connected, buf) _UniffiConverterTypeChannelState.write(value.state, buf) _UniffiConverterOptionalString.write(value.short_channel_id, buf) - _UniffiConverterOptionalBytes.write(value.channel_id, buf) - _UniffiConverterOptionalBytes.write(value.funding_txid, buf) + _UniffiConverterOptionalString.write(value.channel_id, buf) + _UniffiConverterOptionalString.write(value.funding_txid, buf) _UniffiConverterOptionalUInt32.write(value.funding_outnum, buf) _UniffiConverterOptionalUInt64.write(value.to_us_msat, buf) _UniffiConverterOptionalUInt64.write(value.total_msat, buf) _UniffiConverterOptionalUInt64.write(value.spendable_msat, buf) _UniffiConverterOptionalUInt64.write(value.receivable_msat, buf) + _UniffiConverterOptionalTypeChannelSide.write(value.closer, buf) + _UniffiConverterSequenceString.write(value.status, buf) class ReceiveResponse: @@ -2758,13 +3220,25 @@ def write(value, buf): class SendResponse: status: "PayStatus" - preimage: "bytes" - payment_hash: "bytes" - destination_pubkey: "typing.Optional[bytes]" + preimage: "str" + """ + Payment preimage (proof of payment) as lowercase hex (64 chars). + """ + + payment_hash: "str" + """ + Payment hash as lowercase hex (64 chars). + """ + + destination_pubkey: "typing.Optional[str]" + """ + Recipient node pubkey as lowercase hex (66 chars), if known. + """ + amount_msat: "int" amount_sent_msat: "int" parts: "int" - def __init__(self, *, status: "PayStatus", preimage: "bytes", payment_hash: "bytes", destination_pubkey: "typing.Optional[bytes]", amount_msat: "int", amount_sent_msat: "int", parts: "int"): + def __init__(self, *, status: "PayStatus", preimage: "str", payment_hash: "str", destination_pubkey: "typing.Optional[str]", amount_msat: "int", amount_sent_msat: "int", parts: "int"): self.status = status self.preimage = preimage self.payment_hash = payment_hash @@ -2798,9 +3272,9 @@ class _UniffiConverterTypeSendResponse(_UniffiConverterRustBuffer): def read(buf): return SendResponse( status=_UniffiConverterTypePayStatus.read(buf), - preimage=_UniffiConverterBytes.read(buf), - payment_hash=_UniffiConverterBytes.read(buf), - destination_pubkey=_UniffiConverterOptionalBytes.read(buf), + preimage=_UniffiConverterString.read(buf), + payment_hash=_UniffiConverterString.read(buf), + destination_pubkey=_UniffiConverterOptionalString.read(buf), amount_msat=_UniffiConverterUInt64.read(buf), amount_sent_msat=_UniffiConverterUInt64.read(buf), parts=_UniffiConverterUInt32.read(buf), @@ -2809,9 +3283,9 @@ def read(buf): @staticmethod def check_lower(value): _UniffiConverterTypePayStatus.check_lower(value.status) - _UniffiConverterBytes.check_lower(value.preimage) - _UniffiConverterBytes.check_lower(value.payment_hash) - _UniffiConverterOptionalBytes.check_lower(value.destination_pubkey) + _UniffiConverterString.check_lower(value.preimage) + _UniffiConverterString.check_lower(value.payment_hash) + _UniffiConverterOptionalString.check_lower(value.destination_pubkey) _UniffiConverterUInt64.check_lower(value.amount_msat) _UniffiConverterUInt64.check_lower(value.amount_sent_msat) _UniffiConverterUInt32.check_lower(value.parts) @@ -2819,9 +3293,9 @@ def check_lower(value): @staticmethod def write(value, buf): _UniffiConverterTypePayStatus.write(value.status, buf) - _UniffiConverterBytes.write(value.preimage, buf) - _UniffiConverterBytes.write(value.payment_hash, buf) - _UniffiConverterOptionalBytes.write(value.destination_pubkey, buf) + _UniffiConverterString.write(value.preimage, buf) + _UniffiConverterString.write(value.payment_hash, buf) + _UniffiConverterOptionalString.write(value.destination_pubkey, buf) _UniffiConverterUInt64.write(value.amount_msat, buf) _UniffiConverterUInt64.write(value.amount_sent_msat, buf) _UniffiConverterUInt32.write(value.parts, buf) @@ -2830,6 +3304,48 @@ def write(value, buf): +class ChannelSide(enum.Enum): + """ + Which side of a channel performed a given action (e.g. initiated close). + """ + + LOCAL = 0 + + REMOTE = 1 + + + +class _UniffiConverterTypeChannelSide(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.read_i32() + if variant == 1: + return ChannelSide.LOCAL + if variant == 2: + return ChannelSide.REMOTE + raise InternalError("Raw enum value doesn't match any cases") + + @staticmethod + def check_lower(value): + if value == ChannelSide.LOCAL: + return + if value == ChannelSide.REMOTE: + return + raise ValueError(value) + + @staticmethod + def write(value, buf): + if value == ChannelSide.LOCAL: + buf.write_i32(1) + if value == ChannelSide.REMOTE: + buf.write_i32(2) + + + + + + + class ChannelState(enum.Enum): OPENINGD = 0 @@ -2857,6 +3373,14 @@ class ChannelState(enum.Enum): DUALOPEND_OPEN_COMMIT_READY = 12 + UNKNOWN = 13 + """ + A state reported by the node that this SDK doesn't recognize. + Returned when CLN introduces a new channel state after this SDK + was built. Treated as neither open nor closing by balance math. + """ + + class _UniffiConverterTypeChannelState(_UniffiConverterRustBuffer): @@ -2889,6 +3413,8 @@ def read(buf): return ChannelState.DUALOPEND_OPEN_COMMITTED if variant == 13: return ChannelState.DUALOPEND_OPEN_COMMIT_READY + if variant == 14: + return ChannelState.UNKNOWN raise InternalError("Raw enum value doesn't match any cases") @staticmethod @@ -2919,6 +3445,8 @@ def check_lower(value): return if value == ChannelState.DUALOPEND_OPEN_COMMIT_READY: return + if value == ChannelState.UNKNOWN: + return raise ValueError(value) @staticmethod @@ -2949,6 +3477,8 @@ def write(value, buf): buf.write_i32(12) if value == ChannelState.DUALOPEND_OPEN_COMMIT_READY: buf.write_i32(13) + if value == ChannelState.UNKNOWN: + buf.write_i32(14) @@ -3842,6 +4372,33 @@ def read(cls, buf): +class _UniffiConverterOptionalTypeChannelSide(_UniffiConverterRustBuffer): + @classmethod + def check_lower(cls, value): + if value is not None: + _UniffiConverterTypeChannelSide.check_lower(value) + + @classmethod + def write(cls, value, buf): + if value is None: + buf.write_u8(0) + return + + buf.write_u8(1) + _UniffiConverterTypeChannelSide.write(value, buf) + + @classmethod + def read(cls, buf): + flag = buf.read_u8() + if flag == 0: + return None + elif flag == 1: + return _UniffiConverterTypeChannelSide.read(buf) + else: + raise InternalError("Unexpected flag byte for optional type") + + + class _UniffiConverterOptionalTypeListIndex(_UniffiConverterRustBuffer): @classmethod def check_lower(cls, value): @@ -4575,7 +5132,6 @@ def list_funds(self, ): raise NotImplementedError def list_invoices(self, label: "typing.Optional[str]",invstring: "typing.Optional[str]",payment_hash: "typing.Optional[bytes]",offer_id: "typing.Optional[str]",index: "typing.Optional[ListIndex]",start: "typing.Optional[int]",limit: "typing.Optional[int]"): """ - List all invoices (received payment requests). List invoices (received payment requests). All parameters are optional filters; pass None to fetch all. """ @@ -4616,6 +5172,15 @@ def list_peers(self, ): status. """ + raise NotImplementedError + def node_state(self, ): + """ + Get a snapshot of the node's balances, capacity, and connectivity. + + Aggregates data from multiple RPCs into a single `NodeState`. + Queries the node live on each call — not cached. + """ + raise NotImplementedError def onchain_receive(self, ): """ @@ -4775,7 +5340,6 @@ def list_funds(self, ) -> "ListFundsResponse": def list_invoices(self, label: "typing.Optional[str]",invstring: "typing.Optional[str]",payment_hash: "typing.Optional[bytes]",offer_id: "typing.Optional[str]",index: "typing.Optional[ListIndex]",start: "typing.Optional[int]",limit: "typing.Optional[int]") -> "ListInvoicesResponse": """ - List all invoices (received payment requests). List invoices (received payment requests). All parameters are optional filters; pass None to fetch all. """ @@ -4894,6 +5458,22 @@ def list_peers(self, ) -> "ListPeersResponse": + def node_state(self, ) -> "NodeState": + """ + Get a snapshot of the node's balances, capacity, and connectivity. + + Aggregates data from multiple RPCs into a single `NodeState`. + Queries the node live on each call — not cached. + """ + + return _UniffiConverterTypeNodeState.lift( + _uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_method_node_node_state,self._uniffi_clone_pointer(),) + ) + + + + + def onchain_receive(self, ) -> "OnchainReceiveResponse": """ Generate a fresh on-chain Bitcoin address for receiving funds. @@ -5464,6 +6044,7 @@ def register_or_recover(mnemonic: "str",invite_code: "typing.Optional[str]",conf __all__ = [ "InternalError", + "ChannelSide", "ChannelState", "Error", "InputType", @@ -5487,6 +6068,7 @@ def register_or_recover(mnemonic: "str",invite_code: "typing.Optional[str]",conf "ListPaysResponse", "ListPeerChannelsResponse", "ListPeersResponse", + "NodeState", "OnchainReceiveResponse", "OnchainSendResponse", "ParsedInvoice", diff --git a/libs/gl-sdk/src/input.rs b/libs/gl-sdk/src/input.rs index 4d4f3f3b9..c5140702c 100644 --- a/libs/gl-sdk/src/input.rs +++ b/libs/gl-sdk/src/input.rs @@ -8,10 +8,10 @@ use crate::Error; pub struct ParsedInvoice { /// The original invoice string. pub bolt11: String, - /// 33-byte recipient public key, recovered from the invoice signature. - pub payee_pubkey: Option>, - /// 32-byte payment hash identifying this payment. - pub payment_hash: Vec, + /// Recipient public key as lowercase hex (66 chars), recovered from the invoice signature. + pub payee_pubkey: Option, + /// Payment hash as lowercase hex (64 chars) identifying this payment. + pub payment_hash: String, /// Invoice description. None if the invoice uses a description hash. pub description: Option, /// Requested amount in millisatoshis. None for "any amount" invoices. @@ -82,14 +82,9 @@ fn try_parse_bolt11(input: &str) -> Option> { ))); } - let payee_pubkey = parsed - .recover_payee_pub_key() - .serialize() - .to_vec(); + let payee_pubkey = hex::encode(parsed.recover_payee_pub_key().serialize()); let payment_hash = format!("{}", parsed.payment_hash()); - let payment_hash = hex::decode(&payment_hash) - .unwrap_or_default(); let description = match parsed.description() { lightning_invoice::Bolt11InvoiceDescriptionRef::Direct(d) => Some(d.to_string()), diff --git a/libs/gl-sdk/src/lib.rs b/libs/gl-sdk/src/lib.rs index 16276e84f..268f8f370 100644 --- a/libs/gl-sdk/src/lib.rs +++ b/libs/gl-sdk/src/lib.rs @@ -39,7 +39,7 @@ pub use crate::{ ChannelState, FundChannel, FundOutput, GetInfoResponse, Invoice, InvoicePaidEvent, InvoiceStatus, ListFundsResponse, ListIndex, ListInvoicesResponse, ListPaymentsRequest, ListPeerChannelsResponse, ListPaysResponse, ListPeersResponse, - Node, NodeEvent, NodeEventStream, OnchainReceiveResponse, OnchainSendResponse, + Node, NodeEvent, NodeEventStream, NodeState, OnchainReceiveResponse, OnchainSendResponse, OutputStatus, Pay, PayStatus, Payment, PaymentStatus, PaymentType, PaymentTypeFilter, Peer, PeerChannel, ReceiveResponse, SendResponse, }, diff --git a/libs/gl-sdk/src/node.rs b/libs/gl-sdk/src/node.rs index b42ba4df7..34cb2488f 100644 --- a/libs/gl-sdk/src/node.rs +++ b/libs/gl-sdk/src/node.rs @@ -288,7 +288,131 @@ impl Node { Ok(res.into()) } - /// List all invoices (received payment requests). + /// Get a snapshot of the node's balances, capacity, and connectivity. + /// + /// Aggregates data from multiple RPCs into a single `NodeState`. + /// Queries the node live on each call — not cached. + pub fn node_state(&self) -> Result { + self.check_connected()?; + let cln_client = exec(self.get_cln_client())?.clone(); + + let (info_res, channels_res, funds_res) = exec(async { + let mut c_info = cln_client.clone(); + let mut c_channels = cln_client.clone(); + let mut c_funds = cln_client.clone(); + tokio::join!( + c_info.getinfo(clnpb::GetinfoRequest {}), + c_channels.list_peer_channels(clnpb::ListpeerchannelsRequest { id: None }), + c_funds.list_funds(clnpb::ListfundsRequest { spent: None }), + ) + }); + + let info: GetInfoResponse = info_res + .map_err(|e| Error::Rpc(e.to_string()))? + .into_inner() + .into(); + let channels: ListPeerChannelsResponse = channels_res + .map_err(|e| Error::Rpc(e.to_string()))? + .into_inner() + .into(); + let funds: ListFundsResponse = funds_res + .map_err(|e| Error::Rpc(e.to_string()))? + .into_inner() + .into(); + + let mut channels_balance_msat: u64 = 0; + let mut max_payable_msat: u64 = 0; + let mut total_channel_capacity_msat: u64 = 0; + let mut max_receivable_single_payment_msat: u64 = 0; + let mut total_inbound_liquidity_msat: u64 = 0; + let mut pending_onchain_balance_msat: u64 = 0; + let mut connected_channel_peer_set: std::collections::HashSet = + std::collections::HashSet::new(); + + for ch in &channels.channels { + if ch.state.is_open() { + channels_balance_msat += ch.to_us_msat.unwrap_or(0); + max_payable_msat += ch.spendable_msat.unwrap_or(0); + total_channel_capacity_msat += ch.total_msat.unwrap_or(0); + let receivable = ch.receivable_msat.unwrap_or(0); + if receivable > max_receivable_single_payment_msat { + max_receivable_single_payment_msat = receivable; + } + total_inbound_liquidity_msat += receivable; + } + if channel_payout_still_pending(ch) { + pending_onchain_balance_msat += ch.to_us_msat.unwrap_or(0); + } + if ch.peer_connected { + connected_channel_peer_set.insert(ch.peer_id.clone()); + } + } + let connected_channel_peers: Vec = + connected_channel_peer_set.into_iter().collect(); + + let max_chan_reserve_msat = + channels_balance_msat.saturating_sub(max_payable_msat); + + let mut onchain_balance_msat: u64 = 0; + let mut unconfirmed_onchain_balance_msat: u64 = 0; + let mut immature_onchain_balance_msat: u64 = 0; + let mut utxos: Vec = Vec::with_capacity(funds.outputs.len()); + for output in &funds.outputs { + if !matches!(output.status, OutputStatus::Spent) { + utxos.push(output.clone()); + } + if output.reserved { + continue; + } + match output.status { + OutputStatus::Confirmed => onchain_balance_msat += output.amount_msat, + OutputStatus::Unconfirmed => { + unconfirmed_onchain_balance_msat += output.amount_msat + } + OutputStatus::Immature => { + immature_onchain_balance_msat += output.amount_msat + } + OutputStatus::Spent => {} + } + } + + let total_onchain_msat = onchain_balance_msat + .saturating_add(unconfirmed_onchain_balance_msat) + .saturating_add(immature_onchain_balance_msat); + let total_balance_msat = channels_balance_msat + .saturating_add(total_onchain_msat) + .saturating_add(pending_onchain_balance_msat); + let spendable_balance_msat = max_payable_msat.saturating_add(onchain_balance_msat); + + + Ok(NodeState { + id: info.id, + block_height: info.blockheight, + network: info.network, + version: info.version, + alias: info.alias, + color: info.color, + num_active_channels: info.num_active_channels, + num_pending_channels: info.num_pending_channels, + num_inactive_channels: info.num_inactive_channels, + channels_balance_msat, + max_payable_msat, + total_channel_capacity_msat, + max_chan_reserve_msat, + onchain_balance_msat, + unconfirmed_onchain_balance_msat, + immature_onchain_balance_msat, + pending_onchain_balance_msat, + max_receivable_single_payment_msat, + total_inbound_liquidity_msat, + connected_channel_peers, + utxos, + total_onchain_msat, + total_balance_msat, + spendable_balance_msat, + }) + } + /// List invoices (received payment requests). /// All parameters are optional filters; pass None to fetch all. pub fn list_invoices( @@ -519,8 +643,8 @@ impl Node { pub struct OnchainSendResponse { /// The raw signed transaction bytes. pub tx: Vec, - /// The transaction ID (32 bytes, reversed byte order as is standard). - pub txid: Vec, + /// The transaction id as lowercase hex (64 chars). + pub txid: String, /// The transaction as a Partially Signed Bitcoin Transaction string. pub psbt: String, } @@ -529,7 +653,7 @@ impl From for OnchainSendResponse { fn from(other: clnpb::WithdrawResponse) -> Self { Self { tx: other.tx, - txid: other.txid, + txid: hex::encode(&other.txid), psbt: other.psbt, } } @@ -556,9 +680,12 @@ impl From for OnchainReceiveResponse { #[derive(uniffi::Record)] pub struct SendResponse { pub status: PayStatus, - pub preimage: Vec, - pub payment_hash: Vec, - pub destination_pubkey: Option>, + /// Payment preimage (proof of payment) as lowercase hex (64 chars). + pub preimage: String, + /// Payment hash as lowercase hex (64 chars). + pub payment_hash: String, + /// Recipient node pubkey as lowercase hex (66 chars), if known. + pub destination_pubkey: Option, pub amount_msat: u64, pub amount_sent_msat: u64, pub parts: u32, @@ -568,9 +695,9 @@ impl From for SendResponse { fn from(other: clnpb::PayResponse) -> Self { Self { status: other.status.into(), - preimage: other.payment_preimage, - payment_hash: other.payment_hash, - destination_pubkey: other.destination, + preimage: hex::encode(&other.payment_preimage), + payment_hash: hex::encode(&other.payment_hash), + destination_pubkey: other.destination.as_deref().map(hex::encode), amount_msat: other.amount_msat.unwrap().msat, amount_sent_msat: other.amount_sent_msat.unwrap().msat, parts: other.parts, @@ -621,9 +748,11 @@ impl From for PayStatus { #[allow(unused)] #[derive(Clone, uniffi::Record)] pub struct GetInfoResponse { - pub id: Vec, + /// Node public key as lowercase hex (66 chars). + pub id: String, pub alias: Option, - pub color: Vec, + /// 3-byte RGB color as lowercase hex (6 chars). + pub color: String, pub num_peers: u32, pub num_pending_channels: u32, pub num_active_channels: u32, @@ -638,9 +767,9 @@ pub struct GetInfoResponse { impl From for GetInfoResponse { fn from(other: clnpb::GetinfoResponse) -> Self { Self { - id: other.id, + id: hex::encode(&other.id), alias: other.alias, - color: other.color, + color: hex::encode(&other.color), num_peers: other.num_peers, num_pending_channels: other.num_pending_channels, num_active_channels: other.num_active_channels, @@ -667,7 +796,8 @@ pub struct ListPeersResponse { #[allow(unused)] #[derive(Clone, uniffi::Record)] pub struct Peer { - pub id: Vec, + /// Peer node public key as lowercase hex (66 chars). + pub id: String, pub connected: bool, pub num_channels: Option, pub netaddr: Vec, @@ -686,7 +816,7 @@ impl From for ListPeersResponse { impl From for Peer { fn from(other: clnpb::ListpeersPeers) -> Self { Self { - id: other.id, + id: hex::encode(&other.id), connected: other.connected, num_channels: other.num_channels, netaddr: other.netaddr, @@ -709,17 +839,44 @@ pub struct ListPeerChannelsResponse { #[allow(unused)] #[derive(Clone, uniffi::Record)] pub struct PeerChannel { - pub peer_id: Vec, + /// Peer node public key as lowercase hex (66 chars). + pub peer_id: String, pub peer_connected: bool, pub state: ChannelState, pub short_channel_id: Option, - pub channel_id: Option>, - pub funding_txid: Option>, + /// Channel id as lowercase hex (64 chars). + pub channel_id: Option, + /// Funding transaction id as lowercase hex (64 chars). + pub funding_txid: Option, pub funding_outnum: Option, pub to_us_msat: Option, pub total_msat: Option, pub spendable_msat: Option, pub receivable_msat: Option, + /// Which side initiated the close, if the channel is closing or closed. + pub closer: Option, + /// Human-readable status strings from CLN, ordered oldest to newest. + /// For a channel in `Onchain` state, the last entry indicates whether + /// our payout is still timelocked (`DELAYED_OUTPUT_TO_US`) or already + /// available in the on-chain balance. + pub status: Vec, +} + +/// Which side of a channel performed a given action (e.g. initiated close). +#[derive(Clone, uniffi::Enum)] +pub enum ChannelSide { + Local, + Remote, +} + +impl ChannelSide { + fn from_i32(value: i32) -> Option { + match value { + 0 => Some(ChannelSide::Local), + 1 => Some(ChannelSide::Remote), + _ => None, + } + } } #[derive(Clone, uniffi::Enum)] @@ -737,6 +894,10 @@ pub enum ChannelState { DualopendAwaitingLockin, DualopendOpenCommitted, DualopendOpenCommitReady, + /// A state reported by the node that this SDK doesn't recognize. + /// Returned when CLN introduces a new channel state after this SDK + /// was built. Treated as neither open nor closing by balance math. + Unknown, } impl ChannelState { @@ -755,8 +916,44 @@ impl ChannelState { 10 => ChannelState::DualopendAwaitingLockin, 11 => ChannelState::DualopendOpenCommitted, 12 => ChannelState::DualopendOpenCommitReady, - _ => ChannelState::Onchain, // Default fallback + _ => ChannelState::Unknown, + } + } + + fn is_open(&self) -> bool { + matches!(self, ChannelState::ChanneldNormal) + } + +} + +/// Returns true when the channel still holds on-chain funds that have +/// not yet been credited to the wallet's on-chain balance. +/// +/// In closing states up to `FundingSpendSeen`, the payout has not yet +/// appeared as a wallet UTXO and `to_us_msat` represents funds still +/// locked in the channel. +/// +/// In `Onchain` state CLN keeps the channel around for the duration of +/// the close timelock. Once the close tx is mined the payout is visible +/// in `listfunds.outputs`, so counting `to_us_msat` again would double +/// it. The exception is when we initiated the close and our payout is +/// still timelocked (the last status entry contains `DELAYED_OUTPUT_TO_US`): +/// in that window the funds exist on-chain but are not yet spendable. +fn channel_payout_still_pending(ch: &PeerChannel) -> bool { + match ch.state { + ChannelState::ChanneldShuttingDown + | ChannelState::ClosingdSigexchange + | ChannelState::ClosingdComplete + | ChannelState::AwaitingUnilateral + | ChannelState::FundingSpendSeen => true, + ChannelState::Onchain => { + matches!(ch.closer, Some(ChannelSide::Local)) + && ch + .status + .last() + .is_some_and(|s| s.contains("DELAYED_OUTPUT_TO_US")) } + _ => false, } } @@ -771,18 +968,21 @@ impl From for ListPeerChannelsResponse { impl From for PeerChannel { fn from(other: clnpb::ListpeerchannelsChannels) -> Self { let state = ChannelState::from_i32(other.state); + let closer = other.closer.and_then(ChannelSide::from_i32); Self { - peer_id: other.peer_id, + peer_id: hex::encode(&other.peer_id), peer_connected: other.peer_connected, state, short_channel_id: other.short_channel_id, - channel_id: other.channel_id, - funding_txid: other.funding_txid, + channel_id: other.channel_id.as_deref().map(hex::encode), + funding_txid: other.funding_txid.as_deref().map(hex::encode), funding_outnum: other.funding_outnum, to_us_msat: other.to_us_msat.map(|a| a.msat), total_msat: other.total_msat.map(|a| a.msat), spendable_msat: other.spendable_msat.map(|a| a.msat), receivable_msat: other.receivable_msat.map(|a| a.msat), + closer, + status: other.status, } } } @@ -801,12 +1001,18 @@ pub struct ListFundsResponse { #[allow(unused)] #[derive(Clone, uniffi::Record)] pub struct FundOutput { - pub txid: Vec, + /// Transaction id as lowercase hex (64 chars). + pub txid: String, pub output: u32, pub amount_msat: u64, pub status: OutputStatus, pub address: Option, pub blockheight: Option, + /// True when this UTXO is currently reserved by an in-flight PSBT + /// (e.g. a channel-open or fund-send that has not been broadcast or + /// abandoned). Reserved UTXOs are not spendable and must be excluded + /// from the wallet's spendable balance. + pub reserved: bool, } #[derive(Clone, uniffi::Enum)] @@ -832,15 +1038,18 @@ impl OutputStatus { #[allow(unused)] #[derive(Clone, uniffi::Record)] pub struct FundChannel { - pub peer_id: Vec, + /// Peer node public key as lowercase hex (66 chars). + pub peer_id: String, pub our_amount_msat: u64, pub amount_msat: u64, - pub funding_txid: Vec, + /// Funding transaction id as lowercase hex (64 chars). + pub funding_txid: String, pub funding_output: u32, pub connected: bool, pub state: ChannelState, pub short_channel_id: Option, - pub channel_id: Option>, + /// Channel id as lowercase hex (64 chars). + pub channel_id: Option, } impl From for ListFundsResponse { @@ -856,12 +1065,13 @@ impl From for FundOutput { fn from(other: clnpb::ListfundsOutputs) -> Self { let status = OutputStatus::from_i32(other.status); Self { - txid: other.txid, + txid: hex::encode(&other.txid), output: other.output, amount_msat: other.amount_msat.map(|a| a.msat).unwrap_or(0), status, address: other.address, blockheight: other.blockheight, + reserved: other.reserved, } } } @@ -870,15 +1080,15 @@ impl From for FundChannel { fn from(other: clnpb::ListfundsChannels) -> Self { let state = ChannelState::from_i32(other.state); Self { - peer_id: other.peer_id, + peer_id: hex::encode(&other.peer_id), our_amount_msat: other.our_amount_msat.map(|a| a.msat).unwrap_or(0), amount_msat: other.amount_msat.map(|a| a.msat).unwrap_or(0), - funding_txid: other.funding_txid, + funding_txid: hex::encode(&other.funding_txid), funding_output: other.funding_output, connected: other.connected, state, short_channel_id: other.short_channel_id, - channel_id: other.channel_id, + channel_id: other.channel_id.as_deref().map(hex::encode), } } } @@ -929,7 +1139,8 @@ impl From for InvoiceStatus { pub struct Invoice { pub label: String, pub description: String, - pub payment_hash: Vec, + /// Payment hash as lowercase hex (64 chars). + pub payment_hash: String, pub status: InvoiceStatus, pub amount_msat: Option, pub amount_received_msat: Option, @@ -937,14 +1148,16 @@ pub struct Invoice { pub bolt12: Option, pub paid_at: Option, pub expires_at: u64, - pub payment_preimage: Option>, - pub destination_pubkey: Option>, + /// Payment preimage as lowercase hex (64 chars), if the invoice has been paid. + pub payment_preimage: Option, + /// Recipient node pubkey as lowercase hex (66 chars), recovered from the bolt11. + pub destination_pubkey: Option, } -/// Extract the payee public key from a BOLT11 invoice string. -fn pubkey_from_bolt11(bolt11: &str) -> Option> { +/// Extract the payee public key from a BOLT11 invoice string as hex. +fn pubkey_from_bolt11(bolt11: &str) -> Option { let invoice: Bolt11Invoice = bolt11.parse().ok()?; - Some(invoice.recover_payee_pub_key().serialize().to_vec()) + Some(hex::encode(invoice.recover_payee_pub_key().serialize())) } impl From for Invoice { @@ -953,7 +1166,7 @@ impl From for Invoice { Self { label: other.label, description: other.description.unwrap_or_default(), - payment_hash: other.payment_hash, + payment_hash: hex::encode(&other.payment_hash), status: other.status.into(), amount_msat: other.amount_msat.map(|a| a.msat), amount_received_msat: other.amount_received_msat.map(|a| a.msat), @@ -961,7 +1174,7 @@ impl From for Invoice { bolt12: other.bolt12, paid_at: other.paid_at, expires_at: other.expires_at, - payment_preimage: other.payment_preimage, + payment_preimage: other.payment_preimage.as_deref().map(hex::encode), destination_pubkey, } } @@ -986,16 +1199,19 @@ impl From for ListInvoicesResponse { #[derive(Clone, uniffi::Record)] pub struct Pay { - pub payment_hash: Vec, + /// Payment hash as lowercase hex (64 chars). + pub payment_hash: String, pub status: PayStatus, - pub destination_pubkey: Option>, + /// Recipient node pubkey as lowercase hex (66 chars), if known. + pub destination_pubkey: Option, pub amount_msat: Option, pub amount_sent_msat: Option, pub label: Option, pub bolt11: Option, pub description: Option, pub bolt12: Option, - pub preimage: Option>, + /// Payment preimage as lowercase hex (64 chars), if the payment completed. + pub preimage: Option, pub created_at: u64, pub completed_at: Option, pub number_of_parts: Option, @@ -1010,16 +1226,16 @@ impl From for Pay { o => panic!("Unknown listpays status {}", o), }; Self { - payment_hash: other.payment_hash, + payment_hash: hex::encode(&other.payment_hash), status, - destination_pubkey: other.destination, + destination_pubkey: other.destination.as_deref().map(hex::encode), amount_msat: other.amount_msat.map(|a| a.msat), amount_sent_msat: other.amount_sent_msat.map(|a| a.msat), label: other.label, bolt11: other.bolt11, description: other.description, bolt12: other.bolt12, - preimage: other.preimage, + preimage: other.preimage.as_deref().map(hex::encode), created_at: other.created_at, completed_at: other.completed_at, number_of_parts: other.number_of_parts, @@ -1076,8 +1292,21 @@ pub struct Payment { pub status: PaymentStatus, pub description: Option, pub bolt11: Option, - pub preimage: Option>, - pub destination: Option>, + /// Payment preimage as lowercase hex (64 chars), when known. + pub preimage: Option, + /// Pubkey of the counterparty in the payment, as lowercase hex + /// (66 chars). + /// + /// For `PaymentType::Sent`: the recipient node we paid (when CLN + /// reports it). + /// + /// For `PaymentType::Received`: always `None`. Lightning's privacy + /// model does not reveal the sender's pubkey to the recipient — the + /// HTLC arrives via one of our channel peers, but that peer is + /// usually just a router, not the original payer. The only pubkey + /// derivable from a paid invoice is the *payee* (i.e. our own + /// node), which is uninteresting to display per-row. + pub destination: Option, } #[derive(Clone, uniffi::Enum)] @@ -1115,7 +1344,7 @@ impl From for Payment { .unwrap_or(0); Payment { - id: inv.payment_hash.iter().map(|b| format!("{:02x}", b)).collect::(), + id: hex::encode(&inv.payment_hash), payment_type: PaymentType::Received, payment_time, amount_msat, @@ -1123,7 +1352,7 @@ impl From for Payment { status, description: inv.description, bolt11: inv.bolt11, - preimage: inv.payment_preimage, + preimage: inv.payment_preimage.as_deref().map(hex::encode), destination: None, } } @@ -1143,7 +1372,7 @@ impl From for Payment { let fee_msat = amount_sent_msat.saturating_sub(amount_msat); Payment { - id: pay.payment_hash.iter().map(|b| format!("{:02x}", b)).collect::(), + id: hex::encode(&pay.payment_hash), payment_type: PaymentType::Sent, payment_time, amount_msat, @@ -1151,12 +1380,126 @@ impl From for Payment { status, description: pay.description, bolt11: pay.bolt11, - preimage: pay.preimage, - destination: pay.destination, + preimage: pay.preimage.as_deref().map(hex::encode), + destination: pay.destination.as_deref().map(hex::encode), } } } +// ============================================================ +// NodeState — unified node snapshot +// ============================================================ + +/// A point-in-time snapshot of the node's balances, capacity, and +/// connectivity. Returned by `node_state()`. +/// +/// All amounts are in millisatoshis (1 sat = 1000 msat). +#[derive(Clone, uniffi::Record)] +pub struct NodeState { + /// The node's public key as a lowercase hex string (66 chars). + pub id: String, + /// Latest block height the node has synced to. + pub block_height: u32, + /// The Bitcoin network this node is running on (e.g. "bitcoin", "regtest"). + pub network: String, + /// CLN version string (e.g. "v24.11"). + pub version: String, + /// Human-readable node alias, if set. + pub alias: Option, + /// 3-byte RGB color of the node, as a lowercase hex string (6 chars). + pub color: String, + /// Number of channels that are open and operational. These are the + /// channels that contribute to `channels_balance_msat`, + /// `max_payable_msat`, `total_channel_capacity_msat`, and + /// `total_inbound_liquidity_msat`. + pub num_active_channels: u32, + /// Number of channels that are being opened but not yet confirmed. + /// Pending channels do not contribute to any balance or capacity + /// field on this snapshot; their funds show up only after they + /// transition to active. + pub num_pending_channels: u32, + /// Number of channels that are open but the peer is offline. + /// Inactive channels hold balance but cannot be used for payments + /// until the peer reconnects; they do not contribute to + /// `max_payable_msat` or `total_inbound_liquidity_msat` (those are + /// computed from the live `spendable_msat` / `receivable_msat` + /// reported by CLN, which goes to zero when the peer is offline). + pub num_inactive_channels: u32, + /// Total our-side balance across all open channels, including amounts + /// that protocol reserves make unspendable. + /// + /// This is the field a wallet's home screen should show as the + /// user's "Lightning balance" — it reflects what they own off-chain, + /// matching what they'd expect to see at a glance. + /// + /// Do **not** use this to gate a send button: some of it is locked + /// in channel reserves. Use `max_payable_msat` for that. + pub channels_balance_msat: u64, + /// Aggregate spendable amount across all open channels. Equal to + /// `channels_balance_msat - max_chan_reserve_msat`. + /// + /// This is the field a send screen should gate against — it is what + /// the user can actually move right now over Lightning in total. + /// + /// Caveat: a single Lightning payment is additionally bounded by + /// the largest channel's own `spendable_msat`. Reaching this full + /// aggregate amount in one payment requires multi-path-payment + /// support from the recipient and a working route. + pub max_payable_msat: u64, + /// Sum of all open channel capacities (your side + remote side). + pub total_channel_capacity_msat: u64, + /// Amount locked in protocol channel reserves, computed as + /// `channels_balance_msat - max_payable_msat`. These sats are yours + /// on paper but cannot be spent until the channel closes. + pub max_chan_reserve_msat: u64, + /// Confirmed on-chain balance available for spending or opening channels. + pub onchain_balance_msat: u64, + /// On-chain balance from transactions that have not yet been confirmed. + pub unconfirmed_onchain_balance_msat: u64, + /// On-chain balance confirmed but not yet spendable (e.g. coinbase + /// outputs inside the 100-block maturation window). + pub immature_onchain_balance_msat: u64, + /// On-chain balance locked in channels that are being closed. + /// These funds will become available once the close is confirmed. + pub pending_onchain_balance_msat: u64, + /// Largest single Lightning payment the node can receive without + /// splitting across channels. Bounded by the inbound capacity of + /// the largest open channel. + pub max_receivable_single_payment_msat: u64, + /// Total amount you can receive across all open channels combined. + pub total_inbound_liquidity_msat: u64, + /// Lowercase hex public keys of peers we have at least one channel + /// with and are currently connected to. Peers we're connected to but + /// have no channel with are not represented here; for routing-node + /// use cases, query `list_peers()` directly. + pub connected_channel_peers: Vec, + /// Unspent on-chain outputs owned by the node's wallet. Excludes + /// spent outputs; includes confirmed, unconfirmed, immature, and + /// reserved UTXOs (callers can filter by `status` and `reserved`). + pub utxos: Vec, + + // ------------------------------------------------------------------ + // Aggregate balance views. All amounts in millisatoshis, matching + // the rest of this struct. Callers displaying sats should divide by + // 1000 on the UI side. + // ------------------------------------------------------------------ + /// All non-pending on-chain balance buckets summed: + /// `onchain_balance_msat + unconfirmed_onchain_balance_msat + immature_onchain_balance_msat`. + /// Excludes funds locked in closing channels (`pending_onchain_balance_msat`) + /// since those are not yet on-chain UTXOs. + pub total_onchain_msat: u64, + /// Everything the user owns, summed: channel balance (including + /// protocol reserves) + all on-chain buckets + funds locked in + /// closing channels. The "total holdings" number a wallet home + /// screen typically shows. + pub total_balance_msat: u64, + /// What the user can spend *right now*: + /// `max_payable_msat + onchain_balance_msat`. Excludes reserves, + /// unconfirmed, immature, and pending amounts. The number a + /// send-money screen should gate against. + pub spendable_balance_msat: u64, +} + // ============================================================ // NodeEvent streaming types // ============================================================ @@ -1203,12 +1546,12 @@ pub enum NodeEvent { /// Details of a paid invoice. #[derive(Clone, uniffi::Record)] pub struct InvoicePaidEvent { - /// The payment hash of the paid invoice. - pub payment_hash: Vec, + /// Payment hash of the paid invoice as lowercase hex (64 chars). + pub payment_hash: String, /// The bolt11 invoice string. pub bolt11: String, - /// The preimage that proves payment. - pub preimage: Vec, + /// Preimage that proves payment as lowercase hex (64 chars). + pub preimage: String, /// The label assigned to the invoice. pub label: String, /// Amount received in millisatoshis. @@ -1220,9 +1563,9 @@ impl From for NodeEvent { match other.event { Some(glpb::node_event::Event::InvoicePaid(paid)) => NodeEvent::InvoicePaid { details: InvoicePaidEvent { - payment_hash: paid.payment_hash, + payment_hash: hex::encode(&paid.payment_hash), bolt11: paid.bolt11, - preimage: paid.preimage, + preimage: hex::encode(&paid.preimage), label: paid.label, amount_msat: paid.amount_msat, }, diff --git a/libs/gl-sdk/tests/test_node_methods.py b/libs/gl-sdk/tests/test_node_methods.py index 63f23efa4..dea939826 100644 --- a/libs/gl-sdk/tests/test_node_methods.py +++ b/libs/gl-sdk/tests/test_node_methods.py @@ -6,6 +6,13 @@ import pytest import glsdk +from gltesting.fixtures import * + + +MNEMONIC = ( + "abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon about" +) class TestResponseTypes: @@ -84,23 +91,26 @@ class TestSendResponseFields: """Test that SendResponse includes payment_hash and destination_pubkey.""" def test_send_response_has_payment_hash(self): + preimage_hex = "01" * 32 + payment_hash_hex = "00" * 32 + destination_hex = "02" * 33 response = glsdk.SendResponse( status=glsdk.PayStatus.COMPLETE, - preimage=b"\x01" * 32, - payment_hash=b"\x00" * 32, - destination_pubkey=b"\x02" * 33, + preimage=preimage_hex, + payment_hash=payment_hash_hex, + destination_pubkey=destination_hex, amount_msat=1000, amount_sent_msat=1010, parts=1, ) - assert response.payment_hash == b"\x00" * 32 - assert response.destination_pubkey == b"\x02" * 33 + assert response.payment_hash == payment_hash_hex + assert response.destination_pubkey == destination_hex def test_send_response_destination_pubkey_is_optional(self): response = glsdk.SendResponse( status=glsdk.PayStatus.COMPLETE, - preimage=b"\x01" * 32, - payment_hash=b"\x00" * 32, + preimage="01" * 32, + payment_hash="00" * 32, destination_pubkey=None, amount_msat=1000, amount_sent_msat=1010, @@ -114,11 +124,11 @@ class TestResponseTypeFields: def test_get_info_response_has_expected_fields(self): """Test GetInfoResponse has expected field attributes.""" - # Create instance to check fields + node_id_hex = "02" * 33 response = glsdk.GetInfoResponse( - id=b"\x02" * 33, + id=node_id_hex, alias="test-node", - color=b"\xff\x00\x00", + color="ff0000", num_peers=0, num_pending_channels=0, num_active_channels=0, @@ -129,7 +139,7 @@ def test_get_info_response_has_expected_fields(self): network="regtest", fees_collected_msat=0, ) - assert response.id == b"\x02" * 33 + assert response.id == node_id_hex assert response.alias == "test-node" assert response.network == "regtest" assert response.blockheight == 100 @@ -152,66 +162,160 @@ def test_list_funds_response_has_expected_fields(self): def test_peer_record_has_expected_fields(self): """Test Peer record has expected fields.""" + peer_id_hex = "03" * 33 peer = glsdk.Peer( - id=b"\x03" * 33, + id=peer_id_hex, connected=True, num_channels=1, netaddr=["127.0.0.1:9735"], remote_addr="192.168.1.1:9735", features=b"\x00", ) - assert peer.id == b"\x03" * 33 + assert peer.id == peer_id_hex assert peer.connected is True assert peer.num_channels == 1 assert "127.0.0.1:9735" in peer.netaddr def test_peer_channel_record_has_expected_fields(self): """Test PeerChannel record has expected fields.""" + peer_id_hex = "03" * 33 + channel_id_hex = "00" * 32 + funding_txid_hex = "ab" * 32 channel = glsdk.PeerChannel( - peer_id=b"\x03" * 33, + peer_id=peer_id_hex, peer_connected=True, state=glsdk.ChannelState.CHANNELD_NORMAL, short_channel_id="123x1x0", - channel_id=b"\x00" * 32, - funding_txid=b"\xab" * 32, + channel_id=channel_id_hex, + funding_txid=funding_txid_hex, funding_outnum=0, to_us_msat=500000000, total_msat=1000000000, spendable_msat=400000000, receivable_msat=400000000, + closer=None, + status=[], ) - assert channel.peer_id == b"\x03" * 33 + assert channel.peer_id == peer_id_hex assert channel.peer_connected is True assert channel.state == glsdk.ChannelState.CHANNELD_NORMAL assert channel.total_msat == 1000000000 + assert channel.closer is None + assert channel.status == [] def test_fund_output_record_has_expected_fields(self): """Test FundOutput record has expected fields.""" + txid_hex = "ab" * 32 output = glsdk.FundOutput( - txid=b"\xab" * 32, + txid=txid_hex, output=0, amount_msat=1000000000, status=glsdk.OutputStatus.CONFIRMED, address="bcrt1qtest", blockheight=100, + reserved=False, ) - assert output.txid == b"\xab" * 32 + assert output.txid == txid_hex assert output.amount_msat == 1000000000 assert output.status == glsdk.OutputStatus.CONFIRMED + assert output.reserved is False def test_fund_channel_record_has_expected_fields(self): """Test FundChannel record has expected fields.""" + peer_id_hex = "03" * 33 + funding_txid_hex = "ab" * 32 + channel_id_hex = "00" * 32 channel = glsdk.FundChannel( - peer_id=b"\x03" * 33, + peer_id=peer_id_hex, our_amount_msat=500000000, amount_msat=1000000000, - funding_txid=b"\xab" * 32, + funding_txid=funding_txid_hex, funding_output=0, connected=True, state=glsdk.ChannelState.CHANNELD_NORMAL, short_channel_id="123x1x0", - channel_id=b"\x00" * 32, + channel_id=channel_id_hex, ) - assert channel.peer_id == b"\x03" * 33 + assert channel.peer_id == peer_id_hex assert channel.our_amount_msat == 500000000 assert channel.connected is True + + +class TestNodeStateType: + """Test that NodeState type is properly defined in the bindings.""" + + def test_node_state_type_exists(self): + assert hasattr(glsdk, "NodeState") + + def test_node_state_record_has_expected_fields(self): + node_id_hex = "02" * 33 + peer_id_hex = "03" * 33 + state = glsdk.NodeState( + id=node_id_hex, + block_height=800000, + network="regtest", + version="v24.11", + alias="test-node", + color="ff0000", + num_active_channels=2, + num_pending_channels=1, + num_inactive_channels=0, + channels_balance_msat=500_000_000, + max_payable_msat=450_000_000, + total_channel_capacity_msat=1_000_000_000, + max_chan_reserve_msat=50_000_000, + onchain_balance_msat=100_000_000, + unconfirmed_onchain_balance_msat=50_000_000, + immature_onchain_balance_msat=0, + pending_onchain_balance_msat=0, + max_receivable_single_payment_msat=400_000_000, + total_inbound_liquidity_msat=800_000_000, + connected_channel_peers=[peer_id_hex], + utxos=[], + total_onchain_msat=150_000_000, + total_balance_msat=650_000_000, + spendable_balance_msat=550_000_000, + ) + assert state.id == node_id_hex + assert state.block_height == 800000 + assert state.network == "regtest" + assert state.version == "v24.11" + assert state.channels_balance_msat == 500_000_000 + assert state.max_payable_msat == 450_000_000 + assert state.total_channel_capacity_msat == 1_000_000_000 + assert state.max_chan_reserve_msat == 50_000_000 + assert state.onchain_balance_msat == 100_000_000 + assert state.unconfirmed_onchain_balance_msat == 50_000_000 + assert state.immature_onchain_balance_msat == 0 + assert state.total_onchain_msat == 150_000_000 + assert state.total_balance_msat == 650_000_000 + assert state.spendable_balance_msat == 550_000_000 + assert len(state.connected_channel_peers) == 1 + assert state.connected_channel_peers[0] == peer_id_hex + assert state.utxos == [] + + +class TestNodeStateMethod: + """Test node_state() integration.""" + + def test_node_has_node_state_method(self): + assert hasattr(glsdk.Node, "node_state") + + def test_node_state_returns_valid_snapshot(self, scheduler, nobody_id): + dev_cert = glsdk.DeveloperCert(nobody_id.cert_chain, nobody_id.private_key) + config = glsdk.Config().with_developer_cert(dev_cert) + node = glsdk.register_or_recover(MNEMONIC, None, config) + state = node.node_state() + assert isinstance(state, glsdk.NodeState) + # Node id is a lowercase hex pubkey (33 bytes → 66 chars). + assert isinstance(state.id, str) + assert len(state.id) == 66 + assert state.block_height > 0 + assert state.network == "regtest" + assert state.version != "" + assert state.channels_balance_msat == 0 + assert state.max_payable_msat == 0 + assert state.total_channel_capacity_msat == 0 + assert state.onchain_balance_msat == 0 + assert state.total_inbound_liquidity_msat == 0 + node.disconnect()