feat(gl-sdk): add log + node-event listener callbacks #705
feat(gl-sdk): add log + node-event listener callbacks #705
Conversation
cdecker
left a comment
There was a problem hiding this comment.
I'm not quite happy with encumbering what is supposed to be the simple interface (naked top-level functions) with things like logging and event subscriber handling. That's more advanced concerns and exactly a problem of naked functions: they have no context other than the global static context they could be configured, hence all possible variants must be expressible in the argument list, which adds complexity back in.
I'd say if you use the simple interface you do not get to play with these advanced features, if you want those you have to use the OOP interface, where we can use builders to incrementally expose details. That is also in tune with the semver logic: naked functions change their entire interface when you add optional arguments, whereas builders can add new setters without breaking existing code.
Can we split out the log handling, which I'd merge right away, and keep the event streaming discussion separate?
| fn flush(&self) {} | ||
| } | ||
|
|
||
| static INIT: Once = Once::new(); |
There was a problem hiding this comment.
Not quite sure if Once is the right type here. It should be a Mutex<LogListener>, since otherwise multiple set_logger calls are ignored, which I don't think anybody expects.
Two changes landed together:
1. Unified node_state() snapshot aggregating get_info, list_peer_channels,
and list_funds into a single NodeState record with balances, capacity,
utxos, and connected-channel-peer set.
Balance accounting fixes vs. a naive implementation:
- Reserved UTXOs (in-flight PSBTs) are excluded from on-chain balance.
FundOutput now carries a `reserved` field sourced from CLN's
listfunds.outputs[].reserved; node_state() skips reserved outputs.
- Closing-channel double-count fixed. Onchain-state channels only
contribute to pending_onchain_balance_msat when we initiated the
close and our payout is still timelocked (DELAYED_OUTPUT_TO_US),
otherwise the payout is already visible as a wallet UTXO and would
be counted twice. PeerChannel now exposes `closer` (ChannelSide)
and `status` to support this gate.
- ChannelState::from_i32 unknown fallback now maps to a new
ChannelState::Unknown variant rather than Onchain, which silently
counted unmapped states as closing. Unknown is treated as neither
open nor closing by balance math. The gl-sdk-cli output formatter
handles the new variant too.
- Immature outputs (confirmed but timelocked, e.g. coinbase
maturation) now surface as their own immature_onchain_balance_msat
instead of being silently dropped. OutputStatus match is
exhaustive — future variants become a compile error rather than
silent zero.
- The three RPCs run concurrently via tokio::join! rather than
serially, cutting node_state() latency ~3x on remote nodes.
2. Every identifier (pubkey, payment hash, txid, preimage, funding
txid, channel id) on public structs is now a lowercase hex String
instead of Vec<u8>. Mobile/binding consumers no longer need to
re-encode these fields for display, logging, or block-explorer
deep-links. Affected types: NodeState, PeerChannel, FundOutput,
FundChannel, GetInfoResponse, Peer, Invoice, Pay, Payment,
SendResponse, OnchainSendResponse (txid only; raw tx stays bytes),
InvoicePaidEvent, ParsedInvoice.
Kept as Vec<u8>: Credentials::load/save bytes, raw on-chain tx
bytes, Peer.features bitfield, DeveloperCert/Signer constructor
arguments.
Mobile-developer API surface additions:
- New aggregate NodeState fields: total_onchain_msat (confirmed +
unconfirmed + immature), total_balance_msat (everything the user
owns), spendable_balance_msat (send-screen gate). All msat for
consistency.
- max_chan_reserve_msat (protocol reserve locked across channels).
- utxos: Vec<FundOutput> for coin-control UIs. Excludes spent.
- Docstrings on channels_balance_msat / max_payable_msat explicitly
name their roles: home-screen display vs send-button gate.
- Docstrings on num_active/pending/inactive_channels clarify which
balance fields each count contributes to.
- connected_peers renamed to connected_channel_peers to reflect that
it only lists peers we have a channel with; dedup via HashSet.
- fees_collected_msat removed from NodeState (still available on
GetInfoResponse); it is a routing-node concern that doesn't belong
in the wallet balance snapshot.
NAPI mirrors synced (Buffer→String everywhere the underlying field
became hex), gl-sdk-cli output formatters simplified to pass through
hex strings, and Python + Kotlin NodeState tests updated to match
the new shapes.
Two callback-based listener APIs for mobile integrators. Logging - LogListener: apps implement on_log(LogEntry) to receive log messages from both gl-sdk (via tracing's log bridge) and gl-client (111 log::*! callsites flow through automatically). - LogEntry carries level, message, target module, and source file + line for easier triage of production issues. - LogLevel variants use Rust PascalCase (Error/Warn/Info/Debug/Trace) matching the ChannelState/OutputStatus/PaymentStatus convention; bindings still render as UPPER_SNAKE per uniffi convention. - set_logger(level, listener) returns Result<(), Error>. First call installs; subsequent calls fail with an explicit error instead of panicking or silently no-op'ing. Internal Once guard removed — log::set_boxed_logger already enforces at-most-once. - set_log_level(level) exported: callers can change the filter at runtime without reinstalling, driving a "verbose logs" UI toggle. Implemented by reading log::max_level() inside Log::enabled so the logger picks up global filter changes with no stored state. - Docstrings call out that on_log runs on the thread that emitted the log — implementations should be cheap and non-blocking. - Exposed to JS via a NAPI wrapper that bridges a ThreadsafeFunction to LogListener (napi4 feature enabled). Node events - NodeEventListener: apps implement on_event(NodeEvent) to receive live events. Current variant: InvoicePaid. Unknown server events are skipped silently so the public enum stays exhaustive for integrator match statements. - Replaces the pull-based NodeEventStream + next() surface entirely. NodeEventStream is gone. - Listener is strictly a connect-time concern: register / recover / connect / register_or_recover each gain an event_listener: Option<Box<dyn NodeEventListener>> parameter. The SDK installs it atomically during node bring-up, so events that fire during the first round of RPCs are not missed. - set_event_listener is pub(crate) — not exposed via uniffi or NAPI. Callers who need post-construction flexibility wire a mutable delegate inside their listener implementation (Flow/LiveData/ObservableObject pattern). - Single listener per Node. Drop for Node aborts the dispatch task so teardown is automatic. - InvoicePaidEvent exposes payment_hash and preimage as lowercase hex strings, matching the mobile-facing hex convention used elsewhere. gl-sdk-cli - StreamEvents subcommand removed along with its NodeEventOutput type and AtomicBool/ctrl-c scaffolding. The pull API it used is gone, and Node::new(creds) has no path to install a listener. Tests - Python test_logging.py verifies LogLevel variants, LogEntry shape, LogListener trait, and that set_log_level is callable. - Kotlin LoggingTest.kt installs the listener in @BeforeClass (handles the "already installed" error gracefully) and asserts that register_or_recover actually drives log entries through the listener with non-empty targets. - AuthApi / ListPayment / NodeOperations instrumented tests updated to pass null for the new event_listener argument.
Two callback-based listener APIs for mobile integrators, both
following the Breez SDK shape.
Logging
from both gl-sdk (via tracing's log bridge) and gl-client (111
log::*! callsites flow through automatically).
for easier triage of production issues.
matching the ChannelState/OutputStatus/PaymentStatus convention;
bindings still render as UPPER_SNAKE per uniffi convention.
installs; subsequent calls are silent no-ops. Returns an error only
when another crate has already installed a log logger, replacing the
previous .expect("logger already set") panic.
LogListener (napi4 feature enabled).
Node events
events (InvoicePaid today; room for more).
NodeEventStream is gone.
register / recover / connect / register_or_recover each gain an
event_listener: Option<Box> parameter. The
SDK installs it atomically during node bring-up, so events that fire
during the first round of RPCs are not missed.
Callers who need post-construction flexibility wire a mutable delegate
inside their listener implementation (Flow/LiveData/ObservableObject
pattern). Matches Breez's EventListener shape.
teardown is automatic.
Tests
accepts a listener.
as a smoke test for cross-language log capture.