diff --git a/.gitignore b/.gitignore index ff2928602..217236897 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ examples/javascript/package-lock.json # IntelliJ .idea/ + +# VSCode +.vscode/ diff --git a/libs/gl-sdk-napi/src/lib.rs b/libs/gl-sdk-napi/src/lib.rs index 20f679095..2af6c3aba 100644 --- a/libs/gl-sdk-napi/src/lib.rs +++ b/libs/gl-sdk-napi/src/lib.rs @@ -83,17 +83,28 @@ pub struct Node { impl Credentials { /// Load credentials from raw bytes #[napi(factory)] - pub fn load(raw: Buffer) -> Result { - let inner = GlCredentials::load(raw.to_vec()) - .map_err(|e| Error::from_reason(e.to_string()))?; + 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())) + }) + .await + .map_err(|e| Error::from_reason(e.to_string()))??; + Ok(Self { inner }) } /// Save credentials to raw bytes #[napi] - pub fn save(&self) -> Result { - let bytes = self.inner.save() - .map_err(|e| Error::from_reason(e.to_string()))?; + 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())) + }) + .await + .map_err(|e| Error::from_reason(e.to_string()))??; Ok(Buffer::from(bytes)) } @@ -102,51 +113,63 @@ 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 { - // 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) -> Result { - 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) -> Result { + 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 { - let inner = self.inner - .recover(&signer.inner) - .map_err(|e| Error::from_reason(e.to_string()))?; - + pub async fn recover(&self, signer: &Signer) -> Result { + 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 }) } } @@ -154,50 +177,65 @@ impl Scheduler { #[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 { + // 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 { - let inner = self.inner.authenticate(&credentials.inner) - .map_err(|e| Error::from_reason(e.to_string()))?; - + pub async fn authenticate(&self, credentials: &Credentials) -> Result { + 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 { - let inner = self.inner.start() - .map_err(|e| Error::from_reason(e.to_string()))?; + pub async fn start(&self) -> Result { + 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(); @@ -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 { + // 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, ) -> Result { - + 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) -> Result { + pub async fn send(&self, invoice: String, amount_msat: Option) -> Result { + 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), @@ -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 { - 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), @@ -291,10 +352,16 @@ impl Node { /// Generate a new on-chain address #[napi] - pub fn onchain_receive(&self) -> Result { - let response = self.inner.onchain_receive() - .map_err(|e| Error::from_reason(e.to_string()))?; - + pub async fn onchain_receive(&self) -> Result { + 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, diff --git a/libs/gl-sdk/.tasks.yml b/libs/gl-sdk/.tasks.yml index 231bfd092..84880ffa6 100644 --- a/libs/gl-sdk/.tasks.yml +++ b/libs/gl-sdk/.tasks.yml @@ -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}}/../.." @@ -92,6 +102,7 @@ tasks: - bindings-kotlin - bindings-swift - bindings-ruby + - bindings-typescript package-python: desc: "Build Python wheel package"