Skip to content

feat(gl-sdk): add log + node-event listener callbacks #705

Open
angelix wants to merge 2 commits intomainfrom
ave-logging
Open

feat(gl-sdk): add log + node-event listener callbacks #705
angelix wants to merge 2 commits intomainfrom
ave-logging

Conversation

@angelix
Copy link
Copy Markdown
Contributor

@angelix angelix commented Apr 24, 2026

Two callback-based listener APIs for mobile integrators, both
following the Breez SDK shape.

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 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.
  • 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 (InvoicePaid today; room for more).
  • 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> 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). Matches Breez's EventListener shape.
  • Single listener per Node. Drop for Node aborts the dispatch task so
    teardown is automatic.

Tests

  • Python test_logging.py asserts the logging types exist and set_logger
    accepts a listener.
  • Kotlin LoggingTest.kt installs a listener and drives register_or_recover
    as a smoke test for cross-language log capture.

Copy link
Copy Markdown
Collaborator

@cdecker cdecker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread libs/gl-sdk/src/logging.rs Outdated
fn flush(&self) {}
}

static INIT: Once = Once::new();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

angelix added 2 commits April 25, 2026 02:42
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants