Skip to content

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

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

feat(gl-sdk): add log + node-event listener callbacks #705
angelix wants to merge 3 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 angelix force-pushed the ave-logging branch 4 times, most recently from a5b00c2 to 11736e6 Compare April 25, 2026 17:47
NodeState
- Unified node_state() snapshot aggregating get_info,
  list_peer_channels, and list_funds into a single record with
  balances, capacity, utxos, and connected-channel-peer set.
- Reserved UTXOs (in-flight PSBTs) excluded from on-chain balance.
  FundOutput now carries `reserved` from listfunds.outputs[].reserved.
- 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 a wallet UTXO and would be counted
  twice. PeerChannel 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. gl-sdk-cli output formatter
  handles the new variant too.
- Immature outputs (confirmed but timelocked, e.g. coinbase
  maturation) surface as immature_onchain_balance_msat instead of
  being silently dropped. OutputStatus match is exhaustive.
- The three underlying RPCs run concurrently via tokio::join!.

NodeState aggregate fields:
- total_onchain_msat (confirmed + unconfirmed + immature)
- total_balance_msat (everything the user owns)
- spendable_balance_msat (send-screen gate)
- max_chan_reserve_msat (protocol reserve locked across channels)
- utxos: Vec<FundOutput> for coin-control UIs

Other NodeState changes:
- channels_balance_msat / max_payable_msat docstrings explicitly
  name their roles: home-screen display vs send-button gate.
- num_active/pending/inactive_channels docstrings clarify which
  balance fields each count contributes to.
- connected_peers renamed to connected_channel_peers (only lists
  peers we have a channel with); dedup via HashSet.
- fees_collected_msat removed from NodeState (still on
  GetInfoResponse); it's a routing-node concern.

Hex identifier surface
- Every identifier (pubkey, payment hash, txid, preimage, funding
  txid, channel id) on public structs is now a lowercase hex String
  instead of Vec<u8>. Affected: 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.

list_payments fixes
- Unpaid (open) and expired invoices are dropped from list_payments.
  Only Paid invoices appear as Payment rows on the received side;
  list_invoices() still surfaces the full invoice list for callers
  that want to inspect open invoices directly.
- Payment.destination is documented as always-None for
  PaymentType::Received: Lightning's privacy model does not reveal
  the sender's pubkey to the recipient, and the only pubkey
  derivable from a paid invoice is the payee (our own node), which
  is uninteresting per-row. PaymentStatus::Pending stays on the
  public type — it remains valid for in-flight sent payments.

NAPI mirrors synced (Buffer→String everywhere the underlying field
became hex), gl-sdk-cli output formatters simplified, NodeExtensions
defaults adjusted, and Python + Kotlin tests updated to match.
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.
Returns a pretty-printed JSON envelope { timestamp, node, sdk } where
the node section serializes getinfo/listpeerchannels/listfunds and the
sdk section carries version + node_state. Failed sub-calls are embedded
as { "error": "..." } instead of failing the dump. Adds serde derive on
the response types so each section is real nested JSON, queryable with
jq. Payment and invoice history are intentionally excluded to avoid
leaking preimages, payment hashes, bolt11 strings, and labels into
support dumps.
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