Skip to content
Open
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
24 changes: 24 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,92 +1,84 @@
// Instrumented tests for parse_input() LNURL handling.
// Covers LNURL bech32 strings, lightning addresses, prefix handling,
// and error cases. Pure parsing only — no node, no network.
// Instrumented tests for parse_input() error-before-HTTP cases on
// LNURL / Lightning Address inputs and the LUD-04 tag=login fast path.
//
// Successful resolution of LNURL-pay / LNURL-withdraw requires a
// reachable LNURL service and is covered by gl-testing integration
// tests, not by Android instrumented tests.

package com.blockstream.glsdk

import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class LnurlParseTest {

// LNURL bech32 encoding of https://service.com/lnurl (LUD-01 example).
private val lnurlBech32 =
"LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2"
// 32 zero bytes — a syntactically valid k1.
private val zeroK1 =
"0000000000000000000000000000000000000000000000000000000000000000"

// ============================================================
// LNURL bech32 parsing
// ============================================================

@Test
fun parse_lnurl_bech32_uppercase() {
val result = parseInput(lnurlBech32)
assertTrue(
"Expected LnUrl, got $result",
result is InputType.LnUrl,
)
@Test(expected = Exception::class)
fun parse_invalid_lnurl_bech32_returns_error(): Unit = runBlocking {
parseInput("LNURL1INVALIDDATA")
}

@Test
fun parse_lnurl_bech32_lowercase() {
val result = parseInput(lnurlBech32.lowercase())
assertTrue(
"Expected LnUrl, got $result",
result is InputType.LnUrl,
)
@Test(expected = Exception::class)
fun parse_lightning_address_no_dot_in_domain_returns_error(): Unit = runBlocking {
parseInput("user@localhost")
}

@Test
fun parse_lnurl_with_lightning_prefix() {
val result = parseInput("lightning:$lnurlBech32")
assertTrue(
"Expected LnUrl, got $result",
result is InputType.LnUrl,
)
@Test(expected = Exception::class)
fun parse_lightning_address_empty_local_part_returns_error(): Unit = runBlocking {
parseInput("@example.com")
}

@Test(expected = Exception::class)
fun parse_invalid_lnurl_bech32_returns_error() {
parseInput("LNURL1INVALIDDATA")
fun parse_lightning_address_empty_domain_returns_error(): Unit = runBlocking {
parseInput("user@")
}

// ============================================================
// Lightning address parsing
// LUD-04 tag=login — classified offline (no HTTP fetch)
// ============================================================

@Test
fun parse_lightning_address_simple() {
val result = parseInput("user@example.com")
fun parse_lnurl_auth_url_classifies_as_lnurlauth() = runBlocking {
val url = "https://service.example.com/auth?tag=login&k1=$zeroK1"
val result = parseInput(url)
assertTrue(
"Expected LnUrlAddress, got $result",
result is InputType.LnUrlAddress,
"Expected LnUrlAuth, got $result",
result is InputType.LnUrlAuth,
)
assertEquals("user@example.com", (result as InputType.LnUrlAddress).address)
val data = (result as InputType.LnUrlAuth).data
assertEquals(zeroK1, data.k1)
assertEquals("service.example.com", data.domain)
assertNull(data.action)
assertEquals(url, data.url)
}

@Test
fun parse_lightning_address_with_symbols() {
val result = parseInput("sat.oshi-99@example.com")
assertTrue(
"Expected LnUrlAddress, got $result",
result is InputType.LnUrlAddress,
)
fun parse_lnurl_auth_url_captures_action() = runBlocking {
val url = "https://x.com/a?tag=login&k1=$zeroK1&action=register"
val result = parseInput(url)
assertTrue(result is InputType.LnUrlAuth)
assertEquals("register", (result as InputType.LnUrlAuth).data.action)
}

@Test(expected = Exception::class)
fun parse_lightning_address_no_dot_in_domain_returns_error() {
parseInput("user@localhost")
fun parse_lnurl_auth_rejects_missing_k1(): Unit = runBlocking {
parseInput("https://x.com/a?tag=login")
}

@Test(expected = Exception::class)
fun parse_lightning_address_empty_local_part_returns_error() {
parseInput("@example.com")
fun parse_lnurl_auth_rejects_short_k1(): Unit = runBlocking {
parseInput("https://x.com/a?tag=login&k1=deadbeef")
}

@Test(expected = Exception::class)
fun parse_lightning_address_empty_domain_returns_error() {
parseInput("user@")
fun parse_lnurl_auth_rejects_unknown_action(): Unit = runBlocking {
parseInput("https://x.com/a?tag=login&k1=$zeroK1&action=bogus")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// Instrumented tests for parse_input().
// Tests BOLT11 invoice parsing, node ID parsing, and error cases.
// Tests BOLT11 invoice parsing, node ID parsing, and error cases that
// resolve without HTTP. LNURL / Lightning Address paths are exercised
// in gl-testing integration tests against a live LNURL fixture.

package com.blockstream.glsdk

import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -27,18 +30,21 @@ class ParseInputTest {
// ============================================================

@Test
fun parse_valid_node_id() {
fun parse_valid_node_id() = runBlocking {
val result = parseInput(validNodeId)
assertNotNull(result)
assertTrue(
"Expected NodeId, got $result",
result is InputType.NodeId,
)
}

@Test(expected = Exception::class)
fun parse_invalid_hex_returns_error() {
fun parse_invalid_hex_returns_error(): Unit = runBlocking {
parseInput("not_valid_hex_at_all_but_66_chars_long_xxxxxxxxxxxxxxxxxxxxxxxxxxx")
}

@Test(expected = Exception::class)
fun parse_wrong_prefix_returns_error() {
fun parse_wrong_prefix_returns_error(): Unit = runBlocking {
parseInput("04eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")
}

Expand All @@ -47,34 +53,43 @@ class ParseInputTest {
// ============================================================

@Test
fun parse_valid_bolt11() {
fun parse_valid_bolt11() = runBlocking {
val result = parseInput(bolt11Invoice)
assertNotNull(result)
assertTrue(
"Expected Bolt11, got $result",
result is InputType.Bolt11,
)
}

@Test
fun parse_bolt11_with_lightning_prefix() {
fun parse_bolt11_with_lightning_prefix() = runBlocking {
val result = parseInput("lightning:$bolt11Invoice")
assertNotNull(result)
assertTrue(
"Expected Bolt11, got $result",
result is InputType.Bolt11,
)
}

@Test
fun parse_bolt11_with_uppercase_prefix() {
fun parse_bolt11_with_uppercase_prefix() = runBlocking {
val result = parseInput("LIGHTNING:$bolt11Invoice")
assertNotNull(result)
assertTrue(
"Expected Bolt11, got $result",
result is InputType.Bolt11,
)
}

// ============================================================
// Error cases
// ============================================================

@Test(expected = Exception::class)
fun parse_empty_string_returns_error() {
fun parse_empty_string_returns_error(): Unit = runBlocking {
parseInput("")
}

@Test(expected = Exception::class)
fun parse_garbage_returns_error() {
fun parse_garbage_returns_error(): Unit = runBlocking {
parseInput("hello world")
}
}
40 changes: 9 additions & 31 deletions libs/gl-sdk-napi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,46 +86,24 @@ gl-sdk-napi/
**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, NodeEvent, NodeEventStream } from '@greenlightcln/glsdk';
import { Config, Node, NodeEvent, NodeEventStream, registerOrRecover } from '@greenlightcln/glsdk';

type Network = 'bitcoin' | 'regtest';

class GreenlightApp {
private credentials: Credentials | null = null;
private scheduler: Scheduler;
private signer: Signer;
private config: Config;
private mnemonic: string;
private node: Node | null = null;

constructor(phrase: string, network: Network) {
this.scheduler = new Scheduler(network);
this.signer = new Signer(phrase);
console.log(`✓ Signer created. Node ID: ${this.signer.nodeId().toString('hex')}`);
this.config = new Config().withNetwork(network);
this.mnemonic = phrase;
}

async registerOrRecover(inviteCode?: string): Promise<void> {
try {
console.log('Attempting to register node...');
this.credentials = await this.scheduler.register(this.signer, inviteCode || '');
console.log('✓ Node registered successfully');
} catch (e: any) {
const match = e.message.match(/message: "([^"]+)"/);
console.error(`✗ Registration failed: ${match ? match[1] : e.message}`);
console.log('Attempting recovery...');
try {
this.credentials = await this.scheduler.recover(this.signer);
console.log('✓ Node recovered successfully');
} catch (recoverError) {
console.error('✗ Recovery failed:', recoverError);
throw recoverError;
}
}
}

createNode(): Node {
if (!this.credentials) { throw new Error('Must register/recover before creating node'); }
console.log('Creating node instance...');
this.node = new Node(this.credentials);
console.log('✓ Node created');
async registerOrRecover(inviteCode?: string): Promise<Node> {
console.log('Attempting register-or-recover...');
this.node = await registerOrRecover(this.mnemonic, inviteCode, this.config);
console.log('✓ Node ready');
return this.node;
}

Expand Down
4 changes: 2 additions & 2 deletions libs/gl-sdk-napi/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading