Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ examples/javascript/package-lock.json

# IntelliJ
.idea/

# VSCode
.vscode/
187 changes: 127 additions & 60 deletions libs/gl-sdk-napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,28 @@ pub struct Node {
impl Credentials {
/// Load credentials from raw bytes
#[napi(factory)]
pub fn load(raw: Buffer) -> Result<Credentials> {
let inner = GlCredentials::load(raw.to_vec())
.map_err(|e| Error::from_reason(e.to_string()))?;
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()))
})
.await
.map_err(|e| Error::from_reason(e.to_string()))??;

Ok(Self { inner })
}

/// Save credentials to raw bytes
#[napi]
pub fn save(&self) -> Result<Buffer> {
let bytes = self.inner.save()
.map_err(|e| Error::from_reason(e.to_string()))?;
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()))
})
.await
.map_err(|e| Error::from_reason(e.to_string()))??;

Ok(Buffer::from(bytes))
}
Expand All @@ -102,102 +113,129 @@ impl Credentials {
#[napi]
impl Scheduler {
/// Create a new scheduler client
///
///
/// # Arguments
/// * `network` - Network name ("bitcoin" or "regtest")
#[napi(constructor)]
pub fn new(network: String) -> Result<Self> {
// Parse network string to GlNetwork enum
// Constructor stays sync — it's just parsing a string and initialising a struct
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'",
"Invalid network: {}. Must be 'bitcoin' or 'regtest'",
network
))),
};

let inner = GlScheduler::new(gl_network)
.map_err(|e| Error::from_reason(e.to_string()))?;

Ok(Self { inner })
}

/// Register a new node with the scheduler
///
///
/// # Arguments
/// * `signer` - The signer instance
/// * `code` - Optional invite code
#[napi]
pub fn register(&self, signer: &Signer, code: Option<String>) -> Result<Credentials> {
let inner = self.inner
.register(&signer.inner, code)
.map_err(|e| Error::from_reason(e.to_string()))?;

pub async fn register(&self, signer: &Signer, code: Option<String>) -> Result<Credentials> {
let inner_scheduler = self.inner.clone();
let inner_signer = signer.inner.clone();
let inner = tokio::task::spawn_blocking(move || {
inner_scheduler
.register(&inner_signer, code)
.map_err(|e| Error::from_reason(e.to_string()))
})
.await
.map_err(|e| Error::from_reason(e.to_string()))??;

Ok(Credentials { inner })
}

/// Recover node credentials
///
///
/// # Arguments
/// * `signer` - The signer instance
#[napi]
pub fn recover(&self, signer: &Signer) -> Result<Credentials> {
let inner = self.inner
.recover(&signer.inner)
.map_err(|e| Error::from_reason(e.to_string()))?;

pub async fn recover(&self, signer: &Signer) -> Result<Credentials> {
let inner_scheduler = self.inner.clone();
let inner_signer = signer.inner.clone();
let inner = tokio::task::spawn_blocking(move || {
inner_scheduler
.recover(&inner_signer)
.map_err(|e| Error::from_reason(e.to_string()))
})
.await
.map_err(|e| Error::from_reason(e.to_string()))??;

Ok(Credentials { inner })
}
}

#[napi]
impl Signer {
/// Create a new signer from a BIP39 mnemonic phrase
///
///
/// # Arguments
/// * `phrase` - BIP39 mnemonic phrase (12 or 24 words)
#[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()))?;

Ok(Self { inner })
}

/// Authenticate the signer with credentials
///
///
/// # Arguments
/// * `credentials` - Device credentials from registration
#[napi]
pub fn authenticate(&self, credentials: &Credentials) -> Result<Signer> {
let inner = self.inner.authenticate(&credentials.inner)
.map_err(|e| Error::from_reason(e.to_string()))?;

pub async fn authenticate(&self, credentials: &Credentials) -> Result<Signer> {
let inner_signer = self.inner.clone();
let inner_creds = credentials.inner.clone();
let inner = tokio::task::spawn_blocking(move || {
inner_signer
.authenticate(&inner_creds)
.map_err(|e| Error::from_reason(e.to_string()))
})
.await
.map_err(|e| Error::from_reason(e.to_string()))??;

Ok(Signer { inner })
}

/// Start the signer's background task
/// Returns a handle to control the signer
#[napi]
pub fn start(&self) -> Result<Handle> {
let inner = self.inner.start()
.map_err(|e| Error::from_reason(e.to_string()))?;
pub async fn start(&self) -> Result<Handle> {
let inner_signer = self.inner.clone();
let inner = tokio::task::spawn_blocking(move || {
inner_signer
.start()
.map_err(|e| Error::from_reason(e.to_string()))
})
.await
.map_err(|e| Error::from_reason(e.to_string()))??;

Ok(Handle { inner })
}

/// Get the node ID for this signer
/// (stays sync — pure in-memory computation, no I/O)
#[napi]
pub fn node_id(&self) -> Buffer {
let node_id = self.inner.node_id();
Buffer::from(node_id)
Buffer::from(self.inner.node_id())
}
}

#[napi]
impl Handle {
/// Stop the signer's background task
/// (stays sync — just sends a stop signal)
#[napi]
pub fn stop(&self) {
self.inner.stop();
Expand All @@ -207,58 +245,75 @@ impl Handle {
#[napi]
impl Node {
/// Create a new node connection
///
///
/// # Arguments
/// * `credentials` - Device credentials
#[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()))?;

Ok(Self { inner })
}

/// Stop the node if it is currently running
#[napi]
pub fn stop(&self) -> Result<()> {
self.inner.stop()
.map_err(|e| Error::from_reason(format!("Failed to stop node: {:?}", e)))
pub async fn stop(&self) -> Result<()> {
let inner = self.inner.clone();
tokio::task::spawn_blocking(move || {
inner.stop()
.map_err(|e| Error::from_reason(format!("Failed to stop node: {:?}", e)))
})
.await
.map_err(|e| Error::from_reason(e.to_string()))?
}

/// Receive a payment (generate invoice with JIT channel support)
///
///
/// # Arguments
/// * `label` - Unique label for this invoice
/// * `description` - Invoice description
/// * `amount_msat` - Optional amount in millisatoshis
#[napi]
pub fn receive(
pub async fn receive(
&self,
label: String,
description: String,
amount_msat: Option<i64>,
) -> Result<ReceiveResponse> {

let inner = self.inner.clone();
let amount = amount_msat.map(|a| a as u64);
let response = self.inner.receive(label, description, amount)
.map_err(|e| Error::from_reason(e.to_string()))?;

let response = tokio::task::spawn_blocking(move || {
inner
.receive(label, description, amount)
.map_err(|e| Error::from_reason(e.to_string()))
})
.await
.map_err(|e| Error::from_reason(e.to_string()))??;

Ok(ReceiveResponse {
bolt11: response.bolt11,
})
}

/// Send a payment
///
///
/// # Arguments
/// * `invoice` - BOLT11 invoice string
/// * `amount_msat` - Optional amount for zero-amount invoices
#[napi]
pub fn send(&self, invoice: String, amount_msat: Option<i64>) -> Result<SendResponse> {
pub async fn send(&self, invoice: String, amount_msat: Option<i64>) -> Result<SendResponse> {
let inner = self.inner.clone();
let amount = amount_msat.map(|a| a as u64);
let response = self.inner.send(invoice, amount)
.map_err(|e| Error::from_reason(e.to_string()))?;

let response = tokio::task::spawn_blocking(move || {
inner
.send(invoice, amount)
.map_err(|e| Error::from_reason(e.to_string()))
})
.await
.map_err(|e| Error::from_reason(e.to_string()))??;

Ok(SendResponse {
status: response.status as u32,
preimage: Buffer::from(response.preimage),
Expand All @@ -269,19 +324,25 @@ impl Node {
}

/// Send an on-chain transaction
///
///
/// # Arguments
/// * `destination` - Bitcoin address
/// * `amount_or_all` - Amount (e.g., "10000sat", "1000msat") or "all"
#[napi]
pub fn onchain_send(
pub async fn onchain_send(
&self,
destination: String,
amount_or_all: String,
) -> Result<OnchainSendResponse> {
let response = self.inner.onchain_send(destination, amount_or_all)
.map_err(|e| Error::from_reason(e.to_string()))?;

let inner = self.inner.clone();
let response = tokio::task::spawn_blocking(move || {
inner
.onchain_send(destination, amount_or_all)
.map_err(|e| Error::from_reason(e.to_string()))
})
.await
.map_err(|e| Error::from_reason(e.to_string()))??;

Ok(OnchainSendResponse {
tx: Buffer::from(response.tx),
txid: Buffer::from(response.txid),
Expand All @@ -291,10 +352,16 @@ impl Node {

/// Generate a new on-chain address
#[napi]
pub fn onchain_receive(&self) -> Result<OnchainReceiveResponse> {
let response = self.inner.onchain_receive()
.map_err(|e| Error::from_reason(e.to_string()))?;

pub async fn onchain_receive(&self) -> Result<OnchainReceiveResponse> {
let inner = self.inner.clone();
let response = tokio::task::spawn_blocking(move || {
inner
.onchain_receive()
.map_err(|e| Error::from_reason(e.to_string()))
})
.await
.map_err(|e| Error::from_reason(e.to_string()))??;

Ok(OnchainReceiveResponse {
bech32: response.bech32,
p2tr: response.p2tr,
Expand Down
11 changes: 11 additions & 0 deletions libs/gl-sdk/.tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ tasks:
--language ruby \
--out-dir ./libs/gl-sdk/bindings

bindings-typescript:
desc: "Generate Typescript bindings"
dir: "{{.TASKFILE_DIR}}/../gl-sdk-napi"
deps:
- build
cmds:
- |
npm install
npm run build

bindings-all:
desc: "Generate all language bindings"
dir: "{{.TASKFILE_DIR}}/../.."
Expand All @@ -92,6 +102,7 @@ tasks:
- bindings-kotlin
- bindings-swift
- bindings-ruby
- bindings-typescript

package-python:
desc: "Build Python wheel package"
Expand Down
Loading