Skip to content
Draft
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
2 changes: 1 addition & 1 deletion libs/gl-sdk-napi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ npm test -- --verbose tests/node.spec.ts
The global setup script `./tests/jest.globalSetup.ts` automatically:
- Starts `test_setup.py`, spinning up bitcoind, the Greenlight scheduler, and an LSPS2-compatible LSP node
- Waits for all services to be ready
- Injects the required environment variables (`GL_SCHEDULER_GRPC_URI`, `GL_CA_CRT`, `GL_NOBODY_CRT`, `GL_NOBODY_KEY`, `LSP_RPC_SOCKET`, `LSP_NODE_ID`, `LSP_PROMISE_SECRET`, `GL_FUND_SERVER`)
- Injects the required environment variables (`GL_SCHEDULER_GRPC_URI`, `GL_CA_CRT`, `GL_NOBODY_CRT`, `GL_NOBODY_KEY`, `LSP_RPC_SOCKET`, `LSP_NODE_ID`, `TEST_SETUP_SERVER_URL`)
- Shuts everything down after all tests complete

### Test Helpers
Expand Down
14 changes: 6 additions & 8 deletions libs/gl-sdk-napi/tests/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,19 @@ import * as bip39 from 'bip39';
import { Credentials, Scheduler, Signer, Node } from '../index.js';

describe('Greenlight node', () => {
let node: Node;
let credentials: Credentials;

it('can be setup', async () => {
const scheduler = new Scheduler('regtest');
const rand: Buffer = crypto.randomBytes(16);
const MNEMONIC: string = bip39.entropyToMnemonic(rand.toString("hex"));
const scheduler = new Scheduler('regtest');
const signer = new Signer(MNEMONIC);
const handle = await signer.start();
const nodeId = signer.nodeId();

expect(Buffer.isBuffer(nodeId)).toBe(true);
expect(nodeId.length).toBeGreaterThan(0);

credentials = await scheduler.register(signer);
node = new Node(credentials);
const credentials = await scheduler.register(signer);
const node = new Node(credentials);
expect(node).toBeTruthy();
handle.stop();
await node.stop();
});
});
43 changes: 20 additions & 23 deletions libs/gl-sdk-napi/tests/eventstream.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as crypto from 'crypto';
import * as bip39 from 'bip39';
import { Credentials, Scheduler, Signer, Node, NodeEventStream, NodeEvent, InvoicePaidEvent } from '../index.js';
import { startLspServer, stopLspServer, fundNode } from './test.helper.js';
import { Credentials, Scheduler, Signer, Node, NodeEventStream, NodeEvent, InvoicePaidEvent, Handle } from '../index.js';
import { fundWallet, getGLNode } from './test.helper.js';

describe('NodeEvent (type contract)', () => {
it('NodeEvent and InvoicePaidEvent are assignable from NAPI-generated types', () => {
Expand All @@ -28,16 +28,12 @@ describe('NodeEvent (type contract)', () => {
// ============================================================================

describe('NodeEventStream (integration)', () => {
let credentials: Credentials;
let scheduler: Scheduler = new Scheduler('regtest');
let node: Node;
let handle: Handle;

beforeAll(async () => {
const rand: Buffer = crypto.randomBytes(16);
const MNEMONIC: string = bip39.entropyToMnemonic(rand.toString("hex"));
const scheduler = new Scheduler('regtest');
const signer = new Signer(MNEMONIC);
credentials = await scheduler.register(signer);
node = new Node(credentials);
({node, handle } = await getGLNode(scheduler, false) as { node: Node; handle: Handle });
try {
const probe = await node.streamNodeEvents();
void probe;
Expand All @@ -49,6 +45,7 @@ describe('NodeEventStream (integration)', () => {

afterAll(async () => {
if (node) {
handle.stop();
await node.stop();
}
});
Expand Down Expand Up @@ -94,25 +91,24 @@ describe('NodeEventStream (integration)', () => {
});

it('next returns null after the node is stopped', async () => {
const stream: NodeEventStream = await node.streamNodeEvents();
await node.stop();

const mnemonic2 = bip39.entropyToMnemonic(crypto.randomBytes(16).toString('hex'));
const signer2 = new Signer(mnemonic2);
const handle2 = await signer2.start();
const credentials2 = await scheduler.register(signer2);
let node2 = new Node(credentials2);
const stream: NodeEventStream = await node2.streamNodeEvents();
await node2.stop();
const result = await stream.next();
expect(result).toBeNull();
node = new Node(credentials);
node2 = new Node(credentials2);
handle2.stop()
await node2.stop();
});

it.skip('receives real invoice_paid event', async () => {
await startLspServer();
await fundNode(node, 0.5);
await fundWallet(node, 500_000_000);
const { node: node2, handle: handle2 } = await getGLNode(scheduler, true) as { node: Node; handle: Handle };
const stream: NodeEventStream = await node.streamNodeEvents();
const rand2: Buffer = crypto.randomBytes(16);
const MNEMONIC2: string = bip39.entropyToMnemonic(rand2.toString("hex"));
const scheduler2 = new Scheduler('regtest');
const signer2 = new Signer(MNEMONIC2);
const credentials2 = await scheduler2.register(signer2);
const node2 = new Node(credentials2);

const label = `jest-${Date.now()}`;
const receiveRes = await node.receive(label, 'jest event stream test', 1_000);
const sendResponse = await node2.send(receiveRes.bolt11);
Expand Down Expand Up @@ -146,7 +142,8 @@ describe('NodeEventStream (integration)', () => {
expect(p.label).toBe(label);
expect(typeof p.amountMsat).toBe('number');
expect(p.amountMsat).toBeGreaterThan(0);
await stopLspServer();
handle2.stop();
await node2.stop();
},
15_000 // extended timeout for payment round-trip
);
Expand Down
3 changes: 1 addition & 2 deletions libs/gl-sdk-napi/tests/jest.globalSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ async function waitForEnvFile(): Promise<Record<string, string>> {
'GL_NOBODY_KEY',
'LSP_RPC_SOCKET',
'LSP_NODE_ID',
'LSP_PROMISE_SECRET',
'GL_FUND_SERVER',
'TEST_SETUP_SERVER_URL'
];

const deadline = Date.now() + SERVER_READY_TIMEOUT_MS;
Expand Down
17 changes: 16 additions & 1 deletion libs/gl-sdk-napi/tests/jest.globalTeardown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as path from 'path';

const GLTESTS_DIR = '/tmp/gltests';
const PID_FILE = path.join(GLTESTS_DIR, 'gltestserver.pid');
const ENV_FILE = path.join(GLTESTS_DIR, '.env');

export default async function globalTeardown(): Promise<void> {
console.log(`\n🛑 Stopping test_setup...`);
Expand All @@ -29,9 +30,23 @@ export default async function globalTeardown(): Promise<void> {
if (fs.existsSync(PID_FILE)) {
fs.unlinkSync(PID_FILE);
}

// Clean up TMP_DIR — derived from GL_CERT_PATH in the .env file
if (fs.existsSync(ENV_FILE)) {
const envContents = fs.readFileSync(ENV_FILE, 'utf8');
const certPathMatch = envContents.match(/^GL_CERT_PATH=(.+)$/m);
if (certPathMatch) {
const tmpDir = path.dirname(certPathMatch[1].trim());
if (tmpDir && tmpDir !== '/' && tmpDir.includes('gl-lsp-setup-')) {
fs.rmSync(tmpDir, { recursive: true, force: true });
console.log(` Removed TMP_DIR: ${tmpDir}`);
}
}
fs.unlinkSync(ENV_FILE);
}
} catch {
// Ignore all teardown errors
}

console.log(`✅ test_setup stopped.`);
}
}
94 changes: 26 additions & 68 deletions libs/gl-sdk-napi/tests/node.spec.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import * as crypto from 'crypto';
import * as bip39 from 'bip39';
import { Credentials, Scheduler, Signer, Node } from '../index.js';
import { fundWallet, lspInfo } from './test.helper';
import { Handle, Node, Scheduler } from '../index.js';
import { getGLNode, fundWallet, getLspInvoice } from './test.helper';

describe('Node', () => {
let scheduler: Scheduler = new Scheduler('regtest');
let glNodes: Array<{ node: Node; handle: Handle }> = [];
let node: Node;
let credentials: Credentials;

beforeEach(async () => {
const rand: Buffer = crypto.randomBytes(16);
const MNEMONIC: string = bip39.entropyToMnemonic(rand.toString("hex"));
const scheduler = new Scheduler('regtest');
const signer = new Signer(MNEMONIC);
credentials = await scheduler.register(signer);
node = new Node(credentials);
glNodes.push(await getGLNode(scheduler, false));
node = glNodes[0].node;
});

afterEach(async () => {
if (node) {
await node.stop();
for (const { node: n, handle: h } of glNodes) {
h.stop();
await n.stop();
}
glNodes = [];
});

it('can be constructed from credentials', async () => {
Expand Down Expand Up @@ -111,45 +108,33 @@ describe('Node', () => {
});

describe('calls onchainSend', () => {
it.skip('can attempt to send specific amount on-chain', async () => {
it('can send specific amount on-chain', async () => {
await fundWallet(node, 500_000_000);
const rand2: Buffer = crypto.randomBytes(16);
const MNEMONIC2: string = bip39.entropyToMnemonic(rand2.toString("hex"));
const scheduler2 = new Scheduler('regtest');
const signer2 = new Signer(MNEMONIC2);
const credentials2 = await scheduler2.register(signer2);
const node2 = new Node(credentials2);
const destAddress = (await node2.onchainReceive()).bech32;
const extraGLNode = await getGLNode(scheduler, true) as { node: Node; handle: Handle };
glNodes.push(extraGLNode);
const destAddress = (await extraGLNode.node.onchainReceive()).bech32;
const response = await node.onchainSend(destAddress, '10000sat');
expect(response).toBeTruthy();
});

it.skip('can attempt to send all funds on-chain', async () => {
it('can attempt to send all funds on-chain', async () => {
await fundWallet(node, 500_000_000);
const rand2: Buffer = crypto.randomBytes(16);
const MNEMONIC2: string = bip39.entropyToMnemonic(rand2.toString("hex"));
const scheduler2 = new Scheduler('regtest');
const signer2 = new Signer(MNEMONIC2);
const credentials2 = await scheduler2.register(signer2);
const node2 = new Node(credentials2);
const destAddress = (await node2.onchainReceive()).bech32;
const extraGLNode = await getGLNode(scheduler, true) as { node: Node; handle: Handle };
glNodes.push(extraGLNode);
const destAddress = (await extraGLNode.node.onchainReceive()).bech32;
const response = await node.onchainSend(destAddress, 'all');
expect(response).toBeTruthy();
});
});

describe('calls receive', () => {
it.skip('can create invoice with amount', async () => {
const { rpcSocket, nodeId } = lspInfo();
// Connect to the LSP as a peer
console.log('LSP Node Info:', rpcSocket, nodeId);
// await node.connectPeer(lspNodeInfo.id, lspNodeInfo.bindings[0].address, lspNodeInfo.bindings[0].port);
// await new Promise(resolve => setTimeout(resolve, 2000));

it('can create invoice with amount', async () => {
const extraGLNode = await getGLNode(scheduler, true) as { node: Node; handle: Handle };
glNodes.push(extraGLNode);
const label = `test-${Date.now()}`;
const description = 'Test payment';
const amountMsat = 100000;
const response = await node.receive(label, description, amountMsat);
const response = await extraGLNode.node.receive(label, description, amountMsat);
expect(response).toBeTruthy();
expect(typeof response.bolt11).toBe('string');
expect(response.bolt11.length).toBeGreaterThan(0);
Expand All @@ -159,44 +144,17 @@ describe('Node', () => {

describe('calls send', () => {
it.skip('can attempt to send payment to valid invoice', async () => {
const { rpcSocket, nodeId } = lspInfo();
await fundWallet(node, 500_000_000);
const rand2: Buffer = crypto.randomBytes(16);
const MNEMONIC2: string = bip39.entropyToMnemonic(rand2.toString("hex"));
const scheduler2 = new Scheduler('regtest');
const signer2 = new Signer(MNEMONIC2);
const credentials2 = await scheduler2.register(signer2);
const node2 = new Node(credentials2);
const receiveRes = await node2.receive(`test-${Date.now()}`, 'Test payment', 100000);
const sendResponse = await node.send(receiveRes.bolt11);
const bolt11 = await getLspInvoice(100_000);
const sendResponse = await node.send(bolt11);
expect(sendResponse).toBeTruthy();
});

it.skip('can send with explicit amount for zero-amount invoice', async () => {
const { rpcSocket, nodeId } = lspInfo();
await fundWallet(node, 500_000_000);
const rand2: Buffer = crypto.randomBytes(16);
const MNEMONIC2: string = bip39.entropyToMnemonic(rand2.toString("hex"));
const scheduler2 = new Scheduler('regtest');
const signer2 = new Signer(MNEMONIC2);
const credentials2 = await scheduler2.register(signer2);
const node2 = new Node(credentials2);
const receiveRes = await node2.receive(`test-${Date.now()}`, 'Test payment');
const sendResponse = await node.send(receiveRes.bolt11);
const bolt11 = await getLspInvoice(0);
const sendResponse = await node.send(bolt11, 200_000);
expect(sendResponse).toBeTruthy();
});
});

describe('calls stop', () => {
it('can stop the node', async () => {
const rand: Buffer = crypto.randomBytes(16);
const MNEMONIC: string = bip39.entropyToMnemonic(rand.toString("hex"));
const testScheduler = new Scheduler('regtest');
const testSigner = new Signer(MNEMONIC);
const testCredentials = await testScheduler.register(testSigner);
const testNode = new Node(testCredentials);

await expect(testNode.stop()).resolves.not.toThrow();
});
});
});
47 changes: 44 additions & 3 deletions libs/gl-sdk-napi/tests/test.helper.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { Node } from '../index.js';
import * as fs from 'fs';
import * as crypto from 'crypto';
import * as bip39 from 'bip39';
import { Credentials, Scheduler, Signer, Node, Handle } from '../index.js';

export const lspInfo = () => ({
rpcSocket: process.env.LSP_RPC_SOCKET!,
nodeId: process.env.LSP_NODE_ID!,
promiseSecret: process.env.LSP_PROMISE_SECRET!,
});

export async function fundWallet(node: Node, amountSats = 100_000_000): Promise<boolean> {
const testSetupServerUrl = process.env.TEST_SETUP_SERVER_URL!;
if (!testSetupServerUrl) throw new Error('TEST_SETUP_SERVER_URL not set');

const address = (await node.onchainReceive()).bech32;
const res = await fetch(process.env.GL_FUND_SERVER!, {
const res = await fetch(`${testSetupServerUrl}/fund-wallet`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, amount: amountSats }),
Expand All @@ -29,3 +34,39 @@ export async function fundWallet(node: Node, amountSats = 100_000_000): Promise<

throw new Error('fundNode timed out waiting for node to detect funds');
}

export async function getGLNode(scheduler: Scheduler, connectToLSP: boolean = true): Promise<{ node: Node; handle: Handle }> {
const mnemonic = bip39.entropyToMnemonic(crypto.randomBytes(16).toString('hex'));
const secret = bip39.mnemonicToSeedSync(mnemonic);
const signer = new Signer(mnemonic);
let credentials: Credentials;
if (connectToLSP) {
const testSetupServerUrl = process.env.TEST_SETUP_SERVER_URL!;
if (!testSetupServerUrl) throw new Error('TEST_SETUP_SERVER_URL not set');

const res = await fetch(`${testSetupServerUrl}/connect-to-lsp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ secret: secret.toString('hex') }),
});
if (!res.ok) throw new Error(`Failed to connect node to LSP: ${await res.text()}`);
const { creds_path } = await res.json();
credentials = await Credentials.load(fs.readFileSync(creds_path));
} else {
credentials = await scheduler.register(signer);
}
return { node: new Node(credentials), handle: await signer.start() };
}

export async function getLspInvoice(amountMsat: number = 0): Promise<string> {
const testSetupServerUrl = process.env.TEST_SETUP_SERVER_URL!;
if (!testSetupServerUrl) throw new Error('TEST_SETUP_SERVER_URL not set');
const res = await fetch(`${testSetupServerUrl}/lsp-invoice`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount_msat: amountMsat, label: `test-${Date.now()}`, description: 'Test payment' }),
});
if (!res.ok) throw new Error(`Failed to get LSP invoice: ${await res.text()}`);
const { bolt11 } = await res.json();
return bolt11;
}
Loading
Loading