From c8babbfd5d68f63ae09cde7c9b9a972d780d86d0 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Thu, 19 Mar 2026 13:28:03 +0100 Subject: [PATCH 1/5] misc: Add .mypy_cache to .gitignore, clean up trailing blank lines in node.rs --- .gitignore | 1 + libs/gl-sdk/src/node.rs | 10 ---------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 6d5487b6..6fc724c0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ site target *.egg-info *.dylib +.mypy_cache # These files are auto generated and may contain sensible data that # we do not want to check-in! diff --git a/libs/gl-sdk/src/node.rs b/libs/gl-sdk/src/node.rs index a289edd0..545e52d8 100644 --- a/libs/gl-sdk/src/node.rs +++ b/libs/gl-sdk/src/node.rs @@ -406,8 +406,6 @@ impl From for GetInfoResponse { } } - - // ============================================================ // ListPeers response types // ============================================================ @@ -450,8 +448,6 @@ impl From for Peer { } } - - // ============================================================ // ListPeerChannels response types // ============================================================ @@ -543,8 +539,6 @@ impl From for PeerChannel { } } - - // ============================================================ // ListFunds response types // ============================================================ @@ -641,8 +635,6 @@ impl From for FundChannel { } } - - // ============================================================ // NodeEvent streaming types // ============================================================ @@ -717,5 +709,3 @@ impl From for NodeEvent { } } } - - From c1b29d76b1f95840e2563b7f927d3355917d6d8e Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Thu, 19 Mar 2026 13:28:11 +0100 Subject: [PATCH 2/5] gl-sdk: Add DeveloperCert type and with_developer_cert() builder on Scheduler Allow SDK consumers to provide their Greenlight Developer Console certificate at runtime via a builder pattern on Scheduler. When set, register() and recover() use the provided certificate; otherwise they fall back to the compiled-in default (Nobody::new()). This is needed because the UniFFI SDK (used by Kotlin, Swift, Python, Ruby) previously hardcoded Nobody::new() with no way to override it. --- libs/gl-sdk/glsdk/glsdk.py | 155 +++++++++++++++++++++++++++++++++ libs/gl-sdk/src/credentials.rs | 25 ++++++ libs/gl-sdk/src/lib.rs | 4 +- libs/gl-sdk/src/scheduler.rs | 59 +++++++++---- 4 files changed, 224 insertions(+), 19 deletions(-) diff --git a/libs/gl-sdk/glsdk/glsdk.py b/libs/gl-sdk/glsdk/glsdk.py index a2ea7cb2..9441192a 100644 --- a/libs/gl-sdk/glsdk/glsdk.py +++ b/libs/gl-sdk/glsdk/glsdk.py @@ -490,6 +490,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_scheduler_register() != 20821: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_glsdk_checksum_method_scheduler_with_developer_cert() != 64139: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_signer_authenticate() != 55935: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_method_signer_node_id() != 43931: @@ -498,6 +500,8 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_constructor_credentials_load() != 25306: 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: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_glsdk_checksum_constructor_scheduler_new() != 15239: @@ -630,6 +634,22 @@ class _UniffiForeignFutureStructVoid(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_glsdk_fn_method_credentials_save.restype = _UniffiRustBuffer +_UniffiLib.uniffi_glsdk_fn_clone_developercert.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_clone_developercert.restype = ctypes.c_void_p +_UniffiLib.uniffi_glsdk_fn_free_developercert.argtypes = ( + ctypes.c_void_p, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_free_developercert.restype = None +_UniffiLib.uniffi_glsdk_fn_constructor_developercert_new.argtypes = ( + _UniffiRustBuffer, + _UniffiRustBuffer, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_constructor_developercert_new.restype = ctypes.c_void_p _UniffiLib.uniffi_glsdk_fn_clone_handle.argtypes = ( ctypes.c_void_p, ctypes.POINTER(_UniffiRustCallStatus), @@ -760,6 +780,12 @@ class _UniffiForeignFutureStructVoid(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_glsdk_fn_method_scheduler_register.restype = ctypes.c_void_p +_UniffiLib.uniffi_glsdk_fn_method_scheduler_with_developer_cert.argtypes = ( + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_glsdk_fn_method_scheduler_with_developer_cert.restype = ctypes.c_void_p _UniffiLib.uniffi_glsdk_fn_clone_signer.argtypes = ( ctypes.c_void_p, ctypes.POINTER(_UniffiRustCallStatus), @@ -1104,6 +1130,9 @@ class _UniffiForeignFutureStructVoid(ctypes.Structure): _UniffiLib.uniffi_glsdk_checksum_method_scheduler_register.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_method_scheduler_register.restype = ctypes.c_uint16 +_UniffiLib.uniffi_glsdk_checksum_method_scheduler_with_developer_cert.argtypes = ( +) +_UniffiLib.uniffi_glsdk_checksum_method_scheduler_with_developer_cert.restype = ctypes.c_uint16 _UniffiLib.uniffi_glsdk_checksum_method_signer_authenticate.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_method_signer_authenticate.restype = ctypes.c_uint16 @@ -1116,6 +1145,9 @@ class _UniffiForeignFutureStructVoid(ctypes.Structure): _UniffiLib.uniffi_glsdk_checksum_constructor_credentials_load.argtypes = ( ) _UniffiLib.uniffi_glsdk_checksum_constructor_credentials_load.restype = ctypes.c_uint16 +_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_node_new.restype = ctypes.c_uint16 @@ -1247,6 +1279,8 @@ def write(value, buf): + + class FundChannel: peer_id: "bytes" our_amount_msat: "int" @@ -2926,6 +2960,94 @@ def read(cls, buf: _UniffiRustBuffer): @classmethod def write(cls, value: CredentialsProtocol, buf: _UniffiRustBuffer): buf.write_u64(cls.lower(value)) +class DeveloperCertProtocol(typing.Protocol): + """ + A developer certificate obtained from the Greenlight Developer + Console (GDC). When provided to a `Scheduler` via + `with_developer_cert()`, nodes registered through that scheduler + will be associated with the developer's account. + + If no developer certificate is provided, the scheduler falls back + to the compiled-in default certificate, which may be sufficient + when using an invite code instead. + """ + + pass +# DeveloperCert is a Rust-only trait - it's a wrapper around a Rust implementation. +class DeveloperCert(): + """ + A developer certificate obtained from the Greenlight Developer + Console (GDC). When provided to a `Scheduler` via + `with_developer_cert()`, nodes registered through that scheduler + will be associated with the developer's account. + + If no developer certificate is provided, the scheduler falls back + to the compiled-in default certificate, which may be sufficient + when using an invite code instead. + """ + + _pointer: ctypes.c_void_p + def __init__(self, cert: "bytes",key: "bytes"): + """ + Create a new `DeveloperCert` from the certificate and private + key PEM bytes obtained from the Greenlight Developer Console. + """ + + _UniffiConverterBytes.check_lower(cert) + + _UniffiConverterBytes.check_lower(key) + + self._pointer = _uniffi_rust_call(_UniffiLib.uniffi_glsdk_fn_constructor_developercert_new, + _UniffiConverterBytes.lower(cert), + _UniffiConverterBytes.lower(key)) + + 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_developercert, pointer) + + def _uniffi_clone_pointer(self): + return _uniffi_rust_call(_UniffiLib.uniffi_glsdk_fn_clone_developercert, 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 + + + +class _UniffiConverterTypeDeveloperCert: + + @staticmethod + def lift(value: int): + return DeveloperCert._make_instance_(value) + + @staticmethod + def check_lower(value: DeveloperCert): + if not isinstance(value, DeveloperCert): + raise TypeError("Expected DeveloperCert instance, {} found".format(type(value).__name__)) + + @staticmethod + def lower(value: DeveloperCertProtocol): + if not isinstance(value, DeveloperCert): + raise TypeError("Expected DeveloperCert 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: DeveloperCertProtocol, buf: _UniffiRustBuffer): + buf.write_u64(cls.lower(value)) class HandleProtocol(typing.Protocol): """ A handle to interact with a signer loop running and processing @@ -3418,6 +3540,17 @@ class SchedulerProtocol(typing.Protocol): def recover(self, signer: "Signer"): raise NotImplementedError def register(self, signer: "Signer",code: "typing.Optional[str]"): + raise NotImplementedError + def with_developer_cert(self, cert: "DeveloperCert"): + """ + Configure a developer certificate obtained from the Greenlight + Developer Console. Nodes registered through this scheduler + will be associated with the developer's account. + + Returns a new `Scheduler` instance with the developer + certificate configured. + """ + raise NotImplementedError # Scheduler is a Rust-only trait - it's a wrapper around a Rust implementation. class Scheduler(): @@ -3479,6 +3612,27 @@ def register(self, signer: "Signer",code: "typing.Optional[str]") -> "Credential + def with_developer_cert(self, cert: "DeveloperCert") -> "Scheduler": + """ + Configure a developer certificate obtained from the Greenlight + Developer Console. Nodes registered through this scheduler + will be associated with the developer's account. + + Returns a new `Scheduler` instance with the developer + certificate configured. + """ + + _UniffiConverterTypeDeveloperCert.check_lower(cert) + + return _UniffiConverterTypeScheduler.lift( + _uniffi_rust_call(_UniffiLib.uniffi_glsdk_fn_method_scheduler_with_developer_cert,self._uniffi_clone_pointer(), + _UniffiConverterTypeDeveloperCert.lower(cert)) + ) + + + + + class _UniffiConverterTypeScheduler: @@ -3625,6 +3779,7 @@ def write(cls, value: SignerProtocol, buf: _UniffiRustBuffer): "ReceiveResponse", "SendResponse", "Credentials", + "DeveloperCert", "Handle", "Node", "NodeEventStream", diff --git a/libs/gl-sdk/src/credentials.rs b/libs/gl-sdk/src/credentials.rs index edce6762..647b3c9c 100644 --- a/libs/gl-sdk/src/credentials.rs +++ b/libs/gl-sdk/src/credentials.rs @@ -1,6 +1,31 @@ use crate::Error; use gl_client::credentials::Device as DeviceCredentials; +/// A developer certificate obtained from the Greenlight Developer +/// Console (GDC). When provided to a `Scheduler` via +/// `with_developer_cert()`, nodes registered through that scheduler +/// will be associated with the developer's account. +/// +/// If no developer certificate is provided, the scheduler falls back +/// to the compiled-in default certificate, which may be sufficient +/// when using an invite code instead. +#[derive(uniffi::Object, Clone)] +pub struct DeveloperCert { + pub(crate) inner: gl_client::credentials::Nobody, +} + +#[uniffi::export] +impl DeveloperCert { + /// Create a new `DeveloperCert` from the certificate and private + /// key PEM bytes obtained from the Greenlight Developer Console. + #[uniffi::constructor()] + pub fn new(cert: Vec, key: Vec) -> Self { + Self { + inner: gl_client::credentials::Nobody::with(cert, key), + } + } +} + /// `Credentials` is a container for `node_id`, the mTLS client /// certificate used to authenticate a client against a node, as well /// as the seed secret if present. If no seed is present in the diff --git a/libs/gl-sdk/src/lib.rs b/libs/gl-sdk/src/lib.rs index 8913103d..69da4766 100644 --- a/libs/gl-sdk/src/lib.rs +++ b/libs/gl-sdk/src/lib.rs @@ -19,7 +19,7 @@ pub enum Error { #[error("Invalid argument: {0}={1}")] Argument(String, String), - + #[error("Generic error: {0}")] Other(String), } @@ -30,7 +30,7 @@ mod signer; mod util; pub use crate::{ - credentials::Credentials, + credentials::{Credentials, DeveloperCert}, node::{ ChannelState, FundChannel, FundOutput, GetInfoResponse, InvoicePaidEvent, ListFundsResponse, ListPeerChannelsResponse, ListPeersResponse, Node, NodeEvent, diff --git a/libs/gl-sdk/src/scheduler.rs b/libs/gl-sdk/src/scheduler.rs index 172c406d..5d6982d2 100644 --- a/libs/gl-sdk/src/scheduler.rs +++ b/libs/gl-sdk/src/scheduler.rs @@ -1,9 +1,27 @@ -use crate::{credentials::Credentials, signer::Signer, util::exec, Error}; +use crate::{ + credentials::{Credentials, DeveloperCert}, + signer::Signer, + util::exec, + Error, +}; #[derive(uniffi::Object, Clone)] pub struct Scheduler { credentials: Option, network: gl_client::bitcoin::Network, + developer_cert: Option, +} + +impl Scheduler { + /// Resolve the credentials to use for unauthenticated scheduler + /// calls (register, recover). Uses the developer certificate if + /// one was provided via `with_developer_cert()`, otherwise falls + /// back to the compiled-in default. + fn nobody(&self) -> gl_client::credentials::Nobody { + self.developer_cert + .clone() + .unwrap_or_else(gl_client::credentials::Nobody::new) + } } #[uniffi::export] @@ -12,25 +30,34 @@ impl Scheduler { /// production service pre-configured. #[uniffi::constructor()] pub fn new(network: crate::Network) -> Result { - // We use the nobody credentials since there is no - // authenticated method we expose at the moment. - let creds = None; let network: gl_client::bitcoin::Network = network.into(); Ok(Scheduler { - credentials: creds, + credentials: None, network, + developer_cert: None, }) } + /// Configure a developer certificate obtained from the Greenlight + /// Developer Console. Nodes registered through this scheduler + /// will be associated with the developer's account. + /// + /// Returns a new `Scheduler` instance with the developer + /// certificate configured. + pub fn with_developer_cert(&self, cert: &DeveloperCert) -> Scheduler { + Scheduler { + developer_cert: Some(cert.inner.clone()), + ..self.clone() + } + } + pub fn register(&self, signer: &Signer, code: Option) -> Result { + let nobody = self.nobody(); exec(async move { - let inner = gl_client::scheduler::Scheduler::new( - self.network, - gl_client::credentials::Nobody::new(), - ) - .await - .map_err(|e| Error::Other(e.to_string()))?; + let inner = gl_client::scheduler::Scheduler::new(self.network, nobody) + .await + .map_err(|e| Error::Other(e.to_string()))?; let res = inner .register(&signer.inner, code) @@ -42,13 +69,11 @@ impl Scheduler { } pub fn recover(&self, signer: &Signer) -> Result { + let nobody = self.nobody(); exec(async move { - let inner = gl_client::scheduler::Scheduler::new( - self.network, - gl_client::credentials::Nobody::new(), - ) - .await - .map_err(|e| Error::Other(e.to_string()))?; + let inner = gl_client::scheduler::Scheduler::new(self.network, nobody) + .await + .map_err(|e| Error::Other(e.to_string()))?; let res = inner .recover(&signer.inner) From 8a5061a54b3dea4faa17137a7397cfe74a13e852 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Thu, 19 Mar 2026 13:28:16 +0100 Subject: [PATCH 3/5] gl-sdk-napi: Add DeveloperCert and withDeveloperCert() to NAPI bindings Mirror the gl-sdk DeveloperCert type and Scheduler builder method in the Node.js NAPI layer so JavaScript/TypeScript consumers can provide their developer certificate at runtime. --- libs/gl-sdk-napi/src/lib.rs | 173 +++++++++++++++++++++++------------- 1 file changed, 112 insertions(+), 61 deletions(-) diff --git a/libs/gl-sdk-napi/src/lib.rs b/libs/gl-sdk-napi/src/lib.rs index 4919e49c..20cc9154 100644 --- a/libs/gl-sdk-napi/src/lib.rs +++ b/libs/gl-sdk-napi/src/lib.rs @@ -5,17 +5,18 @@ use napi_derive::napi; // Import from glsdk crate (gl-sdk library) use glsdk::{ + // Enum types for conversion + ChannelState as GlChannelState, Credentials as GlCredentials, + DeveloperCert as GlDeveloperCert, + Handle as GlHandle, + Network as GlNetwork, Node as GlNode, - NodeEventStream as GlNodeEventStream, NodeEvent as GlNodeEvent, + NodeEventStream as GlNodeEventStream, + OutputStatus as GlOutputStatus, Scheduler as GlScheduler, Signer as GlSigner, - Handle as GlHandle, - Network as GlNetwork, - // Enum types for conversion - ChannelState as GlChannelState, - OutputStatus as GlOutputStatus, }; // ============================================================================ @@ -184,6 +185,11 @@ pub struct FundChannel { // Struct Definitions (all structs must be defined before impl blocks) // ============================================================================ +#[napi] +pub struct DeveloperCert { + inner: GlDeveloperCert, +} + #[napi] pub struct Credentials { inner: GlCredentials, @@ -218,6 +224,21 @@ pub struct NodeEventStream { // NAPI Implementations // ============================================================================ +#[napi] +impl DeveloperCert { + /// Create a new developer certificate from cert and key PEM bytes + /// obtained from the Greenlight Developer Console. + /// + /// # Arguments + /// * `cert` - Certificate PEM bytes + /// * `key` - Private key PEM bytes + #[napi(constructor)] + pub fn new(cert: Buffer, key: Buffer) -> Self { + let inner = GlDeveloperCert::new(cert.to_vec(), key.to_vec()); + Self { inner } + } +} + #[napi] impl Credentials { /// Load credentials from raw bytes @@ -225,8 +246,7 @@ impl Credentials { pub async fn load(raw: Buffer) -> Result { let bytes = raw.to_vec(); let inner = tokio::task::spawn_blocking(move || { - GlCredentials::load(bytes) - .map_err(|e| Error::from_reason(e.to_string())) + GlCredentials::load(bytes).map_err(|e| Error::from_reason(e.to_string())) }) .await .map_err(|e| Error::from_reason(e.to_string()))??; @@ -239,8 +259,7 @@ impl Credentials { pub async fn save(&self) -> Result { let inner = self.inner.clone(); let bytes = tokio::task::spawn_blocking(move || { - inner.save() - .map_err(|e| Error::from_reason(e.to_string())) + inner.save().map_err(|e| Error::from_reason(e.to_string())) }) .await .map_err(|e| Error::from_reason(e.to_string()))??; @@ -261,18 +280,34 @@ impl Scheduler { let gl_network = match network.to_lowercase().as_str() { "bitcoin" => GlNetwork::BITCOIN, "regtest" => GlNetwork::REGTEST, - _ => return Err(Error::from_reason(format!( - "Invalid network: {}. Must be 'bitcoin' or 'regtest'", - network - ))), + _ => { + return Err(Error::from_reason(format!( + "Invalid network: {}. Must be 'bitcoin' or 'regtest'", + network + ))) + } }; - let inner = GlScheduler::new(gl_network) - .map_err(|e| Error::from_reason(e.to_string()))?; + let inner = GlScheduler::new(gl_network).map_err(|e| Error::from_reason(e.to_string()))?; Ok(Self { inner }) } + /// Configure a developer certificate obtained from the Greenlight + /// Developer Console. Nodes registered through this scheduler + /// will be associated with the developer's account. + /// + /// Returns a new Scheduler instance with the developer certificate + /// configured. + /// + /// # Arguments + /// * `cert` - Developer certificate from the Greenlight Developer Console + #[napi] + pub fn with_developer_cert(&self, cert: &DeveloperCert) -> Scheduler { + let inner = self.inner.with_developer_cert(&cert.inner); + Scheduler { inner } + } + /// Register a new node with the scheduler /// /// # Arguments @@ -322,8 +357,7 @@ impl Signer { #[napi(constructor)] pub fn new(phrase: String) -> Result { // Constructor stays sync — pure key derivation, no I/O - let inner = GlSigner::new(phrase) - .map_err(|e| Error::from_reason(e.to_string()))?; + let inner = GlSigner::new(phrase).map_err(|e| Error::from_reason(e.to_string()))?; Ok(Self { inner }) } @@ -411,8 +445,8 @@ impl Node { #[napi(constructor)] pub fn new(credentials: &Credentials) -> Result { // Constructor stays sync — connection is established lazily - let inner = GlNode::new(&credentials.inner) - .map_err(|e| Error::from_reason(e.to_string()))?; + let inner = + GlNode::new(&credentials.inner).map_err(|e| Error::from_reason(e.to_string()))?; Ok(Self { inner }) } @@ -422,7 +456,8 @@ impl Node { pub async fn stop(&self) -> Result<()> { let inner = self.inner.clone(); tokio::task::spawn_blocking(move || { - inner.stop() + inner + .stop() .map_err(|e| Error::from_reason(format!("Failed to stop node: {:?}", e))) }) .await @@ -597,14 +632,18 @@ impl Node { .map_err(|e| Error::from_reason(e.to_string()))??; Ok(ListPeersResponse { - peers: response.peers.into_iter().map(|p| Peer { - id: Buffer::from(p.id), - connected: p.connected, - num_channels: p.num_channels, - netaddr: p.netaddr, - remote_addr: p.remote_addr, - features: p.features.map(Buffer::from), - }).collect(), + peers: response + .peers + .into_iter() + .map(|p| Peer { + id: Buffer::from(p.id), + connected: p.connected, + num_channels: p.num_channels, + netaddr: p.netaddr, + remote_addr: p.remote_addr, + features: p.features.map(Buffer::from), + }) + .collect(), }) } @@ -624,19 +663,23 @@ impl Node { .map_err(|e| Error::from_reason(e.to_string()))??; Ok(ListPeerChannelsResponse { - channels: response.channels.into_iter().map(|c| PeerChannel { - peer_id: Buffer::from(c.peer_id), - peer_connected: c.peer_connected, - state: channel_state_to_string(&c.state), - short_channel_id: c.short_channel_id, - channel_id: c.channel_id.map(Buffer::from), - funding_txid: c.funding_txid.map(Buffer::from), - funding_outnum: c.funding_outnum, - to_us_msat: c.to_us_msat.map(|v| v as i64), - total_msat: c.total_msat.map(|v| v as i64), - spendable_msat: c.spendable_msat.map(|v| v as i64), - receivable_msat: c.receivable_msat.map(|v| v as i64), - }).collect(), + channels: response + .channels + .into_iter() + .map(|c| PeerChannel { + peer_id: Buffer::from(c.peer_id), + peer_connected: c.peer_connected, + state: channel_state_to_string(&c.state), + short_channel_id: c.short_channel_id, + channel_id: c.channel_id.map(Buffer::from), + funding_txid: c.funding_txid.map(Buffer::from), + funding_outnum: c.funding_outnum, + to_us_msat: c.to_us_msat.map(|v| v as i64), + total_msat: c.total_msat.map(|v| v as i64), + spendable_msat: c.spendable_msat.map(|v| v as i64), + receivable_msat: c.receivable_msat.map(|v| v as i64), + }) + .collect(), }) } @@ -656,25 +699,33 @@ impl Node { .map_err(|e| Error::from_reason(e.to_string()))??; Ok(ListFundsResponse { - outputs: response.outputs.into_iter().map(|o| FundOutput { - txid: Buffer::from(o.txid), - output: o.output, - amount_msat: o.amount_msat as i64, - status: output_status_to_string(&o.status), - address: o.address, - blockheight: o.blockheight, - }).collect(), - channels: response.channels.into_iter().map(|c| FundChannel { - peer_id: Buffer::from(c.peer_id), - our_amount_msat: c.our_amount_msat as i64, - amount_msat: c.amount_msat as i64, - funding_txid: Buffer::from(c.funding_txid), - funding_output: c.funding_output, - connected: c.connected, - state: channel_state_to_string(&c.state), - short_channel_id: c.short_channel_id, - channel_id: c.channel_id.map(Buffer::from), - }).collect(), + outputs: response + .outputs + .into_iter() + .map(|o| FundOutput { + txid: Buffer::from(o.txid), + output: o.output, + amount_msat: o.amount_msat as i64, + status: output_status_to_string(&o.status), + address: o.address, + blockheight: o.blockheight, + }) + .collect(), + channels: response + .channels + .into_iter() + .map(|c| FundChannel { + peer_id: Buffer::from(c.peer_id), + our_amount_msat: c.our_amount_msat as i64, + amount_msat: c.amount_msat as i64, + funding_txid: Buffer::from(c.funding_txid), + funding_output: c.funding_output, + connected: c.connected, + state: channel_state_to_string(&c.state), + short_channel_id: c.short_channel_id, + channel_id: c.channel_id.map(Buffer::from), + }) + .collect(), }) } } From 45c819b04f24d8c94b1500be90fec8ab235ddd8a Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Thu, 19 Mar 2026 13:28:21 +0100 Subject: [PATCH 4/5] tests: Add DeveloperCert unit and integration tests Test DeveloperCert construction, type error handling, Scheduler builder pattern, and end-to-end registration with an explicit developer certificate from the test fixture. --- libs/gl-sdk/tests/test_basic.py | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/libs/gl-sdk/tests/test_basic.py b/libs/gl-sdk/tests/test_basic.py index 76699648..1899b7dd 100644 --- a/libs/gl-sdk/tests/test_basic.py +++ b/libs/gl-sdk/tests/test_basic.py @@ -57,6 +57,47 @@ def test_node_creation_fails_with_empty_creds(): node = glsdk.Node(creds) +def test_developer_cert_construction(): + """Test that DeveloperCert can be constructed with cert and key bytes.""" + cert = glsdk.DeveloperCert(b"fake-cert-pem", b"fake-key-pem") + assert cert is not None + assert isinstance(cert, glsdk.DeveloperCert) + + +def test_developer_cert_type_error(): + """Test that passing wrong types to DeveloperCert raises TypeError.""" + with pytest.raises(TypeError): + glsdk.DeveloperCert("not bytes", b"key") + with pytest.raises(TypeError): + glsdk.DeveloperCert(b"cert", "not bytes") + + +def test_scheduler_with_developer_cert(): + """Test that with_developer_cert returns a new Scheduler instance.""" + cert = glsdk.DeveloperCert(b"fake-cert-pem", b"fake-key-pem") + scheduler = glsdk.Scheduler(glsdk.Network.BITCOIN) + scheduler_with_cert = scheduler.with_developer_cert(cert) + + # Should return a new Scheduler instance, not modify the original + assert scheduler_with_cert is not None + assert isinstance(scheduler_with_cert, glsdk.Scheduler) + + +def test_register_with_developer_cert(scheduler, nobody_id): + """Test that register works when using an explicit DeveloperCert.""" + # Load the test nobody cert/key from the fixture's byte attributes + dev_cert = glsdk.DeveloperCert(nobody_id.cert_chain, nobody_id.private_key) + + signer = glsdk.Signer( + "abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon about" + ) + s = glsdk.Scheduler(glsdk.Network.BITCOIN).with_developer_cert(dev_cert) + creds = s.register(signer, code=None) + assert creds is not None + assert isinstance(creds, glsdk.Credentials) + + def test_register_and_auth(scheduler, clients): signer = glsdk.Signer( "abandon abandon abandon abandon abandon abandon " From 87c4775c5b14cd57c9db5e75c2e6b1878c9ddcf4 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Thu, 19 Mar 2026 13:28:26 +0100 Subject: [PATCH 5/5] docs: Update certificate reference with runtime SDK examples Expand the runtime certificate section with examples for the gl-sdk (Python, Kotlin, Swift, JavaScript) using the new DeveloperCert type and Scheduler builder, plus gl-client direct usage examples. --- docs/src/reference/certs.md | 95 ++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/docs/src/reference/certs.md b/docs/src/reference/certs.md index 569bec92..3a6ff456 100644 --- a/docs/src/reference/certs.md +++ b/docs/src/reference/certs.md @@ -53,32 +53,101 @@ warning: Set "GL_CUSTOM_NOBODY_KEY" and "GL_CUSTOM_NOBODY_CERT" to use a custom In case you do not want to provide the certificate at compile-time, e.g., because you are using pre-compiled language bindings, you can -also provide the certificates at runtime. The following code snippets show how to construct a `Signer` and a `Scheduler` instance with the certificates: +also provide the certificates at runtime. + +### Using the SDK (recommended) + +The SDK (`gl-sdk`) provides a `DeveloperCert` type and a builder +method on the `Scheduler`. Create a `DeveloperCert` from the PEM +bytes of the certificate and key, then pass it to the scheduler using +`with_developer_cert()`: + +=== "Python" + ```python + import glsdk + + # Load cert and key bytes (e.g., from files or secure storage) + cert = open("client.crt", "rb").read() + key = open("client-key.pem", "rb").read() + + dev_cert = glsdk.DeveloperCert(cert, key) + scheduler = glsdk.Scheduler(glsdk.Network.BITCOIN).with_developer_cert(dev_cert) + creds = scheduler.register(signer, code=None) + ``` + +=== "Kotlin" + ```kotlin + val cert = File("client.crt").readBytes() + val key = File("client-key.pem").readBytes() + + val devCert = DeveloperCert(cert, key) + val scheduler = Scheduler(Network.BITCOIN).withDeveloperCert(devCert) + val creds = scheduler.register(signer, null) + ``` + +=== "Swift" + ```swift + let cert = try Data(contentsOf: URL(fileURLWithPath: "client.crt")) + let key = try Data(contentsOf: URL(fileURLWithPath: "client-key.pem")) + + let devCert = DeveloperCert(cert: cert, key: key) + let scheduler = Scheduler(network: .bitcoin).withDeveloperCert(cert: devCert) + let creds = try scheduler.register(signer: signer, code: nil) + ``` + +=== "JavaScript" + ```javascript + const { DeveloperCert, Scheduler } = require('gl-sdk'); + + const cert = fs.readFileSync('client.crt'); + const key = fs.readFileSync('client-key.pem'); + + const devCert = new DeveloperCert(cert, key); + const scheduler = new Scheduler('bitcoin').withDeveloperCert(devCert); + const creds = await scheduler.register(signer); + ``` + +If you are using an invite code instead of a developer certificate, +simply omit the `with_developer_cert()` call: + +```python +scheduler = glsdk.Scheduler(glsdk.Network.BITCOIN) +creds = scheduler.register(signer, code="your-invite-code") +``` + +### Using gl-client directly + +For the lower-level `gl-client` library, you can construct a +`Nobody` credential with custom certificate bytes: === "Rust" ```rust - use gl_client::tls::{Signer, Scheduler, TlsConfig}; - let tls = TlsConfig::new()?.identity(certificate, key); + use gl_client::credentials::Nobody; + use gl_client::scheduler::Scheduler; + use gl_client::signer::Signer; - let signer = Signer(seed, Network::Bitcoin, tls); + let cert = std::fs::read("client.crt")?; + let key = std::fs::read("client-key.pem")?; + let developer_creds = Nobody::with(cert, key); - let scheduler = Scheduler::with(signer.node_id(), Network::Bitcoin, "uri", &tls).await?; + let signer = Signer::new(seed, Network::Bitcoin, developer_creds.clone())?; + let scheduler = Scheduler::new(Network::Bitcoin, developer_creds).await?; + let res = scheduler.register(&signer, None).await?; ``` === "Python" ```python - from glclient import TlsConfig, Signer, Scheduler - tls = TlsConfig().identity(res.device_cert, res.device_key) + from glclient import Credentials, Signer, Scheduler - signer = Signer(seed, network="bitcoin", tls=tls) + cert = open("client.crt", "rb").read() + key = open("client-key.pem", "rb").read() + creds = Credentials.nobody_with(cert, key) - node = Scheduler(node_id=signer.node_id(), network="bitcoin", tls=tls).node() + signer = Signer(seed, network="bitcoin", creds=creds) + scheduler = Scheduler(network="bitcoin", creds=creds) + res = scheduler.register(signer) ``` -Notice that this is the same way that the `TlsConfig` is configured -with the user credentials provided from the `register()` and -`recover()` results. - !!! important Certificates are credentials authenticating you as the developer of the Application, just like API keys. Do not publish