diff --git a/libs/gl-sdk-napi/README.md b/libs/gl-sdk-napi/README.md index bbff53757..dbff3486a 100644 --- a/libs/gl-sdk-napi/README.md +++ b/libs/gl-sdk-napi/README.md @@ -83,9 +83,10 @@ gl-sdk-napi/ **Async/Await**: All network operations are asynchronous. Always use await or handle returned promises properly to avoid unhandled rejections or unexpected behavior. +**Streaming**: streamNodeEvents() runs as a background task — call startEventStream(node) without await so it listens for events concurrently while your app continues calling other node methods. When you call node.stop(), next() returns null and the loop exits cleanly. ```typescript -import { Scheduler, Signer, Node, Credentials, OnchainReceiveResponse } from '@greenlightcln/glsdk'; +import { Scheduler, Signer, Node, Credentials, OnchainReceiveResponse, NodeEvent, NodeEventStream } from '@greenlightcln/glsdk'; type Network = 'bitcoin' | 'regtest'; @@ -98,8 +99,7 @@ class GreenlightApp { constructor(phrase: string, network: Network) { this.scheduler = new Scheduler(network); this.signer = new Signer(phrase); - const nodeId = this.signer.nodeId(); - console.log(`✓ Signer created. Node ID: ${nodeId.toString('hex')}`); + console.log(`✓ Signer created. Node ID: ${this.signer.nodeId().toString('hex')}`); } async registerOrRecover(inviteCode?: string): Promise { @@ -129,6 +129,53 @@ class GreenlightApp { return this.node; } + // Starts the event stream as a background task — returns immediately. + // The loop runs concurrently while other node methods are called. + startEventStream(): void { + if (!this.node) { throw new Error('Must create node before starting event stream'); } + const node = this.node; + + (async () => { + let stream: NodeEventStream; + try { + stream = await node.streamNodeEvents(); + console.log('✓ Event stream started'); + } catch (e: any) { + if (e?.message?.includes('Unimplemented')) { + console.warn('StreamNodeEvents not supported by this node build — skipping'); + return; + } + throw e; + } + + while (true) { + const event: NodeEvent | null = await stream.next(); + + // null means the stream closed (node stopped or connection lost) + if (event === null) { + console.log('Event stream closed'); + break; + } + + switch (event.eventType) { + case 'invoice_paid': { + const p = event.invoicePaid!; + console.log('✓ invoice_paid:'); + console.log(` payment_hash: ${p.paymentHash.toString('hex')}`); + console.log(` preimage: ${p.preimage.toString('hex')}`); + console.log(` bolt11: ${p.bolt11}`); + console.log(` label: ${p.label}`); + console.log(` amount_msat: ${p.amountMsat}`); + break; + } + default: + console.log(`Received unrecognised event type: "${event.eventType}" — skipping`); + break; + } + } + })().catch(e => console.error('Event stream error:', e)); + } + async getOnchainAddress(): Promise { if (!this.node) { this.createNode(); } console.log('Generating on-chain address...'); @@ -137,6 +184,8 @@ class GreenlightApp { async cleanup(): Promise { if (this.node) { + // Stopping the node causes stream.next() to return null, + // which exits the startEventStream() loop cleanly. await this.node.stop(); console.log('✓ Node stopped'); } @@ -146,17 +195,25 @@ class GreenlightApp { async function quickStart(): Promise { const phrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; const network: Network = 'regtest'; + console.log('=== Greenlight SDK Demo ==='); const app = new GreenlightApp(phrase, network); + try { await app.registerOrRecover(); app.createNode(); + + // Start listening for events in the background — does not block. + app.startEventStream(); + + // Continue using the node normally while the stream listens concurrently. const address = await app.getOnchainAddress(); console.log(`✓ Bech32 Address: ${address.bech32}`); console.log(`✓ P2TR Address: ${address.p2Tr}`); } catch (e) { console.error('✗ Error:', e); } finally { + // Stops the node and closes the event stream. await app.cleanup(); } } @@ -164,6 +221,23 @@ async function quickStart(): Promise { quickStart(); ``` +### Event Types + +| `eventType` | Payload field | Description | +|-----------------|----------------|------------------------------------| +| `invoice_paid` | `invoicePaid` | An invoice was paid to this node | +| `unknown` | — | An unrecognised server-side event | + +#### `InvoicePaidEvent` fields + +| Field | Type | Description | +|---------------|----------|----------------------------------------| +| `paymentHash` | `Buffer` | Payment hash of the settled invoice | +| `preimage` | `Buffer` | Preimage that proves payment | +| `bolt11` | `string` | The BOLT11 invoice string | +| `label` | `string` | Label assigned to the invoice | +| `amountMsat` | `number` | Amount received in millisatoshis | + ## Development ### Running Tests diff --git a/libs/gl-sdk-napi/example.ts b/libs/gl-sdk-napi/example.ts deleted file mode 100644 index ed099b72b..000000000 --- a/libs/gl-sdk-napi/example.ts +++ /dev/null @@ -1,352 +0,0 @@ -/** - * Example script tests gl-sdk-napi library usage for: - * - Registration/Recovery - * - Node creation - * - Receiving payments - * - Sending payments - * - * This follows the same pattern as glsdk_example.py - */ - -import * as fs from 'fs'; -import * as crypto from 'crypto'; -import { - Scheduler, - Signer, - Node, - Credentials, - ReceiveResponse, - SendResponse, - OnchainReceiveResponse, - OnchainSendResponse, -} from './index'; - -// Network type (matches gl-sdk Network enum) -type Network = 'bitcoin' | 'regtest'; - -/** - * An application demonstrating Greenlight node operations. - * - * The pattern follows the gl-sdk structure: - * - Signer: Created from a BIP39 mnemonic phrase - * - Scheduler: Handles node registration and recovery - * - Credentials: Contains node authentication information - * - Node: Main interface for Lightning operations - */ -class GreenlightApp { - private phrase: string; - private network: Network; - private credentials: Credentials | null = null; - private scheduler: Scheduler; - private node: Node | null = null; - private signer: Signer; - - /** - * Initialize the Greenlight application. - * - * @param phrase - BIP39 mnemonic phrase for node identity - * @param network - Network ('bitcoin' or 'regtest') - */ - constructor(phrase: string, network: Network) { - this.phrase = phrase; - this.network = network; - this.scheduler = new Scheduler(network); - - // Create signer from mnemonic phrase - this.signer = new Signer(phrase); - const nodeId = this.signer.nodeId(); - - console.log(`✓ Signer created for network: ${network}`); - console.log(`✓ Node ID: ${nodeId.toString('hex')}`); - } - - /** - * Registers or recovers a node on Greenlight. - * - * This method will: - * 1. Try to register a new node - * 2. If registration fails (node exists), recover instead - * 3. Store the returned credentials for future operations - * - * The credentials contain the node_id and mTLS client certificate - * for authenticating against the node. - */ - registerOrRecover(): void { - try { - console.log('Attempting to register node...'); - this.credentials = this.scheduler.register(this.signer, ''); - console.log('✓ Node registered successfully'); - } catch (e) { - console.log(`Registration failed (node may already exist): ${e}`); - console.log('Attempting to recover node...'); - try { - this.credentials = this.scheduler.recover(this.signer); - console.log('✓ Node recovered successfully'); - } catch (recoverError) { - console.log(`✗ Recovery also failed: ${recoverError}`); - throw recoverError; - } - } - } - - /** - * Create a Node instance using the credentials. - * - * The Node is the main entrypoint to interact with the Lightning node. - * - * @returns Node instance for making Lightning operations - */ - createNode(): Node { - if (this.credentials === null) { - throw new Error('Must register/recover before creating node'); - } - - console.log('Creating node instance...'); - this.node = new Node(this.credentials); - console.log('✓ Node created successfully'); - return this.node; - } - - /** - * Create a Lightning invoice to receive a payment. - * - * This method generates a BOLT11 invoice that includes negotiation - * of an LSPS2 / JIT channel, meaning that if there is no channel - * sufficient to receive the requested funds, the node will negotiate - * an opening. - * - * @param label - Unique label for the invoice - * @param description - Invoice description - * @param amountMsat - Optional amount in millisatoshis (null for any amount) - * @returns ReceiveResponse containing the BOLT11 invoice string - */ - receive( - label: string, - description: string, - amountMsat: number | null = null - ): ReceiveResponse { - if (this.node === null) { - this.createNode(); - } - - console.log(`Creating invoice: ${amountMsat ?? 'any'} msat - '${description}'`); - - const invoice = this.node!.receive(label, description, amountMsat); - console.log('✓ Invoice created successfully'); - return invoice; - } - - /** - * Pay a Lightning invoice. - * - * @param invoice - BOLT11 invoice string to pay - * @param amountMsat - Optional amount in millisatoshis (for zero-amount invoices) - * @returns SendResponse containing payment details - */ - send(invoice: string, amountMsat: number | null = null): SendResponse { - if (this.node === null) { - this.createNode(); - } - - console.log(`Paying invoice: ${invoice.substring(0, 50)}...`); - - const payment = this.node!.send(invoice, amountMsat); - console.log('✓ Payment sent successfully'); - return payment; - } - - /** - * Get an on-chain address to receive Bitcoin. - * - * @returns OnchainReceiveResponse containing the Bitcoin address - */ - onchainReceive(): OnchainReceiveResponse { - if (this.node === null) { - this.createNode(); - } - - console.log('Generating on-chain receive address...'); - - const response = this.node!.onchainReceive(); - console.log('✓ On-chain address generated'); - return response; - } - - /** - * Send Bitcoin on-chain. - * - * @param destination - Bitcoin address to send to - * @param amountOrAll - Amount in satoshis (e.g., '10000sat') or 'all' to send all funds - * @returns OnchainSendResponse containing transaction details - */ - onchainSend(destination: string, amountOrAll: string): OnchainSendResponse { - if (this.node === null) { - this.createNode(); - } - - console.log(`Sending on-chain: ${amountOrAll} to ${destination}`); - - const response = this.node!.onchainSend(destination, amountOrAll); - console.log('✓ On-chain transaction sent'); - return response; - } - - /** - * Stop the node if it is currently running. - */ - stopNode(): void { - if (this.node !== null) { - console.log('Stopping node...'); - this.node.stop(); - console.log('✓ Node stopped'); - } - } - - /** - * Save credentials to a file. - * - * @param filepath - Path to save credentials - */ - saveCredentials(filepath: string): void { - if (this.credentials === null) { - throw new Error('No credentials to save'); - } - - try { - const credsBytes = this.credentials.save(); - fs.writeFileSync(filepath, credsBytes); - console.log(`✓ Credentials saved to ${filepath}`); - } catch (e) { - console.log(`✗ Failed to save credentials: ${e}`); - throw e; - } - } - - /** - * Load credentials from a file. - * - * @param filepath - Path to load credentials from - * @returns Loaded credentials - */ - loadCredentials(filepath: string): Credentials { - try { - const credsBytes = fs.readFileSync(filepath); - this.credentials = Credentials.load(credsBytes); - console.log(`✓ Credentials loaded from ${filepath}`); - return this.credentials; - } catch (e) { - console.log(`✗ Failed to load credentials: ${e}`); - throw e; - } - } -} - -/** - * Main demonstration function. - */ -function main(): void { - console.log('='.repeat(70)); - console.log('GL-SDK-NAPI Example: Register, Create Node, and Create Invoice'); - console.log('Inspired by glsdk_example.py pattern'); - console.log('='.repeat(70)); - console.log(); - - // Configuration - // NOTE: These should be persisted and loaded from disk in production - // Default test mnemonic (DO NOT USE IN PRODUCTION) - const phrase = process.env.MNEMONIC || - 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; - const network: Network = 'regtest'; // Options: 'bitcoin', 'regtest' - - // Step 1: Initialize application - console.log('Step 1: Initializing Application'); - console.log('-'.repeat(70)); - const app = new GreenlightApp(phrase, network); - console.log(); - - // Step 2: Register or recover node - console.log('Step 2: Register or Recover Node'); - console.log('-'.repeat(70)); - try { - app.registerOrRecover(); - } catch (e) { - console.log(`✗ Failed to register/recover: ${e}`); - console.log('Note: This may fail without a proper Greenlight environment'); - console.error(e); - return; - } - console.log(); - - // Step 3: Create the node - console.log('Step 3: Creating Node'); - console.log('-'.repeat(70)); - try { - app.createNode(); - } catch (e) { - console.log(`✗ Failed to create node: ${e}`); - console.error(e); - return; - } - console.log(); - - // Step 4: Create an invoice (receive payment) - console.log('Step 4: Creating Invoice (Receive)'); - console.log('-'.repeat(70)); - try { - const randomLabel = `test-invoice-${crypto.randomBytes(4).toString('hex')}`; - const invoiceResponse = app.receive( - randomLabel, - 'Test payment for GL-SDK-NAPI demo', - 100000 - ); - console.log(`Invoice response: ${JSON.stringify(invoiceResponse)}`); - } catch (e) { - console.log(`✗ Failed to create invoice: ${e}`); - console.error(e); - } - console.log(); - - // Step 5: Generate on-chain receive address - console.log('Step 5: On-chain Receive Address'); - console.log('-'.repeat(70)); - try { - const onchainResponse = app.onchainReceive(); - console.log(`Onchain receive Bech32: ${onchainResponse.bech32}, P2TR: ${onchainResponse.p2Tr}`); - } catch (e) { - console.log(`✗ Failed to get on-chain address: ${e}`); - console.error(e); - } - console.log(); - - // Step 6: Save credentials (optional) - console.log('Step 6: Saving Credentials'); - console.log('-'.repeat(70)); - try { - app.saveCredentials('/tmp/glsdk_credentials.bin'); - } catch (e) { - console.log(`Note: Credential saving: ${e}`); - } - console.log(); - - // Step 7: Stop the node - console.log('Step 7: Stopping Node'); - console.log('-'.repeat(70)); - try { - app.stopNode(); - } catch (e) { - console.log(`Note: Node stop: ${e}`); - } - console.log(); - - console.log('='.repeat(70)); - console.log('Test Complete!'); - console.log('='.repeat(70)); -} - -// Run the example -try { - main(); -} catch (e) { - console.log(`\n✗ Unexpected error: ${e}`); - console.error(e); -} diff --git a/libs/gl-sdk-napi/jest.config.js b/libs/gl-sdk-napi/jest.config.js index f6a46cdae..c501abcb7 100644 --- a/libs/gl-sdk-napi/jest.config.js +++ b/libs/gl-sdk-napi/jest.config.js @@ -1,6 +1,12 @@ module.exports = { preset: 'ts-jest/presets/default-esm', testEnvironment: 'node', + maxWorkers: 1, + testTimeout: 30000, + runner: 'jest-runner', + resetModules: true, + restoreMocks: true, + clearMocks: true, extensionsToTreatAsEsm: ['.ts'], transform: { '^.+\\.ts$': [ diff --git a/libs/gl-sdk-napi/src/lib.rs b/libs/gl-sdk-napi/src/lib.rs index 2af6c3aba..4919e49c5 100644 --- a/libs/gl-sdk-napi/src/lib.rs +++ b/libs/gl-sdk-napi/src/lib.rs @@ -7,10 +7,15 @@ use napi_derive::napi; use glsdk::{ Credentials as GlCredentials, Node as GlNode, + NodeEventStream as GlNodeEventStream, + NodeEvent as GlNodeEvent, Scheduler as GlScheduler, Signer as GlSigner, Handle as GlHandle, Network as GlNetwork, + // Enum types for conversion + ChannelState as GlChannelState, + OutputStatus as GlOutputStatus, }; // ============================================================================ @@ -46,6 +51,135 @@ pub struct OnchainReceiveResponse { pub p2tr: String, } +// ============================================================================ +// Event Streaming Response Types +// ============================================================================ + +#[napi(object)] +pub struct InvoicePaidEvent { + pub payment_hash: Buffer, + pub bolt11: String, + pub preimage: Buffer, + pub label: String, + /// Amount in millisatoshis (as i64 for JS compatibility) + pub amount_msat: i64, +} + +#[napi(object)] +pub struct NodeEvent { + /// Discriminant: "invoice_paid" | "unknown" + pub event_type: String, + /// Present when event_type == "invoice_paid" + pub invoice_paid: Option, +} + +// ============================================================================ +// GetInfo Response Types +// ============================================================================ + +#[napi(object)] +pub struct GetInfoResponse { + pub id: Buffer, + pub alias: Option, + pub color: Buffer, + pub num_peers: u32, + pub num_pending_channels: u32, + pub num_active_channels: u32, + pub num_inactive_channels: u32, + pub version: String, + pub lightning_dir: String, + pub blockheight: u32, + pub network: String, + /// Fees collected in millisatoshis (as i64 for JS compatibility) + pub fees_collected_msat: i64, +} + +// ============================================================================ +// ListPeers Response Types +// ============================================================================ + +#[napi(object)] +pub struct ListPeersResponse { + pub peers: Vec, +} + +#[napi(object)] +pub struct Peer { + pub id: Buffer, + pub connected: bool, + pub num_channels: Option, + pub netaddr: Vec, + pub remote_addr: Option, + pub features: Option, +} + +// ============================================================================ +// ListPeerChannels Response Types +// ============================================================================ + +#[napi(object)] +pub struct ListPeerChannelsResponse { + pub channels: Vec, +} + +#[napi(object)] +pub struct PeerChannel { + pub peer_id: Buffer, + pub peer_connected: bool, + /// Channel state as string (e.g., "CHANNELD_NORMAL", "OPENINGD") + pub state: String, + pub short_channel_id: Option, + pub channel_id: Option, + pub funding_txid: Option, + pub funding_outnum: Option, + /// Balance to us in millisatoshis (as i64 for JS compatibility) + pub to_us_msat: Option, + /// Total channel capacity in millisatoshis (as i64 for JS compatibility) + pub total_msat: Option, + /// Spendable balance in millisatoshis (as i64 for JS compatibility) + pub spendable_msat: Option, + /// Receivable balance in millisatoshis (as i64 for JS compatibility) + pub receivable_msat: Option, +} + +// ============================================================================ +// ListFunds Response Types +// ============================================================================ + +#[napi(object)] +pub struct ListFundsResponse { + pub outputs: Vec, + pub channels: Vec, +} + +#[napi(object)] +pub struct FundOutput { + pub txid: Buffer, + pub output: u32, + /// Amount in millisatoshis (as i64 for JS compatibility) + pub amount_msat: i64, + /// Output status (e.g., "unconfirmed", "confirmed", "spent", "immature") + pub status: String, + pub address: Option, + pub blockheight: Option, +} + +#[napi(object)] +pub struct FundChannel { + pub peer_id: Buffer, + /// Our amount in millisatoshis (as i64 for JS compatibility) + pub our_amount_msat: i64, + /// Total amount in millisatoshis (as i64 for JS compatibility) + pub amount_msat: i64, + pub funding_txid: Buffer, + pub funding_output: u32, + pub connected: bool, + /// Channel state as string (e.g., "CHANNELD_NORMAL", "OPENINGD") + pub state: String, + pub short_channel_id: Option, + pub channel_id: Option, +} + // ============================================================================ // Struct Definitions (all structs must be defined before impl blocks) // ============================================================================ @@ -75,6 +209,11 @@ pub struct Node { inner: GlNode, } +#[napi] +pub struct NodeEventStream { + inner: std::sync::Arc, +} + // ============================================================================ // NAPI Implementations // ============================================================================ @@ -242,6 +381,27 @@ impl Handle { } } +#[napi] +impl NodeEventStream { + /// Get the next event from the stream. + /// + /// Blocks the calling thread (but not the JS event loop) until an + /// event is available. Returns `null` when the stream ends or the + /// connection is lost. + #[napi] + pub async fn next(&self) -> Result> { + let stream = std::sync::Arc::clone(&self.inner); + tokio::task::spawn_blocking(move || { + stream + .next() + .map_err(|e| Error::from_reason(e.to_string())) + .map(|opt| opt.map(napi_node_event_from_gl)) + }) + .await + .map_err(|e| Error::from_reason(e.to_string()))? + } +} + #[napi] impl Node { /// Create a new node connection @@ -367,4 +527,205 @@ impl Node { p2tr: response.p2tr, }) } + + /// Stream real-time events from the node. + /// + /// Returns a `NodeEventStream`. Call `.next()` on it repeatedly to + /// receive events (e.g., invoice payments) as they occur. + /// + /// Returns `Unimplemented` if the connected node build does not yet + /// support `StreamNodeEvents`. + #[napi] + pub async fn stream_node_events(&self) -> Result { + let inner = self.inner.clone(); + let gl_stream = tokio::task::spawn_blocking(move || { + inner + .stream_node_events() + .map_err(|e| Error::from_reason(e.to_string())) + }) + .await + .map_err(|e| Error::from_reason(e.to_string()))??; + + // gl-sdk returns Arc — store it directly since + // GlNodeEventStream wraps a Mutex> and is not Clone. + Ok(NodeEventStream { inner: gl_stream }) + } + + /// Get information about the node + /// + /// Returns basic information about the node including its ID, + /// alias, network, and channel counts. + #[napi] + pub async fn get_info(&self) -> Result { + let inner = self.inner.clone(); + let response = tokio::task::spawn_blocking(move || { + inner + .get_info() + .map_err(|e| Error::from_reason(e.to_string())) + }) + .await + .map_err(|e| Error::from_reason(e.to_string()))??; + + Ok(GetInfoResponse { + id: Buffer::from(response.id), + alias: response.alias, + color: Buffer::from(response.color), + num_peers: response.num_peers, + num_pending_channels: response.num_pending_channels, + num_active_channels: response.num_active_channels, + num_inactive_channels: response.num_inactive_channels, + version: response.version, + lightning_dir: response.lightning_dir, + blockheight: response.blockheight, + network: response.network, + fees_collected_msat: response.fees_collected_msat as i64, + }) + } + + /// List all peers connected to this node + /// + /// Returns information about all peers including their connection status. + #[napi] + pub async fn list_peers(&self) -> Result { + let inner = self.inner.clone(); + let response = tokio::task::spawn_blocking(move || { + inner + .list_peers() + .map_err(|e| Error::from_reason(e.to_string())) + }) + .await + .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(), + }) + } + + /// List all channels with peers + /// + /// Returns detailed information about all channels including their + /// state, capacity, and balances. + #[napi] + pub async fn list_peer_channels(&self) -> Result { + let inner = self.inner.clone(); + let response = tokio::task::spawn_blocking(move || { + inner + .list_peer_channels() + .map_err(|e| Error::from_reason(e.to_string())) + }) + .await + .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(), + }) + } + + /// List all funds available to the node + /// + /// Returns information about on-chain outputs and channel funds + /// that are available or pending. + #[napi] + pub async fn list_funds(&self) -> Result { + let inner = self.inner.clone(); + let response = tokio::task::spawn_blocking(move || { + inner + .list_funds() + .map_err(|e| Error::from_reason(e.to_string())) + }) + .await + .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(), + }) + } +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/// Convert a gl-sdk `NodeEvent` to the NAPI flat discriminated-union object. +fn napi_node_event_from_gl(event: GlNodeEvent) -> NodeEvent { + match event { + GlNodeEvent::InvoicePaid { details } => NodeEvent { + event_type: "invoice_paid".to_string(), + invoice_paid: Some(InvoicePaidEvent { + payment_hash: Buffer::from(details.payment_hash), + bolt11: details.bolt11, + preimage: Buffer::from(details.preimage), + label: details.label, + amount_msat: details.amount_msat as i64, + }), + }, + GlNodeEvent::Unknown => NodeEvent { + event_type: "unknown".to_string(), + invoice_paid: None, + }, + } +} + +fn channel_state_to_string(state: &GlChannelState) -> String { + match state { + GlChannelState::Openingd => "OPENINGD".to_string(), + GlChannelState::ChanneldAwaitingLockin => "CHANNELD_AWAITING_LOCKIN".to_string(), + GlChannelState::ChanneldNormal => "CHANNELD_NORMAL".to_string(), + GlChannelState::ChanneldShuttingDown => "CHANNELD_SHUTTING_DOWN".to_string(), + GlChannelState::ClosingdSigexchange => "CLOSINGD_SIGEXCHANGE".to_string(), + GlChannelState::ClosingdComplete => "CLOSINGD_COMPLETE".to_string(), + GlChannelState::AwaitingUnilateral => "AWAITING_UNILATERAL".to_string(), + GlChannelState::FundingSpendSeen => "FUNDING_SPEND_SEEN".to_string(), + GlChannelState::Onchain => "ONCHAIN".to_string(), + GlChannelState::DualopendOpenInit => "DUALOPEND_OPEN_INIT".to_string(), + GlChannelState::DualopendAwaitingLockin => "DUALOPEND_AWAITING_LOCKIN".to_string(), + GlChannelState::DualopendOpenCommitted => "DUALOPEND_OPEN_COMMITTED".to_string(), + GlChannelState::DualopendOpenCommitReady => "DUALOPEND_OPEN_COMMIT_READY".to_string(), + } +} + +fn output_status_to_string(status: &GlOutputStatus) -> String { + match status { + GlOutputStatus::Unconfirmed => "unconfirmed".to_string(), + GlOutputStatus::Confirmed => "confirmed".to_string(), + GlOutputStatus::Spent => "spent".to_string(), + GlOutputStatus::Immature => "immature".to_string(), + } } diff --git a/libs/gl-sdk-napi/tests/advanced.spec.ts b/libs/gl-sdk-napi/tests/advanced.spec.ts deleted file mode 100644 index 1b49bb2ff..000000000 --- a/libs/gl-sdk-napi/tests/advanced.spec.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { describe, it, expect, beforeAll, afterEach } from '@jest/globals'; -import { - Credentials, - Scheduler, - Signer, - Node, - ReceiveResponse, - SendResponse, - OnchainReceiveResponse, - OnchainSendResponse, -} from '../index.js'; - -const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; - -describe('Credentials', () => { - it('can save and load raw credentials', async () => { - const original = await Credentials.load(Buffer.from('test')); - const raw = await original.save(); - - expect(Buffer.isBuffer(raw)).toBe(true); - - const restored = await Credentials.load(raw); - const raw2 = await restored.save(); - - expect(raw2.equals(raw)).toBe(true); - }); -}); - -describe('Signer', () => { - it('can be constructed from a mnemonic', async () => { - const signer = new Signer(MNEMONIC); - expect(signer).toBeTruthy(); - }); - - it('can return a node id', async () => { - const signer = new Signer(MNEMONIC); - const nodeId = signer.nodeId(); - - expect(Buffer.isBuffer(nodeId)).toBe(true); - expect(nodeId.length).toBeGreaterThan(0); - }); - - it('returns consistent node id for same mnemonic', async () => { - const signer1 = new Signer(MNEMONIC); - const signer2 = new Signer(MNEMONIC); - - const nodeId1 = signer1.nodeId(); - const nodeId2 = signer2.nodeId(); - - expect(nodeId1.equals(nodeId2)).toBe(true); - }); - - it('can be constructed with different mnemonics', async () => { - const mnemonic2 = 'legal winner thank year wave sausage worth useful legal winner thank yellow'; - const signer = new Signer(mnemonic2); - expect(signer).toBeTruthy(); - - const nodeId = signer.nodeId(); - expect(Buffer.isBuffer(nodeId)).toBe(true); - }); -}); - -describe('Scheduler', () => { - it('can be constructed for regtest', async () => { - const scheduler = new Scheduler('regtest'); - expect(scheduler).toBeTruthy(); - }); - - it('can be constructed for bitcoin mainnet', async () => { - const scheduler = new Scheduler('bitcoin'); - expect(scheduler).toBeTruthy(); - }); -}); - -describe('Integration: scheduler and signer', () => { - let scheduler: Scheduler; - let signer: Signer; - - beforeAll(() => { - scheduler = new Scheduler('regtest'); - signer = new Signer(MNEMONIC); - }); - - it('can recover credentials', async () => { - const recovered = await scheduler.recover(signer); - expect(recovered).toBeInstanceOf(Credentials); - expect((await recovered.save()).length).toBeGreaterThan(0); - }); - - it('handles registration of existing node (falls back to recovery)', async () => { - try { - const credentials = await scheduler.register(signer, ''); - expect(credentials).toBeInstanceOf(Credentials); - } catch (e) { - const recovered = await scheduler.recover(signer); - expect(recovered).toBeInstanceOf(Credentials); - } - }); -}); - -describe('Node', () => { - let node: Node; - let credentials: Credentials; - - beforeAll(async () => { - const scheduler = new Scheduler('regtest'); - const signer = new Signer(MNEMONIC); - credentials = await scheduler.recover(signer); - node = new Node(credentials); - }); - - afterEach(() => { - // Clean up after each test if needed - }); - - it('can be constructed from credentials', async () => { - expect(node).toBeTruthy(); - }); - - describe('onchainReceive', () => { - it('returns valid on-chain addresses', async () => { - const res = await node.onchainReceive(); - - expect(typeof res.bech32).toBe('string'); - expect(res.bech32.length).toBeGreaterThan(0); - expect(res.bech32.startsWith('bc1')).toBe(true); - - expect(typeof res.p2Tr).toBe('string'); - expect(res.p2Tr.length).toBeGreaterThan(0); - expect(res.p2Tr.startsWith('bc1p')).toBe(true); - }); - - it('generates different addresses on multiple calls', async () => { - const res1 = await node.onchainReceive(); - const res2 = await node.onchainReceive(); - - expect(res1.bech32).not.toBe(res2.bech32); - expect(res1.p2Tr).not.toBe(res2.p2Tr); - }); - }); - - // Note: These are currently failing - // describe('receive (Lightning invoice)', () => { - // it('can create invoice with amount', async () => { - // const label = `test-${Date.now()}`; - // const description = 'Test payment'; - // const amountMsat = 100000; - - // const response = await node.receive(label, description, amountMsat); - - // expect(response).toBeTruthy(); - // expect(typeof response.bolt11).toBe('string'); - // expect(response.bolt11.length).toBeGreaterThan(0); - // expect(response.bolt11.toLowerCase().startsWith('ln')).toBe(true); - // }); - // }); - - // describe('send (Lightning payment)', () => { - // it('can attempt to send payment to valid invoice', async () => { - // const label = `test-send-${Date.now()}`; - // const receiveResponse = await node.receive(label, 'Test for send', 1000); - - // try { - // const sendResponse = await node.send(receiveResponse.bolt11, null); - // expect(sendResponse).toBeTruthy(); - // } catch (e) { - // expect(e).toBeDefined(); - // } - // }); - - // it('can send with explicit amount for zero-amount invoice', async () => { - // const label = `test-send-amount-${Date.now()}`; - // const receiveResponse = await node.receive(label, 'Zero amount invoice', null); - - // try { - // const sendResponse = await node.send(receiveResponse.bolt11, 5000); - // expect(sendResponse).toBeTruthy(); - // } catch (e) { - // expect(e).toBeDefined(); - // } - // }); - // }); - - // describe('onchainSend', () => { - // it('can attempt to send specific amount on-chain', async () => { - // const destAddress = (await node.onchainReceive()).bech32; - - // try { - // const response = await node.onchainSend(destAddress, '10000sat'); - // expect(response).toBeTruthy(); - // } catch (e) { - // expect(e).toBeDefined(); - // } - // }); - - // it('can attempt to send all funds on-chain', async () => { - // const destAddress = (await node.onchainReceive()).bech32; - - // try { - // const response = await node.onchainSend(destAddress, 'all'); - // expect(response).toBeTruthy(); - // } catch (e) { - // expect(e).toBeDefined(); - // } - // }); - // }); - - describe('stop', () => { - it('can stop the node', async () => { - const testScheduler = new Scheduler('regtest'); - const testSigner = new Signer(MNEMONIC); - const testCredentials = await testScheduler.recover(testSigner); - const testNode = new Node(testCredentials); - - await expect(testNode.stop()).resolves.not.toThrow(); - }); - }); -}); diff --git a/libs/gl-sdk-napi/tests/eventstream.spec.ts b/libs/gl-sdk-napi/tests/eventstream.spec.ts new file mode 100644 index 000000000..44d327de1 --- /dev/null +++ b/libs/gl-sdk-napi/tests/eventstream.spec.ts @@ -0,0 +1,194 @@ +import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'; +import { + Credentials, + Scheduler, + Signer, + Node, + NodeEventStream, + NodeEvent, + InvoicePaidEvent, +} from '../index.js'; + +const MNEMONIC = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; + +describe('NodeEvent (type contract)', () => { + it('NodeEvent and InvoicePaidEvent are assignable from NAPI-generated types', () => { + // This is a compile-time check only. If the NAPI bindings change the + // field names or types, tsc will fail here before Jest even runs. + // The runtime assertion is intentionally trivial. + const details: InvoicePaidEvent = { + paymentHash: Buffer.from('deadbeef', 'hex'), + bolt11: 'lnbcrt1...', + preimage: Buffer.from('cafebabe', 'hex'), + label: 'test-label', + amountMsat: 1_000_000, + }; + const event: NodeEvent = { eventType: 'invoice_paid', invoicePaid: details }; + + // Only assert what tsc cannot: that the import itself resolved and + // the constructed value is truthy (i.e. the module loaded correctly). + expect(event).toBeDefined(); + }); +}); + +// ============================================================================ +// NodeEventStream integration tests — require a live regtest scheduler +// ============================================================================ + +describe('NodeEventStream (integration)', () => { + let credentials: Credentials; + let node: Node; + let streamingSupported = true; + let suiteAvailable = true; + + beforeAll(async () => { + try { + const scheduler = new Scheduler('regtest'); + const signer = new Signer(MNEMONIC); + credentials = await scheduler.recover(signer); + node = new Node(credentials); + } catch (e: any) { + console.warn(`⚠ Scheduler unavailable — skipping all integration tests`); + console.warn(`(${e?.message ?? e})`); + suiteAvailable = false; + return; + } + + try { + const probe = await node.streamNodeEvents(); + void probe; + } catch (e: any) { + console.warn(`⚠ StreamNodeEvents probe failed — skipping stream tests`); + console.warn(`(${e?.message ?? e})`); + // Treat every probe failure as "not supported" — covers both + // Unimplemented and any Unavailable thrown at this point. + streamingSupported = false; + } + }); + + afterAll(async () => { + if (suiteAvailable && node) { + await node.stop(); + } + }); + + /** + * Wrapper around `it` that skips the test body when: + * - The regtest scheduler was unreachable (`suiteAvailable = false`), or + * - The connected node does not implement StreamNodeEvents (`streamingSupported = false`). + */ + const streamIt = (name: string, fn: () => Promise, timeout?: number) => + it( + name, + async () => { + if (!suiteAvailable) { + console.log('Skipped — scheduler unavailable'); + return; + } + if (!streamingSupported) { + console.log('Skipped — StreamNodeEvents not supported on this node'); + return; + } + await fn(); + }, + timeout + ); + + it('does not throw error on future unknown event type', () => { + if (!suiteAvailable) { + console.log('Skipped — regtest scheduler unavailable'); + return; + } + + const unknownEvent: NodeEvent = { eventType: 'new_future_event' as string, invoicePaid: undefined }; + + expect(() => { + switch (unknownEvent.eventType) { + case 'invoice_paid': break; + case 'unknown': + default: break; + } + }).not.toThrow(); + }); + + streamIt('streamNodeEvents returns a next method', async () => { + const stream = await node.streamNodeEvents(); + expect(stream).toBeDefined(); + expect(typeof stream.next).toBe('function'); + }); + + streamIt('next resolves to null or a well-formed NodeEvent within 2 seconds', async () => { + const stream: NodeEventStream = await node.streamNodeEvents(); + + // Race next() against a 2 s timeout — no live events is fine here. + const result = await Promise.race([ + stream.next(), + new Promise(resolve => setTimeout(() => resolve(null), 2_000)), + ]); + + if (result === null) return; // timed out, no events — acceptable + + expect(result).toHaveProperty('eventType'); + expect(typeof result.eventType).toBe('string'); + + if (result.eventType === 'invoice_paid') { + expect(result.invoicePaid).toBeDefined(); + expect(Buffer.isBuffer(result.invoicePaid!.paymentHash)).toBe(true); + expect(Buffer.isBuffer(result.invoicePaid!.preimage)).toBe(true); + expect(typeof result.invoicePaid!.amountMsat).toBe('number'); + } + }); + + streamIt('next returns null after the node is stopped', async () => { + const stream: NodeEventStream = await node.streamNodeEvents(); + await node.stop(); + + const result = await stream.next(); + expect(result).toBeNull(); + node = new Node(credentials); + }); + + // -------------------------------------------------------------------------- + // invoice_paid round-trip + // -------------------------------------------------------------------------- + + streamIt('receives real invoice_paid event', async () => { + const stream: NodeEventStream = await node.streamNodeEvents(); + const label = `jest-${Date.now()}`; + const invoice = await node.receive(label, 'jest event stream test', 1_000); + expect(invoice.bolt11).toMatch(/^ln/i); + + let paid: NodeEvent | null = null; + const deadline = Date.now() + 10_000; + + while (Date.now() < deadline) { + const event = await Promise.race([ + stream.next(), + new Promise(resolve => + setTimeout(() => resolve(null), deadline - Date.now()) + ), + ]); + + if (event === null) break; + if (event.eventType === 'invoice_paid') { paid = event; break; } + } + + expect(paid).not.toBeNull(); + expect(paid!.eventType).toBe('invoice_paid'); + + const p = paid!.invoicePaid!; + expect(p).toBeDefined(); + expect(Buffer.isBuffer(p.paymentHash)).toBe(true); + expect(p.paymentHash.length).toBeGreaterThan(0); + expect(Buffer.isBuffer(p.preimage)).toBe(true); + expect(p.preimage.length).toBeGreaterThan(0); + expect(p.bolt11).toBe(invoice.bolt11); + expect(p.label).toBe(label); + expect(typeof p.amountMsat).toBe('number'); + expect(p.amountMsat).toBeGreaterThan(0); + }, + 15_000 // extended timeout for payment round-trip + ); + +}); diff --git a/libs/gl-sdk-napi/tests/misc.spec.ts b/libs/gl-sdk-napi/tests/misc.spec.ts new file mode 100644 index 000000000..5f4b7cf23 --- /dev/null +++ b/libs/gl-sdk-napi/tests/misc.spec.ts @@ -0,0 +1,90 @@ +import { describe, it, expect, beforeAll } from '@jest/globals'; +import { Credentials, Scheduler, Signer } from '../index.js'; + +const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; + +describe('Credentials', () => { + it('can save and load raw credentials', async () => { + const original = await Credentials.load(Buffer.from('test')); + const raw = await original.save(); + + expect(Buffer.isBuffer(raw)).toBe(true); + + const restored = await Credentials.load(raw); + const raw2 = await restored.save(); + + expect(raw2.equals(raw)).toBe(true); + }); +}); + +describe('Signer', () => { + it('can be constructed from a mnemonic', async () => { + const signer = new Signer(MNEMONIC); + expect(signer).toBeTruthy(); + }); + + it('can return a node id', async () => { + const signer = new Signer(MNEMONIC); + const nodeId = signer.nodeId(); + + expect(Buffer.isBuffer(nodeId)).toBe(true); + expect(nodeId.length).toBeGreaterThan(0); + }); + + it('returns consistent node id for same mnemonic', async () => { + const signer1 = new Signer(MNEMONIC); + const signer2 = new Signer(MNEMONIC); + + const nodeId1 = signer1.nodeId(); + const nodeId2 = signer2.nodeId(); + + expect(nodeId1.equals(nodeId2)).toBe(true); + }); + + it('can be constructed with different mnemonics', async () => { + const mnemonic2 = 'legal winner thank year wave sausage worth useful legal winner thank yellow'; + const signer = new Signer(mnemonic2); + expect(signer).toBeTruthy(); + + const nodeId = signer.nodeId(); + expect(Buffer.isBuffer(nodeId)).toBe(true); + }); +}); + +describe('Scheduler', () => { + it('can be constructed for regtest', async () => { + const scheduler = new Scheduler('regtest'); + expect(scheduler).toBeTruthy(); + }); + + it('can be constructed for bitcoin mainnet', async () => { + const scheduler = new Scheduler('bitcoin'); + expect(scheduler).toBeTruthy(); + }); +}); + +describe('Integration: scheduler and signer', () => { + let scheduler: Scheduler; + let signer: Signer; + + beforeAll(() => { + scheduler = new Scheduler('regtest'); + signer = new Signer(MNEMONIC); + }); + + it('can recover credentials', async () => { + const recovered = await scheduler.recover(signer); + expect(recovered).toBeInstanceOf(Credentials); + expect((await recovered.save()).length).toBeGreaterThan(0); + }); + + it('handles registration of existing node (falls back to recovery)', async () => { + try { + const credentials = await scheduler.register(signer, ''); + expect(credentials).toBeInstanceOf(Credentials); + } catch (e) { + const recovered = await scheduler.recover(signer); + expect(recovered).toBeInstanceOf(Credentials); + } + }); +}); diff --git a/libs/gl-sdk-napi/tests/node.spec.ts b/libs/gl-sdk-napi/tests/node.spec.ts new file mode 100644 index 000000000..51a6ef5bb --- /dev/null +++ b/libs/gl-sdk-napi/tests/node.spec.ts @@ -0,0 +1,233 @@ +import { describe, it, expect, beforeAll, afterEach } from '@jest/globals'; +import { Credentials, Scheduler, Signer, Node } from '../index.js'; + +const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; + +// Helper to detect expected infrastructure-missing errors so tests skip +// gracefully instead of failing till the regtest environment is not fully set up. +function isInfraError(e: any): string | null { + const msg: string = e?.message ?? String(e); + if ( + msg.includes('NotFound') || + msg.includes('LSPS2') || + msg.includes('LSP') || + msg.includes('Unavailable') || + msg.includes('fatal alert') || + msg.includes('Could not afford') || + msg.includes('do not have sufficient outgoing balance') + ) { + const inner = msg.match(/message: \\"([^\\]+)\\"/)?.[1] + ?? msg.match(/message: "([^"]+)"/)?.[1] + ?? msg; + return inner; + } + return null; +} + +describe('Node', () => { + let node: Node; + let credentials: Credentials; + + beforeAll(async () => { + const scheduler = new Scheduler('regtest'); + const signer = new Signer(MNEMONIC); + credentials = await scheduler.recover(signer); + node = new Node(credentials); + }); + + afterAll(async () => { + if (node) { + await node.stop(); + } + }); + + it('can be constructed from credentials', async () => { + expect(node).toBeTruthy(); + }); + + describe('calls getInfo', () => { + it('returns node information with expected fields', async () => { + const info = await node.getInfo(); + + // Verify response structure + expect(info).toBeTruthy(); + expect(Buffer.isBuffer(info.id)).toBe(true); + expect(info.id.length).toBeGreaterThan(0); + expect(Buffer.isBuffer(info.color)).toBe(true); + expect(typeof info.numPeers).toBe('number'); + expect(typeof info.numPendingChannels).toBe('number'); + expect(typeof info.numActiveChannels).toBe('number'); + expect(typeof info.numInactiveChannels).toBe('number'); + expect(typeof info.version).toBe('string'); + expect(typeof info.lightningDir).toBe('string'); + expect(typeof info.blockheight).toBe('number'); + expect(typeof info.network).toBe('string'); + expect(typeof info.feesCollectedMsat).toBe('number'); + + // Verify response values + expect(info.id).toEqual(Buffer.from('03653e90c1ce4660fd8505dd6d643356e93cfe202af109d382787639dd5890e87d', 'hex')); + expect(info.color).toEqual(Buffer.from('03653e', 'hex')); + expect(info.numPeers).toBe(0); + expect(info.numPendingChannels).toBe(0); + expect(info.numActiveChannels).toBe(0); + expect(info.numInactiveChannels).toBe(0); + expect(info.lightningDir).toBe('/tmp/bitcoin'); + expect(info.network).toBe('bitcoin'); + expect(info.feesCollectedMsat).toBe(0); + + // Alias is optional + if (info.alias !== null && info.alias !== undefined) { + expect(typeof info.alias).toBe('string'); + expect(info.alias).toContain('PEEVEDGENESIS'); + } + }); + }); + + describe('calls listPeers', () => { + it('returns peer information with expected structure', async () => { + const response = await node.listPeers(); + + expect(response).toBeTruthy(); + expect(Array.isArray(response.peers)).toBe(true); + }); + }); + + describe('calls listPeerChannels', () => { + it('returns channel information with expected structure', async () => { + const response = await node.listPeerChannels(); + + expect(response).toBeTruthy(); + expect(Array.isArray(response.channels)).toBe(true); + }); + }); + + describe('calls listFunds', () => { + it('returns fund information with expected structure', async () => { + const response = await node.listFunds(); + + expect(response).toBeTruthy(); + expect(Array.isArray(response.outputs)).toBe(true); + expect(Array.isArray(response.channels)).toBe(true); + }); + }); + + describe('calls send', () => { + it('can attempt to send payment to valid invoice (Temporarily Skipped)', async () => { + try { + const sendResponse = await node.send('lnbcrt1m1p5hd6utsp5agu3y5gpheh3vf87ye5ungmlx3tnl308gw7vhle3qnwy3kfr7cqspp5ycvzfrqwc6wg73e2am5m79qn5wwee40qu7xs2ruukcs7jh5elu0qdp92fjkxetfwe5kueeqg9kk7atwwssrzvpsxqcrqxqyjw5qcqp2rzjqwm6pkr77u7ykj7zktj0857j6qhrgsh6uddrhgzgq5j7astuh6h9yqq9dyqqqqgqqqqqqqqpqqqqqzsqqc9qxpqysgqgutdtmzg8g5cmf33u3ayrx6vscd9xwwww5p3y9vhr9sflruwp84ys09uylzkcl32q2y279t5ky285sw3tv903jfa2y4m0gm6dqtv5ngp4j7l4r'); + expect(sendResponse).toBeTruthy(); + } catch (e: any) { + const skipReason = isInfraError(e); + if (skipReason !== null) { + console.warn(`Skipped — ${skipReason}`); + return; + } + throw e; + } + }); + + it('can send with explicit amount for zero-amount invoice (Temporarily Skipped)', async () => { + try { + const sendResponse = await node.send('lnbcrt1p5eufcmsp5kn4ajrqgyeazf94h9mr8mfdx8yx7dzjpetn9d3zrgklns4fjdt9spp5uetrsq93dwv0cwe392538a8rn6lkk4uv4ydp8yvw27ffehylcrdsdqltfjhymeqg9kk7atwwssyjmnkda5kxegxqyjw5qcqp2rzjqf6e53mdk9eldxu9r00kk3jhsq7cmu89f0rccjdp0ur4tpj5678wjqqyvyqqqqgqqqqqqqqpqqqqqzsqqc9qxpqysgqywxyku7z9s20h982ls86gnnl857q5y5nwswlrl472f2hcug889z8sze7zrtkm2knl50eyrtszk8fecvk8kz8773clhza2xpv2stqnqqqg2eez6', 5000); + expect(sendResponse).toBeTruthy(); + } catch (e: any) { + const skipReason = isInfraError(e); + if (skipReason !== null) { + console.warn(`Skipped — ${skipReason}`); + return; + } + throw e; + } + }); + }); + + describe('calls receive', () => { + it('can create invoice with amount (Temporarily Skipped)', async () => { + const label = `test-${Date.now()}`; + const description = 'Test payment'; + const amountMsat = 100000; + + try { + const response = await node.receive(label, description, amountMsat); + + expect(response).toBeTruthy(); + expect(typeof response.bolt11).toBe('string'); + expect(response.bolt11.length).toBeGreaterThan(0); + expect(response.bolt11.toLowerCase().startsWith('ln')).toBe(true); + } catch (e: any) { + const skipReason = isInfraError(e); + if (skipReason !== null) { + console.warn(`Skipped — ${skipReason}`); + return; + } + throw e; + } + }); + }); + + describe('calls onchainSend', () => { + it('can attempt to send specific amount on-chain (Temporarily Skipped)', async () => { + try { + const destAddress = (await node.onchainReceive()).bech32; + const response = await node.onchainSend(destAddress, '10000sat'); + expect(response).toBeTruthy(); + } catch (e: any) { + const skipReason = isInfraError(e); + if (skipReason !== null) { + console.warn(`Skipped — ${skipReason}`); + return; + } + throw e; + } + }); + + it('can attempt to send all funds on-chain (Temporarily Skipped)', async () => { + try { + const destAddress = (await node.onchainReceive()).bech32; + const response = await node.onchainSend(destAddress, 'all'); + expect(response).toBeTruthy(); + } catch (e: any) { + const skipReason = isInfraError(e); + if (skipReason !== null) { + console.warn(`Skipped — ${skipReason}`); + return; + } + throw e; + } + }); + + describe('calls onchainReceive', () => { + it('returns valid on-chain addresses', async () => { + const res = await node.onchainReceive(); + + expect(typeof res.bech32).toBe('string'); + expect(res.bech32.length).toBeGreaterThan(0); + expect(res.bech32.startsWith('bc1')).toBe(true); + + expect(typeof res.p2Tr).toBe('string'); + expect(res.p2Tr.length).toBeGreaterThan(0); + expect(res.p2Tr.startsWith('bc1p')).toBe(true); + }); + + it('generates different addresses on multiple calls', async () => { + const res1 = await node.onchainReceive(); + const res2 = await node.onchainReceive(); + + expect(res1.bech32).not.toBe(res2.bech32); + expect(res1.p2Tr).not.toBe(res2.p2Tr); + }); + }); + + }); + + describe('calls stop', () => { + it('can stop the node', async () => { + const testScheduler = new Scheduler('regtest'); + const testSigner = new Signer(MNEMONIC); + const testCredentials = await testScheduler.recover(testSigner); + const testNode = new Node(testCredentials); + + await expect(testNode.stop()).resolves.not.toThrow(); + }); + }); +}); diff --git a/libs/gl-sdk-napi/tsconfig.json b/libs/gl-sdk-napi/tsconfig.json index 9b2ca2dd3..429e16130 100644 --- a/libs/gl-sdk-napi/tsconfig.json +++ b/libs/gl-sdk-napi/tsconfig.json @@ -1,9 +1,12 @@ { "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "node20", + "moduleResolution": "node16", "target": "ES2020", "esModuleInterop": true, - "strict": true - } + "strict": true, + "isolatedModules": true, + "skipLibCheck": true + }, + "include": ["tests/**/*.ts", "*.ts"] }