Skip to content
Merged
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: 2 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
@@ -0,0 +1,175 @@
// Instrumented tests for the high-level auth API.
// Tests Config, register, recover, connect, and register_or_recover
// against the Greenlight production scheduler.

package com.blockstream.glsdk

import android.system.Os
import androidx.test.ext.junit.runners.AndroidJUnit4
import okio.ByteString.Companion.decodeBase64
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

@RunWith(AndroidJUnit4::class)
class AuthApiTest {

@Before
fun setup() {
Os.setenv("RUST_LOG", "trace", true)
}

// BIP39 test vector — not a real wallet
private val testMnemonic =
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"

// ============================================================
// Config
// ============================================================

@Test
fun config_default() {
val config = Config()
assertNotNull(config)
}

@Test
fun config_with_network() {
val config = Config().withNetwork(Network.REGTEST)
assertNotNull(config)
}

@Test
fun config_with_developer_cert() {
val cert = DeveloperCert("fake-cert".toByteArray(), "fake-key".toByteArray())
val config = Config().withDeveloperCert(cert)
assertNotNull(config)
}

@Test
fun config_builder_chaining() {
val cert = DeveloperCert("fake-cert".toByteArray(), "fake-key".toByteArray())
val config = Config()
.withDeveloperCert(cert)
.withNetwork(Network.REGTEST)
assertNotNull(config)
}

// ============================================================
// Bad mnemonic
// ============================================================

@Test(expected = Exception.PhraseCorrupted::class)
fun register_bad_mnemonic() {
val config = Config()
register("not a valid mnemonic", null, config)
}

@Test(expected = Exception.PhraseCorrupted::class)
fun recover_bad_mnemonic() {
val config = Config()
recover("not a valid mnemonic", config)
}

@Test(expected = Exception.PhraseCorrupted::class)
fun connect_bad_mnemonic() {
val config = Config()
connect("not a valid mnemonic", "fake-creds".toByteArray(), config)
}

// ============================================================
// Register and connect flow
// ============================================================

@OptIn(ExperimentalUuidApi::class)
@Test
fun register_or_recover_returns_node() {
val config = Config()
val node = registerOrRecover(testMnemonic, null, config)
assertNotNull(node)
node.use { n ->
val creds = n.credentials()
assertNotNull(creds)
assertTrue("Credentials should not be empty", creds.isNotEmpty())
}
}

@OptIn(ExperimentalUuidApi::class)
@Test
fun credentials_roundtrip_via_connect() {
val config = Config()

// Register or recover to get credentials
val savedCreds: ByteArray
registerOrRecover(testMnemonic, null, config).use { node ->
savedCreds = node.credentials()
}

// Connect with the saved credentials
connect(testMnemonic, savedCreds, config).use { node ->
assertNotNull(node)
val reconnectedCreds = node.credentials()
assertTrue("Reconnected credentials should not be empty", reconnectedCreds.isNotEmpty())
}
}

// ============================================================
// Disconnect
// ============================================================

@Test
fun disconnect_is_idempotent() {
val config = Config()

val node = registerOrRecover(testMnemonic, null, config)
// First disconnect
node.disconnect()
// Second disconnect should not throw
node.disconnect()
}

// ============================================================
// Node operations
// ============================================================

@OptIn(ExperimentalUuidApi::class)
@Test
fun register_or_recover_and_create_invoice() {
val config = Config()
registerOrRecover(testMnemonic, null, config).use { node ->
val addrResponse = node.onchainReceive()
assertNotNull(addrResponse)
println("Deposit funds to: $addrResponse")

val invoice = node.receive(
label = Uuid.random().toString(),
description = "Coffee",
amountMsat = 20_000_000uL
)
assertNotNull(invoice)
assertTrue("Invoice bolt11 should not be empty", invoice.bolt11.isNotEmpty())
println("Lightning Invoice: $invoice")
}
}

// ============================================================
// Low-level API still works
// ============================================================

@Test
fun low_level_signer_still_available() {
val signer = Signer(testMnemonic)
assertNotNull(signer)
val nodeId = signer.nodeId()
assertTrue("Node ID should not be empty", nodeId.isNotEmpty())
}

@Test
fun low_level_scheduler_still_available() {
val scheduler = Scheduler(Network.BITCOIN)
assertNotNull(scheduler)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.blockstream.glsdk

import android.system.Os
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

@RunWith(AndroidJUnit4::class)
class NodeOperationsTest {

@Before
fun setup() {
Os.setenv("RUST_LOG", "trace", true)
}

// BIP39 test vector — not a real wallet
private val testMnemonic =
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"

@OptIn(ExperimentalUuidApi::class)
@Test
fun test_onchain_receive_and_invoice() {
val config = Config()

val node = registerOrRecover(mnemonic = testMnemonic, inviteCode = null, config = config)

node.use { n ->
// Get an on-chain address to fund the node
val addrResponse = n.onchainReceive()
println("Deposit funds to: ${addrResponse.toString()}")

// Create a lightning invoice for 1000 sats (1,000,000 msats)
val invoice = n.receive(
label = Uuid.random().toString(),
description = "Coffee",
amountMsat = 20_000_000uL
)
println("Lightning Invoice: ${invoice.toString()}")
}
}
}
4 changes: 2 additions & 2 deletions libs/gl-sdk-napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ pub struct Handle {

#[napi]
pub struct Node {
inner: GlNode,
inner: std::sync::Arc<GlNode>,
}

#[napi]
Expand Down Expand Up @@ -448,7 +448,7 @@ impl Node {
let inner =
GlNode::new(&credentials.inner).map_err(|e| Error::from_reason(e.to_string()))?;

Ok(Self { inner })
Ok(Self { inner: std::sync::Arc::new(inner) })
}

/// Stop the node if it is currently running
Expand Down
1 change: 0 additions & 1 deletion libs/gl-sdk/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ All bindings are generated into `bindings/` directory.

## Important Notes

- **Workspace structure:** This is a workspace member. The workspace root is at `/home/cdecker/dev/greenlight/202509-sdk/public/`
- **Task commands:** All `task sdk:*` commands work from any directory in the workspace
- **UniFFI workflow:** Build library first, then generate bindings. UniFFI reads the compiled library to generate foreign code.
- **Working directory:** When using cargo directly, commands should be run from the workspace root with `-p gl-sdk`
Expand Down
2 changes: 2 additions & 0 deletions libs/gl-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ crate-type = ["cdylib", "staticlib", "rlib"]
name = "glsdk"

[dependencies]
anyhow = "1"
bip39 = "2.2.0"
gl-client = { version = "0.3.3", path = "../gl-client" }
hex = "0.4"
once_cell = "1.21.3"
thiserror = "2.0.17"
tokio = { version = "1", features = ["sync"] }
Expand Down
52 changes: 52 additions & 0 deletions libs/gl-sdk/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SDK configuration for Greenlight node operations.
// Holds network selection and optional developer certificate.

use std::sync::Arc;
use crate::credentials::DeveloperCert;
use crate::Network;

#[derive(uniffi::Object, Clone)]
pub struct Config {
pub(crate) network: gl_client::bitcoin::Network,
pub(crate) developer_cert: Option<gl_client::credentials::Nobody>,
}

impl Config {
/// Resolve the credentials to use for unauthenticated scheduler
/// calls (register, recover). Uses the developer certificate if
/// one was provided, otherwise falls back to the compiled-in default.
pub(crate) fn nobody(&self) -> gl_client::credentials::Nobody {
self.developer_cert
.clone()
.unwrap_or_else(gl_client::credentials::Nobody::new)
}
}

#[uniffi::export]
impl Config {
/// Create a Config with default settings: BITCOIN network, no developer certificate.
#[uniffi::constructor()]
pub fn new() -> Self {
Self {
network: gl_client::bitcoin::Network::Bitcoin,
developer_cert: None,
}
}

/// Return a new Config with the given developer certificate.
/// Nodes registered through this config will be associated with the developer's account.
pub fn with_developer_cert(&self, cert: &DeveloperCert) -> Arc<Config> {
Arc::new(Config {
developer_cert: Some(cert.inner.clone()),
..self.clone()
})
}

/// Return a new Config with the given network.
pub fn with_network(&self, network: Network) -> Arc<Config> {
Arc::new(Config {
network: network.into(),
..self.clone()
})
}
}
Loading
Loading