Skip to main content

glsdk/
lib.rs

1use std::collections::HashMap;
2
3uniffi::setup_scaffolding!();
4
5#[derive(uniffi::Error, thiserror::Error, Debug)]
6pub enum Error {
7    #[error("{msg}")]
8    DuplicateNode {
9        code: i32,
10        msg: String,
11        values: HashMap<String, String>,
12    },
13
14    #[error("{msg}")]
15    NoSuchNode {
16        code: i32,
17        msg: String,
18        values: HashMap<String, String>,
19    },
20
21    #[error("{msg}")]
22    UnparseableCreds {
23        code: i32,
24        msg: String,
25        values: HashMap<String, String>,
26    },
27
28    #[error("{msg}")]
29    PhraseCorrupted {
30        code: i32,
31        msg: String,
32        values: HashMap<String, String>,
33    },
34
35    #[error("{msg}")]
36    Rpc {
37        code: i32,
38        msg: String,
39        values: HashMap<String, String>,
40    },
41
42    #[error("{msg}")]
43    Argument {
44        code: i32,
45        msg: String,
46        values: HashMap<String, String>,
47    },
48
49    #[error("{msg}")]
50    Other {
51        code: i32,
52        msg: String,
53        values: HashMap<String, String>,
54    },
55}
56
57impl Error {
58    pub fn duplicate_node(node_id: impl Into<String>) -> Self {
59        let node_id = node_id.into();
60        Error::DuplicateNode {
61            code: 1000,
62            msg: format!(
63                "There is already a node for node_id={node_id}, maybe you want to recover?"
64            ),
65            values: HashMap::from([("node_id".into(), node_id)]),
66        }
67    }
68
69    pub fn no_such_node(node_id: impl Into<String>) -> Self {
70        let node_id = node_id.into();
71        Error::NoSuchNode {
72            code: 1001,
73            msg: format!(
74                "There is no node with node_id={node_id}, maybe you need to register first?"
75            ),
76            values: HashMap::from([("node_id".into(), node_id)]),
77        }
78    }
79
80    pub fn unparseable_creds() -> Self {
81        Error::UnparseableCreds {
82            code: 1100,
83            msg: "The provided credentials could not be parsed, please recover.".into(),
84            values: HashMap::new(),
85        }
86    }
87
88    pub fn phrase_corrupted() -> Self {
89        Error::PhraseCorrupted {
90            code: 1101,
91            msg: "The passphrase you provided fails the checksum".into(),
92            values: HashMap::new(),
93        }
94    }
95
96    pub fn rpc(detail: impl Into<String>) -> Self {
97        let detail = detail.into();
98        Error::Rpc {
99            code: 2000,
100            msg: format!("Error calling the rpc: {detail}"),
101            values: HashMap::from([("detail".into(), detail)]),
102        }
103    }
104
105    pub fn argument(arg_name: impl Into<String>, arg_value: impl Into<String>) -> Self {
106        let arg_name = arg_name.into();
107        let arg_value = arg_value.into();
108        Error::Argument {
109            code: 3000,
110            msg: format!("Invalid argument: {arg_name}={arg_value}"),
111            values: HashMap::from([
112                ("arg_name".into(), arg_name),
113                ("arg_value".into(), arg_value),
114            ]),
115        }
116    }
117
118    pub fn other(detail: impl Into<String>) -> Self {
119        let detail = detail.into();
120        Error::Other {
121            code: 9000,
122            msg: format!("Generic error: {detail}"),
123            values: HashMap::from([("detail".into(), detail)]),
124        }
125    }
126}
127
128mod config;
129mod credentials;
130mod input;
131mod lnurl;
132mod logging;
133mod node;
134mod node_builder;
135mod scheduler;
136mod signer;
137mod util;
138
139pub use crate::{
140    config::Config,
141    credentials::{Credentials, DeveloperCert},
142    node::{
143        ChannelState, FundChannel, FundOutput, GetInfoResponse, Invoice,
144        InvoicePaidEvent, InvoiceStatus, ListFundsResponse, ListIndex, ListInvoicesResponse,
145        ListPaymentsRequest, ListPeerChannelsResponse, ListPaysResponse, ListPeersResponse,
146        Node, NodeEvent, NodeEventListener, NodeEventStream, NodeState, OnchainBalanceState,
147        OnchainFeeRates, OnchainReceiveResponse, OnchainSendResponse, Outpoint, OutputStatus,
148        Pay, PayStatus, Payment, PaymentStatus, PaymentType, PaymentTypeFilter, Peer,
149        PeerChannel, PreparedOnchainSend, ReceiveResponse, SendResponse,
150    },
151    input::{ParsedInput, ParsedInvoice, ResolvedInput},
152    logging::{LogEntry, LogLevel, LogListener},
153    lnurl::{
154        LnUrlErrorData, LnUrlPayRequest, LnUrlPayRequestData, LnUrlPayResult,
155        LnUrlPaySuccessData, LnUrlWithdrawRequest, LnUrlWithdrawRequestData,
156        LnUrlWithdrawResult, LnUrlWithdrawSuccessData, SuccessActionProcessed,
157    },
158    node_builder::NodeBuilder,
159    scheduler::Scheduler,
160    signer::{Handle, Signer},
161};
162
163/// Which scheduler operation to perform.
164enum SchedulerAction {
165    Register { invite_code: Option<String> },
166    Recover,
167}
168
169/// Shared implementation for register and recover flows.
170fn schedule_node(
171    seed: Vec<u8>,
172    config: &config::Config,
173    action: SchedulerAction,
174) -> Result<std::sync::Arc<node::Node>, Error> {
175    use std::sync::Arc;
176
177    let network = config.network;
178    let nobody = config.nobody();
179
180    let seed_for_async = seed.clone();
181    let credentials = util::exec(async move {
182        let signer =
183            gl_client::signer::Signer::new(seed_for_async, network, nobody.clone())
184                .map_err(|e| Error::other(e.to_string()))?;
185
186        let scheduler = gl_client::scheduler::Scheduler::new(network, nobody)
187            .await
188            .map_err(|e| Error::other(e.to_string()))?;
189
190        let node_id_hex = hex::encode(signer.node_id());
191
192        let creds_bytes = match action {
193            SchedulerAction::Register { invite_code } => {
194                scheduler
195                    .register(&signer, invite_code)
196                    .await
197                    .map_err(|e| map_scheduler_error(e, &node_id_hex))?
198                    .creds
199            }
200            SchedulerAction::Recover => {
201                scheduler
202                    .recover(&signer)
203                    .await
204                    .map_err(|e| map_scheduler_error(e, &node_id_hex))?
205                    .creds
206            }
207        };
208
209        credentials::Credentials::load(creds_bytes)
210    })?;
211
212    let authenticated_signer =
213        gl_client::signer::Signer::new(seed, network, credentials.inner.clone())
214            .map_err(|e| Error::other(e.to_string()))?;
215
216    let handle = signer::Handle::spawn(authenticated_signer);
217    let node = node::Node::with_signer(credentials, handle, network)?;
218    Ok(Arc::new(node))
219}
220
221/// Map scheduler errors to specific Error variants.
222/// First tries tonic status codes, then falls back to error message matching.
223fn map_scheduler_error(e: anyhow::Error, node_id_hex: &str) -> Error {
224    // Walk the error chain looking for a tonic::Status with a specific code
225    for cause in e.chain() {
226        if let Some(status) = cause.downcast_ref::<tonic::Status>() {
227            match status.code() {
228                tonic::Code::AlreadyExists => {
229                    return Error::duplicate_node(node_id_hex.to_string())
230                }
231                tonic::Code::NotFound => return Error::no_such_node(node_id_hex.to_string()),
232                // Don't return here — the tonic status might be a generic
233                // wrapper (e.g. Internal/Unknown) around a more specific
234                // error. Fall through to string matching.
235                _ => {}
236            }
237        }
238    }
239
240    // Fall back to checking the full error message for known patterns.
241    let msg = e.to_string();
242    if msg.contains("NOT_FOUND")
243        || msg.contains("no rows returned")
244        || msg.contains("Recovery failed")
245    {
246        Error::no_such_node(node_id_hex.to_string())
247    } else if msg.contains("ALREADY_EXISTS") {
248        Error::duplicate_node(node_id_hex.to_string())
249    } else {
250        Error::other(msg)
251    }
252}
253
254/// Parse a BIP39 mnemonic into a seed.
255fn parse_mnemonic(mnemonic: &str) -> Result<Vec<u8>, Error> {
256    use bip39::Mnemonic;
257    use std::str::FromStr;
258    let phrase = Mnemonic::from_str(mnemonic).map_err(|_e| Error::phrase_corrupted())?;
259    Ok(phrase.to_seed_normalized("").to_vec())
260}
261
262/// Crate-internal: connect using saved credentials. The builder
263/// (`NodeBuilder::connect`) is the public entry point.
264pub(crate) fn connect_internal(
265    mnemonic: String,
266    credentials: Vec<u8>,
267    config: &config::Config,
268) -> Result<std::sync::Arc<node::Node>, Error> {
269    use std::sync::Arc;
270
271    let seed = parse_mnemonic(&mnemonic)?;
272    let network = config.network;
273    let creds = credentials::Credentials::load(credentials)?;
274
275    let authenticated_signer =
276        gl_client::signer::Signer::new(seed, network, creds.inner.clone())
277            .map_err(|e| Error::other(e.to_string()))?;
278
279    let handle = signer::Handle::spawn(authenticated_signer);
280    let node = node::Node::with_signer(creds, handle, network)?;
281    Ok(Arc::new(node))
282}
283
284/// Crate-internal: register a fresh node. The builder
285/// (`NodeBuilder::register`) is the public entry point.
286pub(crate) fn register_internal(
287    mnemonic: String,
288    invite_code: Option<String>,
289    config: &config::Config,
290) -> Result<std::sync::Arc<node::Node>, Error> {
291    let seed = parse_mnemonic(&mnemonic)?;
292    schedule_node(seed, config, SchedulerAction::Register { invite_code })
293}
294
295/// Crate-internal: recover an existing node. The builder
296/// (`NodeBuilder::recover`) is the public entry point.
297pub(crate) fn recover_internal(
298    mnemonic: String,
299    config: &config::Config,
300) -> Result<std::sync::Arc<node::Node>, Error> {
301    let seed = parse_mnemonic(&mnemonic)?;
302    schedule_node(seed, config, SchedulerAction::Recover)
303}
304
305/// Crate-internal: register-or-recover. The builder
306/// (`NodeBuilder::register_or_recover`) is the public entry point.
307pub(crate) fn register_or_recover_internal(
308    mnemonic: String,
309    invite_code: Option<String>,
310    config: &config::Config,
311) -> Result<std::sync::Arc<node::Node>, Error> {
312    match recover_internal(mnemonic.clone(), config) {
313        Ok(node) => Ok(node),
314        Err(Error::NoSuchNode { .. }) => register_internal(mnemonic, invite_code, config),
315        Err(e) => Err(e),
316    }
317}
318
319/// Crate-internal: connect signerless — credentials only, no
320/// SDK-side signer spawned. Used by `NodeBuilder::connect` when no
321/// mnemonic is set.
322///
323/// Signing-required RPCs (`pay`, `receive` JIT-channel, etc.) rely
324/// on a signer running elsewhere — typically the CLN node's local
325/// signer or a paired device. This is the supported model for
326/// signerless clients (browser extensions, paired devices, hardware
327/// signers held outside the SDK process).
328pub(crate) fn connect_signerless_internal(
329    credentials: Vec<u8>,
330    _config: &config::Config,
331) -> Result<std::sync::Arc<node::Node>, Error> {
332    use std::sync::Arc;
333    let creds = credentials::Credentials::load(credentials)?;
334    let node = node::Node::signerless(creds)?;
335    Ok(Arc::new(node))
336}
337
338/// Synchronously classify the input. **No HTTP, no I/O.**
339///
340/// Recognises BOLT11 invoices, node IDs, LNURL bech32 strings, and
341/// Lightning Addresses. Strips `lightning:` / `LIGHTNING:` prefixes
342/// automatically. LNURL inputs are decoded to their underlying URL
343/// but **not fetched** — the caller chooses whether to resolve
344/// further (via `resolve_input`) or to surface the URL to the user
345/// as-is.
346///
347/// Use this for offline operations like clipboard validation or
348/// invoice sanity checks. Use `resolve_input` for the QR-scan flow
349/// where you want the resolved pay/withdraw data in one call.
350#[uniffi::export]
351pub fn parse_input(input: String) -> Result<input::ParsedInput, Error> {
352    input::parse_input(input)
353}
354
355/// Classify and resolve the input.
356///
357/// Internally calls `parse_input` for offline classification, then
358/// for LNURL bech32 strings and Lightning Addresses performs the
359/// HTTP GET to the LNURL endpoint and returns typed pay or withdraw
360/// request data. For BOLT11 invoices and node IDs it returns
361/// immediately without I/O.
362///
363/// Strips `lightning:` / `LIGHTNING:` prefixes automatically.
364///
365/// # Blocking
366///
367/// This function blocks the calling thread while any network I/O
368/// completes. The SDK exposes a **synchronous-only** public API so
369/// that every language binding (Python, Kotlin, Swift, Ruby, C++)
370/// works without requiring an async runtime on the caller side.
371/// Async work is executed internally on a shared Tokio runtime
372/// managed by the SDK.
373#[uniffi::export]
374pub fn resolve_input(input: String) -> Result<input::ResolvedInput, Error> {
375    util::exec(async { input::resolve_input(input).await })
376}
377
378/// Set up SDK logging. Call once before any other SDK function.
379///
380/// The listener receives all log messages from the SDK and the
381/// underlying Greenlight client library. Call once, as early as
382/// possible, so early logs are captured. Returns an error if a logger
383/// has already been installed in this process. To change the filter
384/// after installation, use `set_log_level`.
385#[uniffi::export]
386pub fn set_logger(
387    level: logging::LogLevel,
388    listener: Box<dyn logging::LogListener>,
389) -> Result<(), Error> {
390    logging::set_logger(level, listener)
391}
392
393/// Change the log filter at runtime without reinstalling the listener.
394#[uniffi::export]
395pub fn set_log_level(level: logging::LogLevel) {
396    logging::set_log_level(level)
397}
398
399#[derive(uniffi::Enum, Debug)]
400pub enum Network {
401    BITCOIN,
402    REGTEST,
403}
404
405impl From<Network> for gl_client::bitcoin::Network {
406    fn from(n: Network) -> gl_client::bitcoin::Network {
407        match n {
408            Network::BITCOIN => gl_client::bitcoin::Network::Bitcoin,
409            Network::REGTEST => gl_client::bitcoin::Network::Regtest,
410        }
411    }
412}