Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
95 changes: 82 additions & 13 deletions docs/src/reference/certs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
173 changes: 112 additions & 61 deletions libs/gl-sdk-napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

// ============================================================================
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -218,15 +224,29 @@ 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
#[napi(factory)]
pub async fn load(raw: Buffer) -> Result<Credentials> {
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()))??;
Expand All @@ -239,8 +259,7 @@ impl Credentials {
pub async fn save(&self) -> Result<Buffer> {
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()))??;
Expand All @@ -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
Expand Down Expand Up @@ -322,8 +357,7 @@ impl Signer {
#[napi(constructor)]
pub fn new(phrase: String) -> Result<Self> {
// 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 })
}
Expand Down Expand Up @@ -411,8 +445,8 @@ impl Node {
#[napi(constructor)]
pub fn new(credentials: &Credentials) -> Result<Self> {
// 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 })
}
Expand All @@ -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
Expand Down Expand Up @@ -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(),
})
}

Expand All @@ -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(),
})
}

Expand All @@ -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(),
})
}
}
Expand Down
Loading
Loading