feat(gl-sdk): builder-style Node creation, signerless by default#709
Open
feat(gl-sdk): builder-style Node creation, signerless by default#709
Conversation
1baf3b0 to
b44e390
Compare
b44e390 to
bc247f8
Compare
cdecker
reviewed
Apr 27, 2026
Collaborator
cdecker
left a comment
There was a problem hiding this comment.
Just a minor thing about losing the Signerless support, otherwise good to go 👍
| /// No I/O happens until you call `register` / `recover` / | ||
| /// `connect` / `register_or_recover`. | ||
| #[uniffi::constructor] | ||
| pub fn new(mnemonic: String, config: &Config) -> Arc<Self> { |
Collaborator
There was a problem hiding this comment.
Yeah, this makes the seed / mnemonic mandatory, which means we no longer have signerless clients.
Contributor
Author
There was a problem hiding this comment.
Added support to signerless clients
| /// callers stay source-compatible. | ||
| #[derive(uniffi::Object)] | ||
| pub struct NodeBuilder { | ||
| state: Mutex<BuilderState>, |
Collaborator
There was a problem hiding this comment.
Not quite sure why this needs to be wrapped in a mutex?
Collaborator
There was a problem hiding this comment.
Is it because it is a uniffi::Object which implies Clone maybe?
857277d to
fe92a73
Compare
fe92a73 to
f202cb3
Compare
7 tasks
NodeBuilder is the sole public entry point for Node construction
across all foreign bindings. The former free functions register /
recover / connect / register_or_recover are demoted to crate-private
helpers (`*_internal`); foreign-binding consumers go through
`NodeBuilder` exclusively.
Two design rules drive the shape:
1. Naked free functions are hard to extend without semver breaks. A
builder absorbs new modifiers as additional with_* setters —
additive forever.
2. Signer access ≡ root access on the node (self-certifies runes,
mints TLS certs). The SDK must support keyless clients that don't
hold the seed at all (paired devices, browser extensions, hardware
signers). Signerless connect must be a first-class path, not an
afterthought.
Resulting surface:
// Signerless connect — caller has no mnemonic in this process.
// SDK runs no signer; signing happens at the CLN node, a paired
// device, or hardware. The keyless-client model.
let node = NodeBuilder::new(&config).connect(credentials, None)?;
// Signed connect — caller hands the mnemonic per-call, SDK
// spawns a signer for the lifetime of the Node.
let node = NodeBuilder::new(&config).connect(credentials, Some(mnemonic))?;
// Register / recover / register_or_recover require a mnemonic
// by definition (the signer must sign the registration /
// recovery challenge). Mnemonic is positional, not stateful.
let node = NodeBuilder::new(&config)
.with_event_listener(listener)
.register(mnemonic, invite_code)?;
The mnemonic is never stored on the builder. It is a positional
argument on the build call that needs it, so its lifetime is bounded
to that call and there is no half-set state. Modifiers like
with_event_listener live on the builder; secrets do not.
Surface
- NodeBuilder::new(config) — collects config + optional modifiers,
no I/O.
- with_event_listener(listener) — fluent setter that returns a fresh
`Arc<NodeBuilder>` carrying the new listener; the original builder
is unchanged. No interior mutability.
- register(mnemonic, invite_code) — mnemonic required.
- recover(mnemonic) — mnemonic required.
- register_or_recover(mnemonic, invite_code) — mnemonic required.
- connect(credentials, mnemonic: Option<String>) — mnemonic optional;
None produces a signerless Node.
Builder shape
- Two fields, both immutable after construction: `config:
Arc<Config>` and `event_listener: Option<Arc<dyn NodeEventListener>>`.
No `Mutex`, no `RefCell`, no interior mutability anywhere — the
builder is a value, not a state machine.
- `with_*` setters take `self: Arc<Self>` and return a new
`Arc<NodeBuilder>` with the modified field, sharing the rest via
`Arc::clone`. Single small allocation per setter call; the rest is
pointer copies.
- The listener is stored as `Arc<dyn NodeEventListener>` so the same
builder can drive multiple builds — each build clones the Arc and
hands it to the resulting Node. (UniFFI's callback lowering hands
us a `Box<dyn Trait>` at the FFI boundary; the setter re-wraps it
via `Arc::from(box)` once.)
Implementation notes
- Node::signerless(credentials) is a new pub (non-UniFFI-export)
Rust constructor. Used by the builder for the None-mnemonic path
and by gl-sdk-napi to back its `new Node(credentials)` constructor
with the same signerless semantics.
- crate::connect_signerless_internal wires Node::signerless into
the lib.rs internals; crate::connect_internal continues to drive
the signed connect via the SDK signer spawn.
- Node::set_event_listener (pub(crate)) takes
`Arc<dyn NodeEventListener>`; spawns a background tokio task that
tails the gRPC event stream and dispatches to listener.on_event.
The task is aborted on Drop and replaced if a new listener is set.
- The polling-style Node::stream_node_events() API stays for callers
who prefer to drive events themselves; the builder route is just a
callback-style alternative that can't miss early events.
Tests migrated
- Python: test_auth_api.py (24 callsites), test_list_payments.py
(6), test_node_methods.py (2).
- Kotlin: AuthApiTest.kt (8), NodeOperationsTest.kt (2),
ListPaymentTest.kt (3), LoggingTest.kt (1). All converted to the
positional-mnemonic-on-build-call shape.
Verified: `cargo build -p gl-sdk -p gl-sdk-node` clean; Python
bindings expose `NodeBuilder` (with the new method signatures) and
`NodeEventListener` and no longer expose the demoted free
functions.
f202cb3 to
1d74506
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
NodeBuilder is the sole public entry point for Node construction
across all foreign bindings. The former free functions register /
recover / connect / register_or_recover are demoted to crate-private
helpers (
*_internal); foreign-binding consumers go throughNodeBuilderexclusively.Two design rules drive the shape:
builder absorbs new modifiers as additional with_* setters —
additive forever.
mints TLS certs). The SDK must support keyless clients that don't
hold the seed at all (paired devices, browser extensions, hardware
signers). Signerless connect must be a first-class path, not an
afterthought.
Resulting surface:
The mnemonic is never stored on the builder. It is a positional
argument on the build call that needs it, so its lifetime is bounded
to that call and there is no half-set state. Modifiers like
with_event_listener live on the builder; secrets do not.
Surface
no I/O.
Arc so calls chain in every binding.
None produces a signerless Node.
Implementation notes
Rust constructor. Used by the builder for the None-mnemonic path
and by gl-sdk-napi to back its
new Node(credentials)constructorwith the same signerless semantics.
the lib.rs internals; crate::connect_internal continues to drive
the signed connect via the SDK signer spawn.
(pub(crate)) which spawns a background tokio task that tails the
gRPC event stream and dispatches to listener.on_event. The task
is aborted on Drop and replaced if a new listener is set.
who prefer to drive events themselves; the builder route is just
a callback-style alternative that can't miss early events.
so reusing a NodeBuilder for a second build requires re-setting
the listener.
Tests migrated
(6), test_node_methods.py (2).
ListPaymentTest.kt (3), LoggingTest.kt (1). All converted to the
positional-mnemonic-on-build-call shape.
Verified:
cargo build -p gl-sdk -p gl-sdk-nodeclean; Pythonbindings expose
NodeBuilder(with the new method signatures) andNodeEventListenerand no longer expose the demoted freefunctions.