From c05fe41f19d1d91f2769fc3bc52c9e6d6430bc70 Mon Sep 17 00:00:00 2001 From: Angelos Veglektsis Date: Mon, 27 Apr 2026 13:06:11 +0300 Subject: [PATCH] feat(gl-sdk): builder-style Node creation, signerless by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` 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) — mnemonic optional; None produces a signerless Node. Builder shape - Two fields, both immutable after construction: `config: Arc` and `event_listener: Option>`. No `Mutex`, no `RefCell`, no interior mutability anywhere — the builder is a value, not a state machine. - `with_*` setters take `self: Arc` and return a new `Arc` 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` 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` 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`; 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. --- gitlab/deploy-maven.yml | 16 +- .../com/blockstream/glsdk/AuthApiTest.kt | 16 +- .../com/blockstream/glsdk/ListPaymentTest.kt | 6 +- .../com/blockstream/glsdk/LoggingTest.kt | 2 +- .../blockstream/glsdk/NodeOperationsTest.kt | 4 +- libs/gl-sdk-cli/src/node.rs | 4 +- libs/gl-sdk-cli/src/output.rs | 3 - libs/gl-sdk-napi/src/lib.rs | 15 +- libs/gl-sdk/glsdk/glsdk.py | 565 +++++++++++++----- libs/gl-sdk/src/lib.rs | 91 +-- libs/gl-sdk/src/node.rs | 152 ++++- libs/gl-sdk/src/node_builder.rs | 162 +++++ libs/gl-sdk/tests/test_auth_api.py | 42 +- libs/gl-sdk/tests/test_list_payments.py | 12 +- libs/gl-sdk/tests/test_node_methods.py | 4 +- 15 files changed, 810 insertions(+), 284 deletions(-) create mode 100644 libs/gl-sdk/src/node_builder.rs diff --git a/gitlab/deploy-maven.yml b/gitlab/deploy-maven.yml index 7c67363b7..4c15bd8ca 100644 --- a/gitlab/deploy-maven.yml +++ b/gitlab/deploy-maven.yml @@ -50,7 +50,11 @@ publish_snapshot_to_maven: - job: build_kotlin artifacts: true rules: - - if: '$CI_COMMIT_BRANCH == "main"' + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + when: never + - if: '$CI_COMMIT_BRANCH == "master"' + when: on_success + - if: '$CI_COMMIT_BRANCH' when: manual script: - cd libs/gl-sdk-android @@ -64,6 +68,12 @@ publish_snapshot_to_maven: - NEXT_PATCH=$((PATCH + 1)) - NEXT_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}" - SNAPSHOT_VERSION="${NEXT_VERSION}-SNAPSHOT" - - echo "Publishing snapshot version ${SNAPSHOT_VERSION} (base=${BASE_VERSION})" - - ./gradlew -PlibraryVersion=${SNAPSHOT_VERSION} publish --no-daemon + - SNAPSHOT_VERSION_COMMIT="${NEXT_VERSION}-${GIT_COMMIT}-SNAPSHOT" + - | + if [ "$CI_COMMIT_BRANCH" = "master" ]; then + echo "Publishing snapshot versions ${SNAPSHOT_VERSION}" + ./gradlew -PlibraryVersion=${SNAPSHOT_VERSION} publish --no-daemon + fi + echo "Publishing snapshot version ${SNAPSHOT_VERSION_COMMIT}" + ./gradlew -PlibraryVersion=${SNAPSHOT_VERSION_COMMIT} publish --no-daemon allow_failure: true diff --git a/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/AuthApiTest.kt b/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/AuthApiTest.kt index 5602b1ded..418f3486e 100644 --- a/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/AuthApiTest.kt +++ b/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/AuthApiTest.kt @@ -65,19 +65,19 @@ class AuthApiTest { @Test(expected = Exception.PhraseCorrupted::class) fun register_bad_mnemonic() { val config = Config() - register("not a valid mnemonic", null, config) + NodeBuilder(config).register("not a valid mnemonic", null) } @Test(expected = Exception.PhraseCorrupted::class) fun recover_bad_mnemonic() { val config = Config() - recover("not a valid mnemonic", config) + NodeBuilder(config).recover("not a valid mnemonic") } @Test(expected = Exception.PhraseCorrupted::class) fun connect_bad_mnemonic() { val config = Config() - connect("not a valid mnemonic", "fake-creds".toByteArray(), config) + NodeBuilder(config).connect("fake-creds".toByteArray(), "not a valid mnemonic") } // ============================================================ @@ -88,7 +88,7 @@ class AuthApiTest { @Test fun register_or_recover_returns_node() { val config = Config() - val node = registerOrRecover(testMnemonic, null, config) + val node = NodeBuilder(config).registerOrRecover(testMnemonic, null) assertNotNull(node) node.use { n -> val creds = n.credentials() @@ -104,12 +104,12 @@ class AuthApiTest { // Register or recover to get credentials val savedCreds: ByteArray - registerOrRecover(testMnemonic, null, config).use { node -> + NodeBuilder(config).registerOrRecover(testMnemonic, null).use { node -> savedCreds = node.credentials() } // Connect with the saved credentials - connect(testMnemonic, savedCreds, config).use { node -> + NodeBuilder(config).connect(savedCreds, testMnemonic).use { node -> assertNotNull(node) val reconnectedCreds = node.credentials() assertTrue("Reconnected credentials should not be empty", reconnectedCreds.isNotEmpty()) @@ -124,7 +124,7 @@ class AuthApiTest { fun disconnect_is_idempotent() { val config = Config() - val node = registerOrRecover(testMnemonic, null, config) + val node = NodeBuilder(config).registerOrRecover(testMnemonic, null) // First disconnect node.disconnect() // Second disconnect should not throw @@ -139,7 +139,7 @@ class AuthApiTest { @Test fun register_or_recover_and_create_invoice() { val config = Config() - registerOrRecover(testMnemonic, null, config).use { node -> + NodeBuilder(config).registerOrRecover(testMnemonic, null).use { node -> val addrResponse = node.onchainReceive() assertNotNull(addrResponse) println("Deposit funds to: $addrResponse") diff --git a/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/ListPaymentTest.kt b/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/ListPaymentTest.kt index 5a962bfbe..d12c2661e 100644 --- a/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/ListPaymentTest.kt +++ b/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/ListPaymentTest.kt @@ -63,7 +63,7 @@ class ListPaymentTest { @Test fun created_invoice_appears_in_list_invoices() { val config = Config() - registerOrRecover(testMnemonic, null, config).use { node -> + NodeBuilder(config).registerOrRecover(testMnemonic, null).use { node -> val label = Uuid.random().toString() node.receive(label = label, description = "Coffee", amountMsat = 10_000_000uL) @@ -84,7 +84,7 @@ class ListPaymentTest { @Test fun unpaid_invoices_excluded() { val config = Config() - registerOrRecover(testMnemonic, null, config).use { node -> + NodeBuilder(config).registerOrRecover(testMnemonic, null).use { node -> val label = Uuid.random().toString() node.receive(label = label, description = "Tea", amountMsat = 5_000_000uL) @@ -107,7 +107,7 @@ class ListPaymentTest { @Test fun type_filter_received_only() { val config = Config() - registerOrRecover(testMnemonic, null, config).use { node -> + NodeBuilder(config).registerOrRecover(testMnemonic, null).use { node -> val label = Uuid.random().toString() node.receive(label = label, description = "Tea", amountMsat = 5_000_000uL) diff --git a/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/LoggingTest.kt b/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/LoggingTest.kt index 780a855f2..d54f268db 100644 --- a/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/LoggingTest.kt +++ b/libs/gl-sdk-android/lib/src/androidInstrumentedTest/kotlin/com/blockstream/glsdk/LoggingTest.kt @@ -53,7 +53,7 @@ class LoggingTest { val config = Config() val mnemonic = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong" try { - registerOrRecover(mnemonic, null, config) + NodeBuilder(config).registerOrRecover(mnemonic, null) } catch (_: Exception) { // May fail on network / credentials — we only care that logs flowed. } 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 597c3d6aa..2c4061bb3 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 @@ -27,7 +27,7 @@ class NodeOperationsTest { fun test_onchain_receive_and_invoice() { val config = Config() - val node = registerOrRecover(mnemonic = testMnemonic, inviteCode = null, config = config) + val node = NodeBuilder(config).registerOrRecover(testMnemonic, null) node.use { n -> // Get an on-chain address to fund the node @@ -47,7 +47,7 @@ class NodeOperationsTest { @Test fun test_node_state_returns_valid_snapshot() { val config = Config() - val node = registerOrRecover(mnemonic = testMnemonic, inviteCode = null, config = config) + val node = NodeBuilder(config).registerOrRecover(testMnemonic, null) node.use { n -> val state = n.nodeState() assertTrue(state.id.isNotEmpty()) diff --git a/libs/gl-sdk-cli/src/node.rs b/libs/gl-sdk-cli/src/node.rs index 448a1da8d..6aad93a6f 100644 --- a/libs/gl-sdk-cli/src/node.rs +++ b/libs/gl-sdk-cli/src/node.rs @@ -52,7 +52,9 @@ pub enum Command { pub fn handle(cmd: Command, data_dir: &DataDir) -> Result<()> { let creds = util::read_credentials(data_dir)?; - let node = glsdk::Node::new(&creds).map_err(|e| Error::Other(e.to_string()))?; + // CLI wraps an externally-running signer (the gl-client signer + // launched out-of-process); the SDK Node is signerless. + let node = glsdk::Node::signerless(creds).map_err(|e| Error::Other(e.to_string()))?; match cmd { Command::GetInfo => get_info(&node), diff --git a/libs/gl-sdk-cli/src/output.rs b/libs/gl-sdk-cli/src/output.rs index d1fd72ba5..f09dca29a 100644 --- a/libs/gl-sdk-cli/src/output.rs +++ b/libs/gl-sdk-cli/src/output.rs @@ -323,8 +323,6 @@ pub enum NodeEventOutput { label: String, amount_msat: u64, }, - #[serde(rename = "unknown")] - Unknown, } impl From for NodeEventOutput { @@ -337,7 +335,6 @@ impl From for NodeEventOutput { label: details.label, amount_msat: details.amount_msat, }, - glsdk::NodeEvent::Unknown => NodeEventOutput::Unknown, } } } diff --git a/libs/gl-sdk-napi/src/lib.rs b/libs/gl-sdk-napi/src/lib.rs index 9a61e0447..e01bf6701 100644 --- a/libs/gl-sdk-napi/src/lib.rs +++ b/libs/gl-sdk-napi/src/lib.rs @@ -503,15 +503,20 @@ impl NodeEventStream { #[napi] impl Node { - /// Create a new node connection + /// Create a signerless node from credentials. + /// + /// No SDK-side signer runs — signing happens elsewhere (paired + /// device, hardware signer, the CLN node's local signer). For + /// the SDK-as-signer model, use the `register` / `recover` / + /// `connect` free functions with a mnemonic. /// /// # Arguments /// * `credentials` - Device credentials #[napi(constructor)] pub fn new(credentials: &Credentials) -> Result { - // Constructor stays sync — connection is established lazily + // Connection is established lazily on first RPC. let inner = - GlNode::new(&credentials.inner).map_err(|e| Error::from_reason(e.to_string()))?; + GlNode::signerless(credentials.inner.clone()).map_err(|e| Error::from_reason(e.to_string()))?; Ok(Self { inner: std::sync::Arc::new(inner) }) } @@ -866,10 +871,6 @@ fn napi_node_event_from_gl(event: GlNodeEvent) -> NodeEvent { amount_msat: details.amount_msat as i64, }), }, - GlNodeEvent::Unknown => NodeEvent { - event_type: "unknown".to_string(), - invoice_paid: None, - }, } } diff --git a/libs/gl-sdk/glsdk/glsdk.py b/libs/gl-sdk/glsdk/glsdk.py index c9c470dcd..2099af126 100644 --- a/libs/gl-sdk/glsdk/glsdk.py +++ b/libs/gl-sdk/glsdk/glsdk.py @@ -460,16 +460,8 @@ def _uniffi_check_contract_api_version(lib): raise InternalError("UniFFI contract version mismatch: try cleaning and rebuilding your project") def _uniffi_check_api_checksums(lib): - if lib.uniffi_glsdk_checksum_func_connect() != 43555: - raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_func_parse_input() != 12312: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_glsdk_checksum_func_recover() != 39257: - raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_glsdk_checksum_func_register() != 39628: - raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_glsdk_checksum_func_register_or_recover() != 65070: - raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_func_set_log_level() != 52328: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_func_set_logger() != 10523: @@ -518,6 +510,16 @@ 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_stream_node_events() != 5933: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_method_nodebuilder_connect() != 47474: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_method_nodebuilder_recover() != 46087: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_method_nodebuilder_register() != 49580: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_method_nodebuilder_register_or_recover() != 5543: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_method_nodebuilder_with_event_listener() != 56760: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_nodeeventstream_next() != 12635: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_scheduler_recover() != 55514: @@ -538,7 +540,7 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_constructor_developercert_new() != 57793: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - if lib.uniffi_glsdk_checksum_constructor_node_new() != 7003: + if lib.uniffi_glsdk_checksum_constructor_nodebuilder_new() != 34740: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_constructor_scheduler_new() != 15239: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") @@ -548,6 +550,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_loglistener_on_log() != 34844: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_method_nodeeventlistener_on_event() != 17790: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") # A ctypes library to expose the extern-C FFI definitions. # This is an implementation detail which will be called internally by the public API. @@ -657,11 +661,19 @@ class _UniffiForeignFutureStructVoid(ctypes.Structure): _UNIFFI_CALLBACK_INTERFACE_LOG_LISTENER_METHOD0 = ctypes.CFUNCTYPE(None,ctypes.c_uint64,_UniffiRustBuffer,ctypes.c_void_p, ctypes.POINTER(_UniffiRustCallStatus), ) +_UNIFFI_CALLBACK_INTERFACE_NODE_EVENT_LISTENER_METHOD0 = ctypes.CFUNCTYPE(None,ctypes.c_uint64,_UniffiRustBuffer,ctypes.c_void_p, + ctypes.POINTER(_UniffiRustCallStatus), +) class _UniffiVTableCallbackInterfaceLogListener(ctypes.Structure): _fields_ = [ ("on_log", _UNIFFI_CALLBACK_INTERFACE_LOG_LISTENER_METHOD0), ("uniffi_free", _UNIFFI_CALLBACK_INTERFACE_FREE), ] +class _UniffiVTableCallbackInterfaceNodeEventListener(ctypes.Structure): + _fields_ = [ + ("on_event", _UNIFFI_CALLBACK_INTERFACE_NODE_EVENT_LISTENER_METHOD0), + ("uniffi_free", _UNIFFI_CALLBACK_INTERFACE_FREE), + ] _UniffiLib.uniffi_glsdk_fn_clone_config.argtypes = ( ctypes.c_void_p, ctypes.POINTER(_UniffiRustCallStatus), @@ -754,11 +766,6 @@ class _UniffiVTableCallbackInterfaceLogListener(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_glsdk_fn_free_node.restype = None -_UniffiLib.uniffi_glsdk_fn_constructor_node_new.argtypes = ( - ctypes.c_void_p, - ctypes.POINTER(_UniffiRustCallStatus), -) -_UniffiLib.uniffi_glsdk_fn_constructor_node_new.restype = ctypes.c_void_p _UniffiLib.uniffi_glsdk_fn_method_node_credentials.argtypes = ( ctypes.c_void_p, ctypes.POINTER(_UniffiRustCallStatus), @@ -865,6 +872,54 @@ class _UniffiVTableCallbackInterfaceLogListener(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_glsdk_fn_method_node_stream_node_events.restype = ctypes.c_void_p +_UniffiLib.uniffi_glsdk_fn_clone_nodebuilder.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_clone_nodebuilder.restype = ctypes.c_void_p +_UniffiLib.uniffi_glsdk_fn_free_nodebuilder.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_free_nodebuilder.restype = None +_UniffiLib.uniffi_glsdk_fn_constructor_nodebuilder_new.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_constructor_nodebuilder_new.restype = ctypes.c_void_p +_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_connect.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + _UniffiRustBuffer, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_connect.restype = ctypes.c_void_p +_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_recover.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_recover.restype = ctypes.c_void_p +_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_register.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + _UniffiRustBuffer, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_register.restype = ctypes.c_void_p +_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_register_or_recover.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + _UniffiRustBuffer, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_register_or_recover.restype = ctypes.c_void_p +_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_with_event_listener.argtypes = ( + ctypes.c_void_p, + ctypes.c_uint64, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_with_event_listener.restype = ctypes.c_void_p _UniffiLib.uniffi_glsdk_fn_clone_nodeeventstream.argtypes = ( ctypes.c_void_p, ctypes.POINTER(_UniffiRustCallStatus), @@ -954,38 +1009,15 @@ class _UniffiVTableCallbackInterfaceLogListener(ctypes.Structure): ctypes.POINTER(_UniffiVTableCallbackInterfaceLogListener), ) _UniffiLib.uniffi_glsdk_fn_init_callback_vtable_loglistener.restype = None -_UniffiLib.uniffi_glsdk_fn_func_connect.argtypes = ( - _UniffiRustBuffer, - _UniffiRustBuffer, - ctypes.c_void_p, - ctypes.POINTER(_UniffiRustCallStatus), +_UniffiLib.uniffi_glsdk_fn_init_callback_vtable_nodeeventlistener.argtypes = ( + ctypes.POINTER(_UniffiVTableCallbackInterfaceNodeEventListener), ) -_UniffiLib.uniffi_glsdk_fn_func_connect.restype = ctypes.c_void_p +_UniffiLib.uniffi_glsdk_fn_init_callback_vtable_nodeeventlistener.restype = None _UniffiLib.uniffi_glsdk_fn_func_parse_input.argtypes = ( _UniffiRustBuffer, ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_glsdk_fn_func_parse_input.restype = _UniffiRustBuffer -_UniffiLib.uniffi_glsdk_fn_func_recover.argtypes = ( - _UniffiRustBuffer, - ctypes.c_void_p, - ctypes.POINTER(_UniffiRustCallStatus), -) -_UniffiLib.uniffi_glsdk_fn_func_recover.restype = ctypes.c_void_p -_UniffiLib.uniffi_glsdk_fn_func_register.argtypes = ( - _UniffiRustBuffer, - _UniffiRustBuffer, - ctypes.c_void_p, - ctypes.POINTER(_UniffiRustCallStatus), -) -_UniffiLib.uniffi_glsdk_fn_func_register.restype = ctypes.c_void_p -_UniffiLib.uniffi_glsdk_fn_func_register_or_recover.argtypes = ( - _UniffiRustBuffer, - _UniffiRustBuffer, - ctypes.c_void_p, - ctypes.POINTER(_UniffiRustCallStatus), -) -_UniffiLib.uniffi_glsdk_fn_func_register_or_recover.restype = ctypes.c_void_p _UniffiLib.uniffi_glsdk_fn_func_set_log_level.argtypes = ( _UniffiRustBuffer, ctypes.POINTER(_UniffiRustCallStatus), @@ -1265,21 +1297,9 @@ class _UniffiVTableCallbackInterfaceLogListener(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.ffi_glsdk_rust_future_complete_void.restype = None -_UniffiLib.uniffi_glsdk_checksum_func_connect.argtypes = ( -) -_UniffiLib.uniffi_glsdk_checksum_func_connect.restype = ctypes.c_uint16 _UniffiLib.uniffi_glsdk_checksum_func_parse_input.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_func_parse_input.restype = ctypes.c_uint16 -_UniffiLib.uniffi_glsdk_checksum_func_recover.argtypes = ( -) -_UniffiLib.uniffi_glsdk_checksum_func_recover.restype = ctypes.c_uint16 -_UniffiLib.uniffi_glsdk_checksum_func_register.argtypes = ( -) -_UniffiLib.uniffi_glsdk_checksum_func_register.restype = ctypes.c_uint16 -_UniffiLib.uniffi_glsdk_checksum_func_register_or_recover.argtypes = ( -) -_UniffiLib.uniffi_glsdk_checksum_func_register_or_recover.restype = ctypes.c_uint16 _UniffiLib.uniffi_glsdk_checksum_func_set_log_level.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_func_set_log_level.restype = ctypes.c_uint16 @@ -1352,6 +1372,21 @@ class _UniffiVTableCallbackInterfaceLogListener(ctypes.Structure): _UniffiLib.uniffi_glsdk_checksum_method_node_stream_node_events.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_method_node_stream_node_events.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_connect.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_connect.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_recover.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_recover.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_register.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_register.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_register_or_recover.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_register_or_recover.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_with_event_listener.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_method_nodebuilder_with_event_listener.restype = ctypes.c_uint16 _UniffiLib.uniffi_glsdk_checksum_method_nodeeventstream_next.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_method_nodeeventstream_next.restype = ctypes.c_uint16 @@ -1382,9 +1417,9 @@ class _UniffiVTableCallbackInterfaceLogListener(ctypes.Structure): _UniffiLib.uniffi_glsdk_checksum_constructor_developercert_new.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_constructor_developercert_new.restype = ctypes.c_uint16 -_UniffiLib.uniffi_glsdk_checksum_constructor_node_new.argtypes = ( +_UniffiLib.uniffi_glsdk_checksum_constructor_nodebuilder_new.argtypes = ( ) -_UniffiLib.uniffi_glsdk_checksum_constructor_node_new.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_constructor_nodebuilder_new.restype = ctypes.c_uint16 _UniffiLib.uniffi_glsdk_checksum_constructor_scheduler_new.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_constructor_scheduler_new.restype = ctypes.c_uint16 @@ -1397,6 +1432,9 @@ class _UniffiVTableCallbackInterfaceLogListener(ctypes.Structure): _UniffiLib.uniffi_glsdk_checksum_method_loglistener_on_log.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_method_loglistener_on_log.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_method_nodeeventlistener_on_event.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_method_nodeeventlistener_on_event.restype = ctypes.c_uint16 _UniffiLib.ffi_glsdk_uniffi_contract_version.argtypes = ( ) _UniffiLib.ffi_glsdk_uniffi_contract_version.restype = ctypes.c_uint32 @@ -1554,6 +1592,8 @@ def write(value, buf): + + class FundChannel: peer_id: "str" """ @@ -4179,24 +4219,6 @@ def __eq__(self, other): return False return True - class UNKNOWN: - """ - An unknown event type was received. This can happen if the - server sends a new event type that this client doesn't know about. - """ - - - def __init__(self,): - pass - - def __str__(self): - return "NodeEvent.UNKNOWN()".format() - - def __eq__(self, other): - if not other.is_UNKNOWN(): - return False - return True - # For each variant, we have `is_NAME` and `is_name` methods for easily checking @@ -4205,17 +4227,12 @@ def is_INVOICE_PAID(self) -> bool: return isinstance(self, NodeEvent.INVOICE_PAID) def is_invoice_paid(self) -> bool: return isinstance(self, NodeEvent.INVOICE_PAID) - def is_UNKNOWN(self) -> bool: - return isinstance(self, NodeEvent.UNKNOWN) - def is_unknown(self) -> bool: - return isinstance(self, NodeEvent.UNKNOWN) # Now, a little trick - we make each nested variant class be a subclass of the main # enum class, so that method calls and instance checks etc will work intuitively. # We might be able to do this a little more neatly with a metaclass, but this'll do. NodeEvent.INVOICE_PAID = type("NodeEvent.INVOICE_PAID", (NodeEvent.INVOICE_PAID, NodeEvent,), {}) # type: ignore -NodeEvent.UNKNOWN = type("NodeEvent.UNKNOWN", (NodeEvent.UNKNOWN, NodeEvent,), {}) # type: ignore @@ -4228,9 +4245,6 @@ def read(buf): return NodeEvent.INVOICE_PAID( _UniffiConverterTypeInvoicePaidEvent.read(buf), ) - if variant == 2: - return NodeEvent.UNKNOWN( - ) raise InternalError("Raw enum value doesn't match any cases") @staticmethod @@ -4238,8 +4252,6 @@ def check_lower(value): if value.is_INVOICE_PAID(): _UniffiConverterTypeInvoicePaidEvent.check_lower(value.details) return - if value.is_UNKNOWN(): - return raise ValueError(value) @staticmethod @@ -4247,8 +4259,6 @@ def write(value, buf): if value.is_INVOICE_PAID(): buf.write_i32(1) _UniffiConverterTypeInvoicePaidEvent.write(value.details, buf) - if value.is_UNKNOWN(): - buf.write_i32(2) @@ -4534,6 +4544,68 @@ def _uniffi_free(uniffi_handle): + +class NodeEventListener(typing.Protocol): + """ + Callback interface for receiving node events. + + `on_event` is invoked from the SDK's internal event-dispatch task. + Implementations should be cheap and non-blocking; to update UI, + dispatch to the main thread from inside the handler. + + Installed via `NodeBuilder::with_event_listener(...)` so events + emitted during node bring-up are captured. The polling-style + `Node::stream_node_events()` API is still available for callers + that prefer to drive events themselves. + """ + + def on_event(self, event: "NodeEvent"): + raise NotImplementedError + + +# Put all the bits inside a class to keep the top-level namespace clean +class _UniffiTraitImplNodeEventListener: + # For each method, generate a callback function to pass to Rust + + @_UNIFFI_CALLBACK_INTERFACE_NODE_EVENT_LISTENER_METHOD0 + def on_event( + uniffi_handle, + event, + uniffi_out_return, + uniffi_call_status_ptr, + ): + uniffi_obj = _UniffiConverterTypeNodeEventListener._handle_map.get(uniffi_handle) + def make_call(): + args = (_UniffiConverterTypeNodeEvent.lift(event), ) + method = uniffi_obj.on_event + return method(*args) + + + write_return_value = lambda v: None + _uniffi_trait_interface_call( + uniffi_call_status_ptr.contents, + make_call, + write_return_value, + ) + + @_UNIFFI_CALLBACK_INTERFACE_FREE + def _uniffi_free(uniffi_handle): + _UniffiConverterTypeNodeEventListener._handle_map.remove(uniffi_handle) + + # Generate the FFI VTable. This has a field for each callback interface method. + _uniffi_vtable = _UniffiVTableCallbackInterfaceNodeEventListener( + on_event, + _uniffi_free + ) + # Send Rust a pointer to the VTable. Note: this means we need to keep the struct alive forever, + # or else bad things will happen when Rust tries to access it. + _UniffiLib.uniffi_glsdk_fn_init_callback_vtable_nodeeventlistener(ctypes.byref(_uniffi_vtable)) + +# The _UniffiConverter which transforms the Callbacks in to Handles to pass to Rust. +_UniffiConverterTypeNodeEventListener = _UniffiCallbackInterfaceFfiConverter() + + + class _UniffiConverterOptionalUInt32(_UniffiConverterRustBuffer): @classmethod def check_lower(cls, value): @@ -5576,11 +5648,9 @@ class Node(): """ _pointer: ctypes.c_void_p - def __init__(self, credentials: "Credentials"): - _UniffiConverterTypeCredentials.check_lower(credentials) - - self._pointer = _uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_constructor_node_new, - _UniffiConverterTypeCredentials.lower(credentials)) + + def __init__(self, *args, **kwargs): + raise ValueError("This class has no default constructor") def __del__(self): # In case of partial initialization of instances. @@ -5972,6 +6042,246 @@ def read(cls, buf: _UniffiRustBuffer): @classmethod def write(cls, value: NodeProtocol, buf: _UniffiRustBuffer): buf.write_u64(cls.lower(value)) +class NodeBuilderProtocol(typing.Protocol): + """ + Configurable Node construction. See module docs. + """ + + def connect(self, credentials: "bytes",mnemonic: "typing.Optional[str]"): + """ + Connect to an existing node using saved credentials and return + a connected Node with any configured modifiers applied. + + If `mnemonic` is `Some(...)`, the SDK spawns a signer for the + connected Node. If `None`, the Node is signerless and signing + happens elsewhere (paired device, CLN node's local signer, + hardware signer). + """ + + raise NotImplementedError + def recover(self, mnemonic: "str"): + """ + Recover credentials for an existing node and return a + connected Node with any configured modifiers applied. + + `mnemonic` is required — recovery drives the signer to + authenticate. + """ + + raise NotImplementedError + def register(self, mnemonic: "str",invite_code: "typing.Optional[str]"): + """ + Register a new Greenlight node and return a connected Node + with the SDK signer running and any configured modifiers + applied. + + `mnemonic` is required — registration drives the signer to + sign the registration challenge, so the SDK must hold the + seed for this call. + """ + + raise NotImplementedError + def register_or_recover(self, mnemonic: "str",invite_code: "typing.Optional[str]"): + """ + Try to recover; if the node doesn't exist, register a new one. + + `mnemonic` is required — both recover and register drive the + signer. + """ + + raise NotImplementedError + def with_event_listener(self, listener: "NodeEventListener"): + """ + Install a node event listener. Events fire from the moment the + gRPC stream is established by the build call (`register` / + `recover` / `connect` / …), so attach the listener via the + builder rather than after the fact to capture events from the + very first moment. + + Returns the same builder for fluent chaining. + """ + + raise NotImplementedError +# NodeBuilder is a Rust-only trait - it's a wrapper around a Rust implementation. +class NodeBuilder(): + """ + Configurable Node construction. See module docs. + """ + + _pointer: ctypes.c_void_p + def __init__(self, config: "Config"): + """ + Create a builder for a Node with `config`. No I/O happens + until you call `connect` / `register` / `recover` / + `register_or_recover`. + """ + + _UniffiConverterTypeConfig.check_lower(config) + + self._pointer = _uniffi_rust_call(_UniffiLib.uniffi_glsdk_fn_constructor_nodebuilder_new, + _UniffiConverterTypeConfig.lower(config)) + + def __del__(self): + # In case of partial initialization of instances. + pointer = getattr(self, "_pointer", None) + if pointer is not None: + _uniffi_rust_call(_UniffiLib.uniffi_glsdk_fn_free_nodebuilder, pointer) + + def _uniffi_clone_pointer(self): + return _uniffi_rust_call(_UniffiLib.uniffi_glsdk_fn_clone_nodebuilder, self._pointer) + + # Used by alternative constructors or any methods which return this type. + @classmethod + def _make_instance_(cls, pointer): + # Lightly yucky way to bypass the usual __init__ logic + # and just create a new instance with the required pointer. + inst = cls.__new__(cls) + inst._pointer = pointer + return inst + + + def connect(self, credentials: "bytes",mnemonic: "typing.Optional[str]") -> "Node": + """ + Connect to an existing node using saved credentials and return + a connected Node with any configured modifiers applied. + + If `mnemonic` is `Some(...)`, the SDK spawns a signer for the + connected Node. If `None`, the Node is signerless and signing + happens elsewhere (paired device, CLN node's local signer, + hardware signer). + """ + + _UniffiConverterBytes.check_lower(credentials) + + _UniffiConverterOptionalString.check_lower(mnemonic) + + return _UniffiConverterTypeNode.lift( + _uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_connect,self._uniffi_clone_pointer(), + _UniffiConverterBytes.lower(credentials), + _UniffiConverterOptionalString.lower(mnemonic)) + ) + + + + + + def recover(self, mnemonic: "str") -> "Node": + """ + Recover credentials for an existing node and return a + connected Node with any configured modifiers applied. + + `mnemonic` is required — recovery drives the signer to + authenticate. + """ + + _UniffiConverterString.check_lower(mnemonic) + + return _UniffiConverterTypeNode.lift( + _uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_recover,self._uniffi_clone_pointer(), + _UniffiConverterString.lower(mnemonic)) + ) + + + + + + def register(self, mnemonic: "str",invite_code: "typing.Optional[str]") -> "Node": + """ + Register a new Greenlight node and return a connected Node + with the SDK signer running and any configured modifiers + applied. + + `mnemonic` is required — registration drives the signer to + sign the registration challenge, so the SDK must hold the + seed for this call. + """ + + _UniffiConverterString.check_lower(mnemonic) + + _UniffiConverterOptionalString.check_lower(invite_code) + + return _UniffiConverterTypeNode.lift( + _uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_register,self._uniffi_clone_pointer(), + _UniffiConverterString.lower(mnemonic), + _UniffiConverterOptionalString.lower(invite_code)) + ) + + + + + + def register_or_recover(self, mnemonic: "str",invite_code: "typing.Optional[str]") -> "Node": + """ + Try to recover; if the node doesn't exist, register a new one. + + `mnemonic` is required — both recover and register drive the + signer. + """ + + _UniffiConverterString.check_lower(mnemonic) + + _UniffiConverterOptionalString.check_lower(invite_code) + + return _UniffiConverterTypeNode.lift( + _uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_register_or_recover,self._uniffi_clone_pointer(), + _UniffiConverterString.lower(mnemonic), + _UniffiConverterOptionalString.lower(invite_code)) + ) + + + + + + def with_event_listener(self, listener: "NodeEventListener") -> "NodeBuilder": + """ + Install a node event listener. Events fire from the moment the + gRPC stream is established by the build call (`register` / + `recover` / `connect` / …), so attach the listener via the + builder rather than after the fact to capture events from the + very first moment. + + Returns the same builder for fluent chaining. + """ + + _UniffiConverterTypeNodeEventListener.check_lower(listener) + + return _UniffiConverterTypeNodeBuilder.lift( + _uniffi_rust_call(_UniffiLib.uniffi_glsdk_fn_method_nodebuilder_with_event_listener,self._uniffi_clone_pointer(), + _UniffiConverterTypeNodeEventListener.lower(listener)) + ) + + + + + + +class _UniffiConverterTypeNodeBuilder: + + @staticmethod + def lift(value: int): + return NodeBuilder._make_instance_(value) + + @staticmethod + def check_lower(value: NodeBuilder): + if not isinstance(value, NodeBuilder): + raise TypeError("Expected NodeBuilder instance, {} found".format(type(value).__name__)) + + @staticmethod + def lower(value: NodeBuilderProtocol): + if not isinstance(value, NodeBuilder): + raise TypeError("Expected NodeBuilder instance, {} found".format(type(value).__name__)) + return value._uniffi_clone_pointer() + + @classmethod + def read(cls, buf: _UniffiRustBuffer): + ptr = buf.read_u64() + if ptr == 0: + raise InternalError("Raw pointer value was null") + return cls.lift(ptr) + + @classmethod + def write(cls, value: NodeBuilderProtocol, buf: _UniffiRustBuffer): + buf.write_u64(cls.lower(value)) class NodeEventStreamProtocol(typing.Protocol): """ A stream of node events. Call `next()` to receive the next event. @@ -6302,23 +6612,6 @@ def write(cls, value: SignerProtocol, buf: _UniffiRustBuffer): # Async support -def connect(mnemonic: "str",credentials: "bytes",config: "Config") -> "Node": - """ - Connect to an existing Greenlight node using previously saved credentials. - """ - - _UniffiConverterString.check_lower(mnemonic) - - _UniffiConverterBytes.check_lower(credentials) - - _UniffiConverterTypeConfig.check_lower(config) - - return _UniffiConverterTypeNode.lift(_uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_func_connect, - _UniffiConverterString.lower(mnemonic), - _UniffiConverterBytes.lower(credentials), - _UniffiConverterTypeConfig.lower(config))) - - def parse_input(input: "str") -> "InputType": """ Parse a string and identify whether it's a BOLT11 invoice or a node ID. @@ -6333,60 +6626,6 @@ def parse_input(input: "str") -> "InputType": _UniffiConverterString.lower(input))) -def recover(mnemonic: "str",config: "Config") -> "Node": - """ - Recover credentials for an existing Greenlight node and return a connected Node. - - The app should call `node.credentials()` to get the credential bytes - and persist them for future `connect()` calls. - """ - - _UniffiConverterString.check_lower(mnemonic) - - _UniffiConverterTypeConfig.check_lower(config) - - return _UniffiConverterTypeNode.lift(_uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_func_recover, - _UniffiConverterString.lower(mnemonic), - _UniffiConverterTypeConfig.lower(config))) - - -def register(mnemonic: "str",invite_code: "typing.Optional[str]",config: "Config") -> "Node": - """ - Register a new Greenlight node and return a connected Node with signer running. - - The app should call `node.credentials()` to get the credential bytes - and persist them for future `connect()` calls. - """ - - _UniffiConverterString.check_lower(mnemonic) - - _UniffiConverterOptionalString.check_lower(invite_code) - - _UniffiConverterTypeConfig.check_lower(config) - - return _UniffiConverterTypeNode.lift(_uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_func_register, - _UniffiConverterString.lower(mnemonic), - _UniffiConverterOptionalString.lower(invite_code), - _UniffiConverterTypeConfig.lower(config))) - - -def register_or_recover(mnemonic: "str",invite_code: "typing.Optional[str]",config: "Config") -> "Node": - """ - Try to recover an existing node; if none exists, register a new one. - """ - - _UniffiConverterString.check_lower(mnemonic) - - _UniffiConverterOptionalString.check_lower(invite_code) - - _UniffiConverterTypeConfig.check_lower(config) - - return _UniffiConverterTypeNode.lift(_uniffi_rust_call_with_error(_UniffiConverterTypeError,_UniffiLib.uniffi_glsdk_fn_func_register_or_recover, - _UniffiConverterString.lower(mnemonic), - _UniffiConverterOptionalString.lower(invite_code), - _UniffiConverterTypeConfig.lower(config))) - - def set_log_level(level: "LogLevel") -> None: """ Change the log filter at runtime without reinstalling the listener. @@ -6456,11 +6695,7 @@ def set_logger(level: "LogLevel",listener: "LogListener") -> None: "PeerChannel", "ReceiveResponse", "SendResponse", - "connect", "parse_input", - "recover", - "register", - "register_or_recover", "set_log_level", "set_logger", "Config", @@ -6468,9 +6703,11 @@ def set_logger(level: "LogLevel",listener: "LogListener") -> None: "DeveloperCert", "Handle", "Node", + "NodeBuilder", "NodeEventStream", "Scheduler", "Signer", "LogListener", + "NodeEventListener", ] diff --git a/libs/gl-sdk/src/lib.rs b/libs/gl-sdk/src/lib.rs index 60d583d4b..4fbd70b1f 100644 --- a/libs/gl-sdk/src/lib.rs +++ b/libs/gl-sdk/src/lib.rs @@ -29,6 +29,7 @@ mod credentials; mod input; mod logging; mod node; +mod node_builder; mod scheduler; mod signer; mod util; @@ -40,12 +41,13 @@ pub use crate::{ ChannelState, FundChannel, FundOutput, GetInfoResponse, Invoice, InvoicePaidEvent, InvoiceStatus, ListFundsResponse, ListIndex, ListInvoicesResponse, ListPaymentsRequest, ListPeerChannelsResponse, ListPaysResponse, ListPeersResponse, - Node, NodeEvent, NodeEventStream, NodeState, OnchainReceiveResponse, OnchainSendResponse, - OutputStatus, Pay, PayStatus, Payment, PaymentStatus, PaymentType, PaymentTypeFilter, - Peer, PeerChannel, ReceiveResponse, SendResponse, + Node, NodeEvent, NodeEventListener, NodeEventStream, NodeState, OnchainReceiveResponse, + OnchainSendResponse, OutputStatus, Pay, PayStatus, Payment, PaymentStatus, PaymentType, + PaymentTypeFilter, Peer, PeerChannel, ReceiveResponse, SendResponse, }, input::{InputType, ParsedInvoice}, logging::{LogEntry, LogLevel, LogListener}, + node_builder::NodeBuilder, scheduler::Scheduler, signer::{Handle, Signer}, }; @@ -149,36 +151,9 @@ fn parse_mnemonic(mnemonic: &str) -> Result, Error> { Ok(phrase.to_seed_normalized("").to_vec()) } -/// Register a new Greenlight node and return a connected Node with signer running. -/// -/// The app should call `node.credentials()` to get the credential bytes -/// and persist them for future `connect()` calls. -#[uniffi::export] -pub fn register( - mnemonic: String, - invite_code: Option, - config: &config::Config, -) -> Result, Error> { - let seed = parse_mnemonic(&mnemonic)?; - schedule_node(seed, config, SchedulerAction::Register { invite_code }) -} - -/// Recover credentials for an existing Greenlight node and return a connected Node. -/// -/// The app should call `node.credentials()` to get the credential bytes -/// and persist them for future `connect()` calls. -#[uniffi::export] -pub fn recover( - mnemonic: String, - config: &config::Config, -) -> Result, Error> { - let seed = parse_mnemonic(&mnemonic)?; - schedule_node(seed, config, SchedulerAction::Recover) -} - -/// Connect to an existing Greenlight node using previously saved credentials. -#[uniffi::export] -pub fn connect( +/// Crate-internal: connect using saved credentials. The builder +/// (`NodeBuilder::connect`) is the public entry point. +pub(crate) fn connect_internal( mnemonic: String, credentials: Vec, config: &config::Config, @@ -198,20 +173,60 @@ pub fn connect( Ok(Arc::new(node)) } -/// Try to recover an existing node; if none exists, register a new one. -#[uniffi::export] -pub fn register_or_recover( +/// Crate-internal: register a fresh node. The builder +/// (`NodeBuilder::register`) is the public entry point. +pub(crate) fn register_internal( + mnemonic: String, + invite_code: Option, + config: &config::Config, +) -> Result, Error> { + let seed = parse_mnemonic(&mnemonic)?; + schedule_node(seed, config, SchedulerAction::Register { invite_code }) +} + +/// Crate-internal: recover an existing node. The builder +/// (`NodeBuilder::recover`) is the public entry point. +pub(crate) fn recover_internal( + mnemonic: String, + config: &config::Config, +) -> Result, Error> { + let seed = parse_mnemonic(&mnemonic)?; + schedule_node(seed, config, SchedulerAction::Recover) +} + +/// Crate-internal: register-or-recover. The builder +/// (`NodeBuilder::register_or_recover`) is the public entry point. +pub(crate) fn register_or_recover_internal( mnemonic: String, invite_code: Option, config: &config::Config, ) -> Result, Error> { - match recover(mnemonic.clone(), config) { + match recover_internal(mnemonic.clone(), config) { Ok(node) => Ok(node), - Err(Error::NoSuchNode(_)) => register(mnemonic, invite_code, config), + Err(Error::NoSuchNode(_)) => register_internal(mnemonic, invite_code, config), Err(e) => Err(e), } } +/// Crate-internal: connect signerless — credentials only, no +/// SDK-side signer spawned. Used by `NodeBuilder::connect` when no +/// mnemonic is set. +/// +/// Signing-required RPCs (`pay`, `receive` JIT-channel, etc.) rely +/// on a signer running elsewhere — typically the CLN node's local +/// signer or a paired device. This is the supported model for +/// signerless clients (browser extensions, paired devices, hardware +/// signers held outside the SDK process). +pub(crate) fn connect_signerless_internal( + credentials: Vec, + _config: &config::Config, +) -> Result, Error> { + use std::sync::Arc; + let creds = credentials::Credentials::load(credentials)?; + let node = node::Node::signerless(creds)?; + Ok(Arc::new(node)) +} + /// Parse a string and identify whether it's a BOLT11 invoice or a node ID. /// /// Strips `lightning:` / `LIGHTNING:` prefixes automatically. diff --git a/libs/gl-sdk/src/node.rs b/libs/gl-sdk/src/node.rs index 8ae0b8984..2d9bc4497 100644 --- a/libs/gl-sdk/src/node.rs +++ b/libs/gl-sdk/src/node.rs @@ -18,12 +18,33 @@ pub struct Node { stored_credentials: Option, signer_handle: Option, disconnected: AtomicBool, + /// Background task that tails the gRPC event stream and dispatches + /// events to the installed listener. A single listener per node; + /// installing a new one aborts the previous task. Aborted on Drop. + event_task: Mutex>>, +} + +impl Drop for Node { + fn drop(&mut self) { + if let Ok(mut guard) = self.event_task.lock() { + if let Some(handle) = guard.take() { + handle.abort(); + } + } + } } -#[uniffi::export] impl Node { - #[uniffi::constructor()] - pub fn new(credentials: &Credentials) -> Result { + /// Construct a signerless Node — credentials only, no SDK-side + /// signer running. The actual signing happens elsewhere (a paired + /// device, a hardware signer, the CLN node's local signer). + /// Operations that require signing fall through to the node side. + /// + /// **Not a UniFFI export.** UniFFI consumers reach this via + /// `NodeBuilder::connect(credentials, None)` (mnemonic omitted). + /// Sibling Rust crates (e.g. `gl-sdk-napi`) call this directly + /// when wrapping signerless flows into their own bindings. + pub fn signerless(credentials: Credentials) -> Result { let node_id = credentials .inner .node_id() @@ -37,11 +58,16 @@ impl Node { inner, cln_client, gl_client, - stored_credentials: Some(credentials.clone()), + stored_credentials: Some(credentials), signer_handle: None, disconnected: AtomicBool::new(false), + event_task: Mutex::new(None), }) } +} + +#[uniffi::export] +impl Node { /// Stop the node if it is currently running. pub fn stop(&self) -> Result<(), Error> { @@ -652,6 +678,54 @@ fn build_diagnostic_json( // Not exported through uniffi impl Node { + /// Install a listener that receives real-time node events. + /// + /// Spawns a background task that tails the gRPC event stream and + /// invokes `listener.on_event(...)` for every event. Each `Node` + /// holds at most one listener — calling again replaces it. The task + /// stops when the stream ends, errors, or the `Node` is dropped. + /// + /// Crate-private — installed via `NodeBuilder::with_event_listener` + /// at construction time so events emitted during node bring-up + /// aren't missed. + pub(crate) fn set_event_listener( + &self, + listener: std::sync::Arc, + ) -> Result<(), Error> { + self.check_connected()?; + let mut gl_client = exec(self.get_gl_client())?.clone(); + let req = glpb::NodeEventsRequest {}; + let stream = exec(gl_client.stream_node_events(req)) + .map_err(|e| Error::Rpc(e.to_string()))? + .into_inner(); + + let mut guard = self + .event_task + .lock() + .map_err(|e| Error::Other(e.to_string()))?; + if let Some(prev) = guard.take() { + prev.abort(); + } + + let task = crate::util::get_runtime().spawn(async move { + let mut stream = stream; + loop { + match stream.message().await { + Ok(Some(raw)) => { + if let Some(event) = node_event_from_pb(raw) { + listener.on_event(event); + } + } + Ok(None) => break, + Err(e) if e.code() == tonic::Code::Unknown => break, + Err(_) => break, + } + } + }); + *guard = Some(task); + Ok(()) + } + fn check_connected(&self) -> Result<(), Error> { if self.disconnected.load(Ordering::Relaxed) { return Err(Error::Other("Node is disconnected".to_string())); @@ -681,6 +755,7 @@ impl Node { stored_credentials: Some(credentials), signer_handle: Some(handle), disconnected: AtomicBool::new(false), + event_task: Mutex::new(None), }) } @@ -1568,6 +1643,21 @@ pub struct NodeState { // NodeEvent streaming types // ============================================================ +/// Callback interface for receiving node events. +/// +/// `on_event` is invoked from the SDK's internal event-dispatch task. +/// Implementations should be cheap and non-blocking; to update UI, +/// dispatch to the main thread from inside the handler. +/// +/// Installed via `NodeBuilder::with_event_listener(...)` so events +/// emitted during node bring-up are captured. The polling-style +/// `Node::stream_node_events()` API is still available for callers +/// that prefer to drive events themselves. +#[uniffi::export(callback_interface)] +pub trait NodeEventListener: Send + Sync { + fn on_event(&self, event: NodeEvent); +} + /// A stream of node events. Call `next()` to receive the next event. /// /// The stream is backed by a gRPC streaming connection to the node. @@ -1588,11 +1678,22 @@ impl NodeEventStream { /// the connection is lost. pub fn next(&self) -> Result, Error> { let mut stream = self.inner.lock().map_err(|e| Error::Other(e.to_string()))?; - match exec(stream.message()) { - Ok(Some(event)) => Ok(Some(event.into())), - Ok(None) => Ok(None), - Err(e) if e.code() == tonic::Code::Unknown => Ok(None), - Err(e) => Err(Error::Rpc(e.to_string())), + // Loop over wire events, skipping any the SDK doesn't recognise, + // until we either decode a known event, the stream ends, or it + // errors. The public `NodeEvent` enum is a closed set — + // unknown server-side events are silently dropped here. + loop { + match exec(stream.message()) { + Ok(Some(raw)) => { + if let Some(event) = node_event_from_pb(raw) { + return Ok(Some(event)); + } + // Unknown event — fall through to next iteration. + } + Ok(None) => return Ok(None), + Err(e) if e.code() == tonic::Code::Unknown => return Ok(None), + Err(e) => return Err(Error::Rpc(e.to_string())), + } } } } @@ -1602,9 +1703,6 @@ impl NodeEventStream { pub enum NodeEvent { /// An invoice was paid. InvoicePaid { details: InvoicePaidEvent }, - /// An unknown event type was received. This can happen if the - /// server sends a new event type that this client doesn't know about. - Unknown, } /// Details of a paid invoice. @@ -1622,20 +1720,24 @@ pub struct InvoicePaidEvent { pub amount_msat: u64, } -impl From for NodeEvent { - fn from(other: glpb::NodeEvent) -> Self { - match other.event { - Some(glpb::node_event::Event::InvoicePaid(paid)) => NodeEvent::InvoicePaid { - details: InvoicePaidEvent { - payment_hash: hex::encode(&paid.payment_hash), - bolt11: paid.bolt11, - preimage: hex::encode(&paid.preimage), - label: paid.label, - amount_msat: paid.amount_msat, - }, +/// Convert a wire-level `glpb::NodeEvent` into the typed SDK enum. +/// +/// Returns `None` for events the SDK doesn't recognise (e.g. a future +/// server-side event type added after the client was built). Callers +/// silently skip `None` so unknown events never reach the foreign +/// bindings — the public `NodeEvent` is a closed set. +fn node_event_from_pb(other: glpb::NodeEvent) -> Option { + match other.event { + Some(glpb::node_event::Event::InvoicePaid(paid)) => Some(NodeEvent::InvoicePaid { + details: InvoicePaidEvent { + payment_hash: hex::encode(&paid.payment_hash), + bolt11: paid.bolt11, + preimage: hex::encode(&paid.preimage), + label: paid.label, + amount_msat: paid.amount_msat, }, - None => NodeEvent::Unknown, - } + }), + None => None, } } diff --git a/libs/gl-sdk/src/node_builder.rs b/libs/gl-sdk/src/node_builder.rs new file mode 100644 index 000000000..c1b0d036d --- /dev/null +++ b/libs/gl-sdk/src/node_builder.rs @@ -0,0 +1,162 @@ +// Builder-style Node creation. +// +// `NodeBuilder` is the sole public entry point for Node construction +// across all foreign bindings. The shape is "request the action, +// optionally with modifiers": +// +// // Signerless connect — caller does not have the mnemonic in +// // this process. The SDK runs no signer; signing happens +// // elsewhere (paired device, CLN node's local signer, hardware +// // signer). This is the supported model for keyless clients. +// let node = NodeBuilder::new(&config).connect(credentials, None)?; +// +// // Signed connect — caller hands the mnemonic per build call, +// // SDK spawns a signer. +// let node = NodeBuilder::new(&config).connect(credentials, Some(mnemonic))?; +// +// // Register / recover require a mnemonic by definition (the +// // signer must sign the registration/recovery challenge). +// let node = NodeBuilder::new(&config) +// .with_event_listener(listener) +// .register(mnemonic, invite_code)?; +// +// The mnemonic is the security-sensitive input: hand it in only when +// you actually want the SDK to act as a signer for that call. +// +// Adding a new modifier later is a new `with_*` setter — additive, +// never breaks existing callers. + +use std::sync::Arc; + +use crate::{ + config::Config, + node::{Node, NodeEventListener}, + Error, +}; + +/// Configurable Node construction. See module docs. +/// +/// All fields are immutable after construction. Each `with_*` setter +/// returns a fresh `Arc` that shares ownership of any +/// previously-installed modifiers via `Arc`. No interior +/// mutability, no locks — the builder is a value, not a state +/// machine. +#[derive(uniffi::Object)] +pub struct NodeBuilder { + config: Arc, + event_listener: Option>, +} + +#[uniffi::export] +impl NodeBuilder { + /// Create a builder for a Node with `config`. No I/O happens + /// until you call `connect` / `register` / `recover` / + /// `register_or_recover`. + #[uniffi::constructor] + pub fn new(config: &Config) -> Arc { + Arc::new(Self { + config: Arc::new(config.clone()), + event_listener: None, + }) + } + + /// Install a node event listener. Events fire from the moment the + /// gRPC stream is established by the build call (`register` / + /// `recover` / `connect` / …), so attach the listener via the + /// builder rather than after the fact to capture events from the + /// very first moment. + /// + /// Returns a new builder that shares the rest of the + /// configuration. Build calls on the returned builder will + /// install the listener; the original builder is unchanged. + pub fn with_event_listener( + self: Arc, + listener: Box, + ) -> Arc { + // UniFFI's callback-interface lowering hands us a + // `Box`. We re-wrap it as `Arc` because + // the builder is reusable across multiple build calls — each + // build clones the Arc into the resulting Node, and `Box` + // can't be cloned. This is a one-time cost paid per setter + // call. + Arc::new(Self { + config: Arc::clone(&self.config), + event_listener: Some(Arc::from(listener)), + }) + } + + /// Register a new Greenlight node and return a connected Node + /// with the SDK signer running and any configured modifiers + /// applied. + /// + /// `mnemonic` is required — registration drives the signer to + /// sign the registration challenge, so the SDK must hold the + /// seed for this call. + pub fn register( + &self, + mnemonic: String, + invite_code: Option, + ) -> Result, Error> { + let node = crate::register_internal(mnemonic, invite_code, &self.config)?; + self.attach_observers(&node)?; + Ok(node) + } + + /// Recover credentials for an existing node and return a + /// connected Node with any configured modifiers applied. + /// + /// `mnemonic` is required — recovery drives the signer to + /// authenticate. + pub fn recover(&self, mnemonic: String) -> Result, Error> { + let node = crate::recover_internal(mnemonic, &self.config)?; + self.attach_observers(&node)?; + Ok(node) + } + + /// Connect to an existing node using saved credentials and return + /// a connected Node with any configured modifiers applied. + /// + /// If `mnemonic` is `Some(...)`, the SDK spawns a signer for the + /// connected Node. If `None`, the Node is signerless and signing + /// happens elsewhere (paired device, CLN node's local signer, + /// hardware signer). + pub fn connect( + &self, + credentials: Vec, + mnemonic: Option, + ) -> Result, Error> { + let node = match mnemonic { + Some(mnemonic) => crate::connect_internal(mnemonic, credentials, &self.config)?, + None => crate::connect_signerless_internal(credentials, &self.config)?, + }; + self.attach_observers(&node)?; + Ok(node) + } + + /// Try to recover; if the node doesn't exist, register a new one. + /// + /// `mnemonic` is required — both recover and register drive the + /// signer. + pub fn register_or_recover( + &self, + mnemonic: String, + invite_code: Option, + ) -> Result, Error> { + let node = + crate::register_or_recover_internal(mnemonic, invite_code, &self.config)?; + self.attach_observers(&node)?; + Ok(node) + } +} + +impl NodeBuilder { + /// Attach all configured modifiers to a freshly-built Node. + /// Modifiers are shared (not consumed) — the same builder can + /// drive multiple builds and they all get the same listener. + fn attach_observers(&self, node: &Arc) -> Result<(), Error> { + if let Some(listener) = self.event_listener.as_ref() { + node.set_event_listener(Arc::clone(listener))?; + } + Ok(()) + } +} diff --git a/libs/gl-sdk/tests/test_auth_api.py b/libs/gl-sdk/tests/test_auth_api.py index 2a0b8e71f..9c75f6d6e 100644 --- a/libs/gl-sdk/tests/test_auth_api.py +++ b/libs/gl-sdk/tests/test_auth_api.py @@ -50,7 +50,7 @@ class TestRegister: def test_register_returns_node(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(MNEMONIC, None, config) + node = glsdk.NodeBuilder(config).register(MNEMONIC, None) assert node is not None assert isinstance(node, glsdk.Node) node.disconnect() @@ -58,7 +58,7 @@ def test_register_returns_node(self, scheduler, nobody_id): def test_register_credentials_roundtrip(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(MNEMONIC, None, config) + node = glsdk.NodeBuilder(config).register(MNEMONIC, None) creds = node.credentials() assert isinstance(creds, bytes) assert len(creds) > 0 @@ -68,7 +68,7 @@ def test_register_bad_mnemonic(self, scheduler, nobody_id): dev_cert = glsdk.DeveloperCert(nobody_id.cert_chain, nobody_id.private_key) config = glsdk.Config().with_developer_cert(dev_cert) with pytest.raises(glsdk.Error.PhraseCorrupted): - glsdk.register("not a valid mnemonic", None, config) + glsdk.NodeBuilder(config).register("not a valid mnemonic", None) class TestRecover: @@ -79,11 +79,11 @@ def test_recover_after_register(self, scheduler, nobody_id): config = glsdk.Config().with_developer_cert(dev_cert) # Register first - node1 = glsdk.register(MNEMONIC, None, config) + node1 = glsdk.NodeBuilder(config).register(MNEMONIC, None) node1.disconnect() # Recover with same mnemonic - node2 = glsdk.recover(MNEMONIC, config) + node2 = glsdk.NodeBuilder(config).recover(MNEMONIC) assert node2 is not None assert isinstance(node2, glsdk.Node) creds = node2.credentials() @@ -94,7 +94,7 @@ def test_recover_nonexistent_node(self, scheduler, nobody_id): dev_cert = glsdk.DeveloperCert(nobody_id.cert_chain, nobody_id.private_key) config = glsdk.Config().with_developer_cert(dev_cert) with pytest.raises(glsdk.Error.NoSuchNode): - glsdk.recover(MNEMONIC, config) + glsdk.NodeBuilder(config).recover(MNEMONIC) class TestConnect: @@ -105,12 +105,12 @@ def test_connect_with_saved_credentials(self, scheduler, nobody_id): config = glsdk.Config().with_developer_cert(dev_cert) # Register and save credentials - node1 = glsdk.register(MNEMONIC, None, config) + node1 = glsdk.NodeBuilder(config).register(MNEMONIC, None) saved_creds = node1.credentials() node1.disconnect() # Connect with saved credentials - node2 = glsdk.connect(MNEMONIC, saved_creds, config) + node2 = glsdk.NodeBuilder(config).connect(saved_creds, MNEMONIC) assert node2 is not None assert isinstance(node2, glsdk.Node) node2.disconnect() @@ -119,7 +119,7 @@ def test_connect_bad_mnemonic(self, scheduler, nobody_id): dev_cert = glsdk.DeveloperCert(nobody_id.cert_chain, nobody_id.private_key) config = glsdk.Config().with_developer_cert(dev_cert) with pytest.raises(glsdk.Error.PhraseCorrupted): - glsdk.connect("bad mnemonic", b"some-creds", config) + glsdk.NodeBuilder(config).connect(b"some-creds", "bad mnemonic") class TestRegisterOrRecover: @@ -128,7 +128,7 @@ class TestRegisterOrRecover: def test_registers_when_no_node_exists(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) + node = glsdk.NodeBuilder(config).register_or_recover(MNEMONIC, None) assert node is not None assert isinstance(node, glsdk.Node) node.disconnect() @@ -138,11 +138,11 @@ def test_recovers_when_node_exists(self, scheduler, nobody_id): config = glsdk.Config().with_developer_cert(dev_cert) # Register first - node1 = glsdk.register(MNEMONIC, None, config) + node1 = glsdk.NodeBuilder(config).register(MNEMONIC, None) node1.disconnect() # register_or_recover should recover - node2 = glsdk.register_or_recover(MNEMONIC, None, config) + node2 = glsdk.NodeBuilder(config).register_or_recover(MNEMONIC, None) assert node2 is not None assert isinstance(node2, glsdk.Node) node2.disconnect() @@ -154,14 +154,14 @@ class TestDisconnect: def test_disconnect_stops_signer(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(MNEMONIC, None, config) + node = glsdk.NodeBuilder(config).register(MNEMONIC, None) # Should not raise node.disconnect() def test_disconnect_idempotent(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(MNEMONIC, None, config) + node = glsdk.NodeBuilder(config).register(MNEMONIC, None) node.disconnect() # Second disconnect should not raise node.disconnect() @@ -175,12 +175,12 @@ def test_duplicate_register(self, scheduler, nobody_id): config = glsdk.Config().with_developer_cert(dev_cert) # Register once - node1 = glsdk.register(MNEMONIC, None, config) + node1 = glsdk.NodeBuilder(config).register(MNEMONIC, None) node1.disconnect() # Register again with same mnemonic should fail with pytest.raises(glsdk.Error.DuplicateNode): - glsdk.register(MNEMONIC, None, config) + glsdk.NodeBuilder(config).register(MNEMONIC, None) class TestConnectBadCredentials: @@ -192,7 +192,7 @@ def test_connect_empty_credentials(self, scheduler, nobody_id): # Empty credentials should fail (Credentials.load returns nobody creds, # but the signer won't be able to authenticate with them) with pytest.raises(glsdk.Error): - glsdk.connect(MNEMONIC, b"", config) + glsdk.NodeBuilder(config).connect(b"", MNEMONIC) class TestMultipleNodes: @@ -208,8 +208,8 @@ def test_two_nodes_independent(self, scheduler, nobody_id): "zoo zoo zoo zoo zoo wrong" ) - node1 = glsdk.register(MNEMONIC, None, config) - node2 = glsdk.register(mnemonic_2, None, config) + node1 = glsdk.NodeBuilder(config).register(MNEMONIC, None) + node2 = glsdk.NodeBuilder(config).register(mnemonic_2, None) assert node1 is not None assert node2 is not None @@ -232,7 +232,7 @@ def test_credentials_still_works_after_disconnect(self, scheduler, nobody_id): """credentials() should work even after disconnect since it's local data.""" dev_cert = glsdk.DeveloperCert(nobody_id.cert_chain, nobody_id.private_key) config = glsdk.Config().with_developer_cert(dev_cert) - node = glsdk.register(MNEMONIC, None, config) + node = glsdk.NodeBuilder(config).register(MNEMONIC, None) node.disconnect() # credentials() is local, should still work creds = node.credentials() @@ -248,7 +248,7 @@ def test_node_new_stores_credentials(self, scheduler, nobody_id): config = glsdk.Config().with_developer_cert(dev_cert) # Register to get valid credentials - node1 = glsdk.register(MNEMONIC, None, config) + node1 = glsdk.NodeBuilder(config).register(MNEMONIC, None) saved_creds = node1.credentials() node1.disconnect() diff --git a/libs/gl-sdk/tests/test_list_payments.py b/libs/gl-sdk/tests/test_list_payments.py index 7f153ec87..4465416fd 100644 --- a/libs/gl-sdk/tests/test_list_payments.py +++ b/libs/gl-sdk/tests/test_list_payments.py @@ -73,7 +73,7 @@ def test_node_has_list_invoices_method(self): def test_list_invoices_empty(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) + node = glsdk.NodeBuilder(config).register_or_recover(MNEMONIC, None) result = node.list_invoices( label=None, invstring=None, payment_hash=None, offer_id=None, index=None, start=None, limit=None, @@ -120,7 +120,7 @@ def test_node_has_list_pays_method(self): def test_list_pays_empty(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) + node = glsdk.NodeBuilder(config).register_or_recover(MNEMONIC, None) result = node.list_pays( bolt11=None, payment_hash=None, status=None, index=None, start=None, limit=None, @@ -242,7 +242,7 @@ def test_node_has_list_payments_method(self): def test_list_payments_empty(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) + node = glsdk.NodeBuilder(config).register_or_recover(MNEMONIC, None) req = glsdk.ListPaymentsRequest( filters=None, from_timestamp=None, to_timestamp=None, include_failures=None, offset=None, limit=None, @@ -259,7 +259,7 @@ class TestListInvoicesIntegration: def test_created_invoice_appears_in_list_invoices(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) + node = glsdk.NodeBuilder(config).register_or_recover(MNEMONIC, None) label = str(uuid.uuid4()) node.receive(label=label, description="coffee", amount_msat=10_000_000) @@ -283,7 +283,7 @@ def test_unpaid_invoices_excluded(self, scheduler, nobody_id): inspection.""" 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) + node = glsdk.NodeBuilder(config).register_or_recover(MNEMONIC, None) label = str(uuid.uuid4()) node.receive(label=label, description="tea", amount_msat=5_000_000) @@ -306,7 +306,7 @@ def test_unpaid_invoices_excluded(self, scheduler, nobody_id): def test_type_filter_received_only(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) + node = glsdk.NodeBuilder(config).register_or_recover(MNEMONIC, None) label = str(uuid.uuid4()) node.receive(label=label, description="tea", amount_msat=5_000_000) diff --git a/libs/gl-sdk/tests/test_node_methods.py b/libs/gl-sdk/tests/test_node_methods.py index 411ca476c..4890ca651 100644 --- a/libs/gl-sdk/tests/test_node_methods.py +++ b/libs/gl-sdk/tests/test_node_methods.py @@ -306,7 +306,7 @@ def test_node_has_node_state_method(self): 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) + node = glsdk.NodeBuilder(config).register_or_recover(MNEMONIC, None) state = node.node_state() assert isinstance(state, glsdk.NodeState) # Node id is a lowercase hex pubkey (33 bytes → 66 chars). @@ -334,7 +334,7 @@ def test_generate_diagnostic_data_returns_well_formed_envelope( ): 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) + node = glsdk.NodeBuilder(config).register_or_recover(MNEMONIC, None) blob = node.generate_diagnostic_data() assert isinstance(blob, str)