Skip to main content

glsdk/
node.rs

1use crate::{credentials::Credentials, signer::Handle, util::exec, Error};
2use std::sync::atomic::{AtomicBool, Ordering};
3use gl_client::credentials::NodeIdProvider;
4use gl_client::node::{Client as GlClient, ClnClient, Node as ClientNode};
5use gl_client::pb::{self as glpb, cln as clnpb};
6use lightning_invoice::Bolt11Invoice;
7use std::sync::{Arc, Mutex};
8use tokio::sync::OnceCell;
9
10/// The `Node` is an RPC stub representing the node running in the
11/// cloud. It is the main entrypoint to interact with the node.
12#[derive(uniffi::Object)]
13#[allow(unused)]
14pub struct Node {
15    inner: ClientNode,
16    cln_client: OnceCell<ClnClient>,
17    gl_client: OnceCell<GlClient>,
18    stored_credentials: Option<Credentials>,
19    signer_handle: Option<Handle>,
20    disconnected: AtomicBool,
21}
22
23#[uniffi::export]
24impl Node {
25    #[uniffi::constructor()]
26    pub fn new(credentials: &Credentials) -> Result<Self, Error> {
27        let node_id = credentials
28            .inner
29            .node_id()
30            .map_err(|_e| Error::UnparseableCreds())?;
31        let inner = ClientNode::new(node_id, credentials.inner.clone())
32            .expect("infallible client instantiation");
33
34        let cln_client = OnceCell::const_new();
35        let gl_client = OnceCell::const_new();
36        Ok(Node {
37            inner,
38            cln_client,
39            gl_client,
40            stored_credentials: Some(credentials.clone()),
41            signer_handle: None,
42            disconnected: AtomicBool::new(false),
43        })
44    }
45
46    /// Stop the node if it is currently running.
47    pub fn stop(&self) -> Result<(), Error> {
48        self.check_connected()?;
49        let mut cln_client = exec(self.get_cln_client())?.clone();
50
51        let req = clnpb::StopRequest {};
52
53        // It's ok, the error here is expected and should just be
54        // telling us that we've lost the connection. This is to
55        // be expected on shutdown, so we clamp this to success.
56        let _ = exec(cln_client.stop(req));
57        Ok(())
58    }
59
60    /// Returns the serialized credentials for this node.
61    /// The app should persist these bytes and pass them to connect() on next launch.
62    pub fn credentials(&self) -> Result<Vec<u8>, Error> {
63        match &self.stored_credentials {
64            Some(creds) => creds.save(),
65            None => Err(Error::Other(
66                "No credentials stored. Use register/recover/connect to create a Node with credentials.".to_string(),
67            )),
68        }
69    }
70
71    /// Disconnects from the node and stops the signer if running.
72    /// After disconnect, all RPC methods will return an error.
73    /// Safe to call multiple times.
74    pub fn disconnect(&self) -> Result<(), Error> {
75        self.disconnected.store(true, Ordering::Relaxed);
76        if let Some(ref handle) = self.signer_handle {
77            handle.try_stop();
78        }
79        Ok(())
80    }
81
82    /// Receive an off-chain payment.
83    ///
84    /// This method generates a request for a payment, also called an
85    /// invoice, that encodes all the information, including amount
86    /// and destination, for a prospective sender to send a lightning
87    /// payment. The invoice includes negotiation of an LSPS2 / JIT
88    /// channel, meaning that if there is no channel sufficient to
89    /// receive the requested funds, the node will negotiate an
90    /// opening, and when/if executed the payment will cause a channel
91    /// to be created, and the incoming payment to be forwarded.
92    pub fn receive(
93        &self,
94        label: String,
95        description: String,
96        amount_msat: Option<u64>,
97    ) -> Result<ReceiveResponse, Error> {
98        self.check_connected()?;
99        let mut gl_client = exec(self.get_gl_client())?.clone();
100
101        let req = gl_client::pb::LspInvoiceRequest {
102            amount_msat: amount_msat.unwrap_or_default(),
103            description: description,
104            label: label,
105            lsp_id: "".to_owned(),
106            token: "".to_owned(),
107        };
108        let res = exec(gl_client.lsp_invoice(req))
109            .map_err(|s| Error::Rpc(s.to_string()))?
110            .into_inner();
111        Ok(ReceiveResponse {
112            bolt11: res.bolt11,
113            opening_fee_msat: res.opening_fee_msat,
114        })
115    }
116
117    pub fn send(&self, invoice: String, amount_msat: Option<u64>) -> Result<SendResponse, Error> {
118        self.check_connected()?;
119        let mut cln_client = exec(self.get_cln_client())?.clone();
120        let req = clnpb::PayRequest {
121            amount_msat: match amount_msat {
122                Some(a) => Some(clnpb::Amount { msat: a }),
123                None => None,
124            },
125
126            bolt11: invoice,
127            description: None,
128            exclude: vec![],
129            exemptfee: None,
130            label: None,
131            localinvreqid: None,
132            maxdelay: None,
133            maxfee: None,
134            maxfeepercent: None,
135            partial_msat: None,
136            retry_for: None,
137            riskfactor: None,
138        };
139        exec(cln_client.pay(req))
140            .map_err(|e| Error::Rpc(e.to_string()))
141            .map(|r| r.into_inner().into())
142    }
143
144    /// Send bitcoin on-chain to a destination address.
145    ///
146    /// # Arguments
147    /// * `destination` — A Bitcoin address (bech32, p2sh, or p2tr).
148    /// * `amount_or_all` — Amount to send. Accepts:
149    ///   - `"50000"` or `"50000sat"` — 50,000 satoshis
150    ///   - `"50000msat"` — 50,000 millisatoshis
151    ///   - `"all"` — sweep the entire on-chain balance
152    ///
153    /// Returns the raw transaction, txid, and PSBT once broadcast.
154    /// The transaction is broadcast immediately — this is not a dry run.
155    pub fn onchain_send(
156        &self,
157        destination: String,
158        amount_or_all: String,
159    ) -> Result<OnchainSendResponse, Error> {
160        self.check_connected()?;
161        let mut cln_client = exec(self.get_cln_client())?.clone();
162
163        // Decode what the user intends to do. Either we have `all`,
164        // or we have an amount that we can parse.
165        let (num, suffix): (String, String) = amount_or_all.chars().partition(|c| c.is_digit(10));
166
167        let num = if num.len() > 0 {
168            num.parse::<u64>().unwrap()
169        } else {
170            0
171        };
172        let satoshi = match (num, suffix.as_ref()) {
173            (n, "") | (n, "sat") => clnpb::AmountOrAll {
174                // No value suffix, interpret as satoshis. This is an
175                // onchain RPC method, hence the sat denomination by
176                // default.
177                value: Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount {
178                    msat: n * 1000,
179                })),
180            },
181            (n, "msat") => clnpb::AmountOrAll {
182                value: Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount {
183                    msat: n,
184                })),
185            },
186            (0, "all") => clnpb::AmountOrAll {
187                value: Some(clnpb::amount_or_all::Value::All(true)),
188            },
189            (_, _) => return Err(Error::Argument("amount_or_all".to_owned(), amount_or_all)),
190        };
191
192        let req = clnpb::WithdrawRequest {
193            destination: destination,
194            minconf: None,
195            feerate: None,
196            satoshi: Some(satoshi),
197            utxos: vec![],
198        };
199
200        exec(cln_client.withdraw(req))
201            .map_err(|e| Error::Rpc(e.to_string()))
202            .map(|r| r.into_inner().into())
203    }
204
205    /// Generate a fresh on-chain Bitcoin address for receiving funds.
206    ///
207    /// Returns both a bech32 (SegWit v0) and a p2tr (Taproot) address.
208    /// Either can be shared with a sender. Deposited funds will appear
209    /// in `node_state().onchain_balance_msat` once confirmed.
210    pub fn onchain_receive(&self) -> Result<OnchainReceiveResponse, Error> {
211        self.check_connected()?;
212        let mut cln_client = exec(self.get_cln_client())?.clone();
213
214        let req = clnpb::NewaddrRequest {
215            addresstype: Some(clnpb::newaddr_request::NewaddrAddresstype::All.into()),
216        };
217
218        let res = exec(cln_client.new_addr(req))
219            .map_err(|e| Error::Rpc(e.to_string()))?
220            .into_inner();
221        Ok(res.into())
222    }
223
224    /// Get information about the node.
225    ///
226    /// Returns basic information about the node including its ID,
227    /// alias, network, and channel counts.
228    pub fn get_info(&self) -> Result<GetInfoResponse, Error> {
229        self.check_connected()?;
230        let mut cln_client = exec(self.get_cln_client())?.clone();
231
232        let req = clnpb::GetinfoRequest {};
233
234        let res = exec(cln_client.getinfo(req))
235            .map_err(|e| Error::Rpc(e.to_string()))?
236            .into_inner();
237        Ok(res.into())
238    }
239
240    /// List all peers connected to this node.
241    ///
242    /// Returns information about all peers including their connection
243    /// status.
244    pub fn list_peers(&self) -> Result<ListPeersResponse, Error> {
245        self.check_connected()?;
246        let mut cln_client = exec(self.get_cln_client())?.clone();
247
248        let req = clnpb::ListpeersRequest {
249            id: None,
250            level: None,
251        };
252
253        let res = exec(cln_client.list_peers(req))
254            .map_err(|e| Error::Rpc(e.to_string()))?
255            .into_inner();
256        Ok(res.into())
257    }
258
259    /// List all channels with peers.
260    ///
261    /// Returns detailed information about all channels including their
262    /// state, capacity, and balances.
263    pub fn list_peer_channels(&self) -> Result<ListPeerChannelsResponse, Error> {
264        self.check_connected()?;
265        let mut cln_client = exec(self.get_cln_client())?.clone();
266
267        let req = clnpb::ListpeerchannelsRequest { id: None };
268
269        let res = exec(cln_client.list_peer_channels(req))
270            .map_err(|e| Error::Rpc(e.to_string()))?
271            .into_inner();
272        Ok(res.into())
273    }
274
275    /// List all funds available to the node.
276    ///
277    /// Returns information about on-chain outputs and channel funds
278    /// that are available or pending.
279    pub fn list_funds(&self) -> Result<ListFundsResponse, Error> {
280        self.check_connected()?;
281        let mut cln_client = exec(self.get_cln_client())?.clone();
282
283        let req = clnpb::ListfundsRequest { spent: None };
284
285        let res = exec(cln_client.list_funds(req))
286            .map_err(|e| Error::Rpc(e.to_string()))?
287            .into_inner();
288        Ok(res.into())
289    }
290
291    /// List all invoices (received payment requests).
292    /// List invoices (received payment requests).
293    /// All parameters are optional filters; pass None to fetch all.
294    pub fn list_invoices(
295        &self,
296        label: Option<String>,
297        invstring: Option<String>,
298        payment_hash: Option<Vec<u8>>,
299        offer_id: Option<String>,
300        index: Option<ListIndex>,
301        start: Option<u64>,
302        limit: Option<u32>,
303    ) -> Result<ListInvoicesResponse, Error> {
304        self.check_connected()?;
305        let mut cln_client = exec(self.get_cln_client())?.clone();
306
307        let req = clnpb::ListinvoicesRequest {
308            label,
309            invstring,
310            payment_hash,
311            offer_id,
312            index: index.map(|i| i.to_i32()),
313            start,
314            limit,
315        };
316
317        let res = exec(cln_client.list_invoices(req))
318            .map_err(|e| Error::Rpc(e.to_string()))?
319            .into_inner();
320        Ok(res.into())
321    }
322
323    /// List outgoing payments.
324    /// All parameters are optional filters; pass None to fetch all.
325    pub fn list_pays(
326        &self,
327        bolt11: Option<String>,
328        payment_hash: Option<Vec<u8>>,
329        status: Option<PayStatus>,
330        index: Option<ListIndex>,
331        start: Option<u64>,
332        limit: Option<u32>,
333    ) -> Result<ListPaysResponse, Error> {
334        self.check_connected()?;
335        let mut cln_client = exec(self.get_cln_client())?.clone();
336
337        // ListpaysRequest.ListpaysStatus: PENDING=0, COMPLETE=1, FAILED=2
338        let cln_status = status.map(|s| match s {
339            PayStatus::PENDING => 0,
340            PayStatus::COMPLETE => 1,
341            PayStatus::FAILED => 2,
342        });
343
344        let req = clnpb::ListpaysRequest {
345            bolt11,
346            payment_hash,
347            status: cln_status,
348            index: index.map(|i| i.to_i32()),
349            start,
350            limit,
351        };
352
353        let res = exec(cln_client.list_pays(req))
354            .map_err(|e| Error::Rpc(e.to_string()))?
355            .into_inner();
356        Ok(res.into())
357    }
358
359    /// List payments (sent and received), merged into a single timeline.
360    ///
361    /// Fetches invoices and outgoing payments from the node, merges
362    /// them into a unified list, and applies optional filters.
363    /// Use `list_invoices`/`list_pays` for direct CLN access.
364    /// Results are sorted newest-first.
365    pub fn list_payments(&self, req: ListPaymentsRequest) -> Result<Vec<Payment>, Error> {
366        self.check_connected()?;
367        let mut cln_client = exec(self.get_cln_client())?.clone();
368
369        let invoices = exec(cln_client.list_invoices(clnpb::ListinvoicesRequest::default()))
370            .map_err(|e| Error::Rpc(e.to_string()))?
371            .into_inner();
372
373        let mut cln_client = exec(self.get_cln_client())?.clone();
374        let pays = exec(cln_client.list_pays(clnpb::ListpaysRequest::default()))
375            .map_err(|e| Error::Rpc(e.to_string()))?
376            .into_inner();
377
378        let mut payments: Vec<Payment> = Vec::new();
379
380        // Should we include received payments?
381        let include_received = req
382            .filters
383            .as_ref()
384            .map(|f| f.is_empty() || f.iter().any(|t| matches!(t, PaymentTypeFilter::Received)))
385            .unwrap_or(true);
386
387        // Should we include sent payments?
388        let include_sent = req
389            .filters
390            .as_ref()
391            .map(|f| f.is_empty() || f.iter().any(|t| matches!(t, PaymentTypeFilter::Sent)))
392            .unwrap_or(true);
393
394        if include_received {
395            payments.extend(invoices.invoices.into_iter().map(|i| -> Payment { i.into() }));
396        }
397        if include_sent {
398            payments.extend(pays.pays.into_iter().map(|p| -> Payment { p.into() }));
399        }
400
401        let include_failures = req.include_failures.unwrap_or(false);
402
403        payments.retain(|p| {
404            if !include_failures && matches!(p.status, PaymentStatus::Failed) {
405                return false;
406            }
407            if let Some(from) = req.from_timestamp {
408                if p.payment_time < from {
409                    return false;
410                }
411            }
412            if let Some(to) = req.to_timestamp {
413                if p.payment_time > to {
414                    return false;
415                }
416            }
417            true
418        });
419
420        // Sort newest first
421        payments.sort_by(|a, b| b.payment_time.cmp(&a.payment_time));
422
423        // Apply pagination
424        let offset = req.offset.unwrap_or(0) as usize;
425        let limit = req.limit.unwrap_or(u32::MAX) as usize;
426        let payments = payments.into_iter().skip(offset).take(limit).collect();
427
428        Ok(payments)
429    }
430
431    /// Stream real-time events from the node.
432    ///
433    /// Returns a `NodeEventStream` iterator. Call `next()` repeatedly
434    /// to receive events as they occur (e.g., invoice payments).
435    ///
436    /// The `next()` method blocks the calling thread until an event
437    /// is available, but does not block the underlying async runtime,
438    /// so other node methods can be called concurrently from other
439    /// threads.
440    pub fn stream_node_events(&self) -> Result<Arc<NodeEventStream>, Error> {
441        self.check_connected()?;
442        let mut gl_client = exec(self.get_gl_client())?.clone();
443        let req = glpb::NodeEventsRequest {};
444        let stream = exec(gl_client.stream_node_events(req))
445            .map_err(|e| Error::Rpc(e.to_string()))?
446            .into_inner();
447        Ok(Arc::new(NodeEventStream {
448            inner: Mutex::new(stream),
449        }))
450    }
451}
452
453// Not exported through uniffi
454impl Node {
455    fn check_connected(&self) -> Result<(), Error> {
456        if self.disconnected.load(Ordering::Relaxed) {
457            return Err(Error::Other("Node is disconnected".to_string()));
458        }
459        Ok(())
460    }
461
462    /// Internal constructor used by the high-level register/recover/connect functions.
463    /// Creates a Node with credentials and signer handle attached.
464    pub(crate) fn with_signer(
465        credentials: Credentials,
466        handle: Handle,
467    ) -> Result<Self, Error> {
468        let node_id = credentials
469            .inner
470            .node_id()
471            .map_err(|_e| Error::UnparseableCreds())?;
472        let inner = ClientNode::new(node_id, credentials.inner.clone())
473            .expect("infallible client instantiation");
474
475        let cln_client = OnceCell::const_new();
476        let gl_client = OnceCell::const_new();
477        Ok(Node {
478            inner,
479            cln_client,
480            gl_client,
481            stored_credentials: Some(credentials),
482            signer_handle: Some(handle),
483            disconnected: AtomicBool::new(false),
484        })
485    }
486
487    async fn get_gl_client<'a>(&'a self) -> Result<&'a GlClient, Error> {
488        let inner = self.inner.clone();
489        self.gl_client
490            .get_or_try_init(|| async { inner.schedule::<GlClient>().await })
491            .await
492            .map_err(|e| Error::Rpc(e.to_string()))
493    }
494
495    async fn get_cln_client<'a>(&'a self) -> Result<&'a ClnClient, Error> {
496        let inner = self.inner.clone();
497
498        self.cln_client
499            .get_or_try_init(|| async { inner.schedule::<ClnClient>().await })
500            .await
501            .map_err(|e| Error::Rpc(e.to_string()))
502    }
503}
504
505/// Result of an on-chain send. The transaction has already been broadcast.
506#[derive(uniffi::Record)]
507pub struct OnchainSendResponse {
508    /// The raw signed transaction bytes.
509    pub tx: Vec<u8>,
510    /// The transaction ID (32 bytes, reversed byte order as is standard).
511    pub txid: Vec<u8>,
512    /// The transaction as a Partially Signed Bitcoin Transaction string.
513    pub psbt: String,
514}
515
516impl From<clnpb::WithdrawResponse> for OnchainSendResponse {
517    fn from(other: clnpb::WithdrawResponse) -> Self {
518        Self {
519            tx: other.tx,
520            txid: other.txid,
521            psbt: other.psbt,
522        }
523    }
524}
525
526/// A pair of on-chain addresses for receiving funds.
527#[derive(uniffi::Record)]
528pub struct OnchainReceiveResponse {
529    /// SegWit v0 (bech32) address — starts with `bc1q` on mainnet.
530    pub bech32: String,
531    /// Taproot (bech32m) address — starts with `bc1p` on mainnet.
532    pub p2tr: String,
533}
534
535impl From<clnpb::NewaddrResponse> for OnchainReceiveResponse {
536    fn from(other: clnpb::NewaddrResponse) -> Self {
537        OnchainReceiveResponse {
538            bech32: other.bech32.unwrap_or_default(),
539            p2tr: other.p2tr.unwrap_or_default(),
540        }
541    }
542}
543
544#[derive(uniffi::Record)]
545pub struct SendResponse {
546    pub status: PayStatus,
547    pub preimage: Vec<u8>,
548    pub payment_hash: Vec<u8>,
549    pub destination_pubkey: Option<Vec<u8>>,
550    pub amount_msat: u64,
551    pub amount_sent_msat: u64,
552    pub parts: u32,
553}
554
555impl From<clnpb::PayResponse> for SendResponse {
556    fn from(other: clnpb::PayResponse) -> Self {
557        Self {
558            status: other.status.into(),
559            preimage: other.payment_preimage,
560            payment_hash: other.payment_hash,
561            destination_pubkey: other.destination,
562            amount_msat: other.amount_msat.unwrap().msat,
563            amount_sent_msat: other.amount_sent_msat.unwrap().msat,
564            parts: other.parts,
565        }
566    }
567}
568
569#[derive(uniffi::Record)]
570pub struct ReceiveResponse {
571    pub bolt11: String,
572    /// The fee charged by the LSP for opening a JIT channel, in
573    /// millisatoshi. This is 0 if no JIT channel was needed.
574    pub opening_fee_msat: u64,
575}
576
577#[derive(uniffi::Enum, Clone)]
578pub enum PayStatus {
579    COMPLETE = 0,
580    PENDING = 1,
581    FAILED = 2,
582}
583
584impl From<clnpb::pay_response::PayStatus> for PayStatus {
585    fn from(other: clnpb::pay_response::PayStatus) -> Self {
586        match other {
587            clnpb::pay_response::PayStatus::Complete => PayStatus::COMPLETE,
588            clnpb::pay_response::PayStatus::Failed => PayStatus::FAILED,
589            clnpb::pay_response::PayStatus::Pending => PayStatus::PENDING,
590        }
591    }
592}
593
594impl From<i32> for PayStatus {
595    fn from(i: i32) -> Self {
596        match i {
597            0 => PayStatus::COMPLETE,
598            1 => PayStatus::PENDING,
599            2 => PayStatus::FAILED,
600            o => panic!("Unknown pay_status {}", o),
601        }
602    }
603}
604
605// ============================================================
606// GetInfo response types
607// ============================================================
608
609#[allow(unused)]
610#[derive(Clone, uniffi::Record)]
611pub struct GetInfoResponse {
612    pub id: Vec<u8>,
613    pub alias: Option<String>,
614    pub color: Vec<u8>,
615    pub num_peers: u32,
616    pub num_pending_channels: u32,
617    pub num_active_channels: u32,
618    pub num_inactive_channels: u32,
619    pub version: String,
620    pub lightning_dir: String,
621    pub blockheight: u32,
622    pub network: String,
623    pub fees_collected_msat: u64,
624}
625
626impl From<clnpb::GetinfoResponse> for GetInfoResponse {
627    fn from(other: clnpb::GetinfoResponse) -> Self {
628        Self {
629            id: other.id,
630            alias: other.alias,
631            color: other.color,
632            num_peers: other.num_peers,
633            num_pending_channels: other.num_pending_channels,
634            num_active_channels: other.num_active_channels,
635            num_inactive_channels: other.num_inactive_channels,
636            version: other.version,
637            lightning_dir: other.lightning_dir,
638            blockheight: other.blockheight,
639            network: other.network,
640            fees_collected_msat: other.fees_collected_msat.map(|a| a.msat).unwrap_or(0),
641        }
642    }
643}
644
645// ============================================================
646// ListPeers response types
647// ============================================================
648
649#[allow(unused)]
650#[derive(Clone, uniffi::Record)]
651pub struct ListPeersResponse {
652    pub peers: Vec<Peer>,
653}
654
655#[allow(unused)]
656#[derive(Clone, uniffi::Record)]
657pub struct Peer {
658    pub id: Vec<u8>,
659    pub connected: bool,
660    pub num_channels: Option<u32>,
661    pub netaddr: Vec<String>,
662    pub remote_addr: Option<String>,
663    pub features: Option<Vec<u8>>,
664}
665
666impl From<clnpb::ListpeersResponse> for ListPeersResponse {
667    fn from(other: clnpb::ListpeersResponse) -> Self {
668        Self {
669            peers: other.peers.into_iter().map(|p| p.into()).collect(),
670        }
671    }
672}
673
674impl From<clnpb::ListpeersPeers> for Peer {
675    fn from(other: clnpb::ListpeersPeers) -> Self {
676        Self {
677            id: other.id,
678            connected: other.connected,
679            num_channels: other.num_channels,
680            netaddr: other.netaddr,
681            remote_addr: other.remote_addr,
682            features: other.features,
683        }
684    }
685}
686
687// ============================================================
688// ListPeerChannels response types
689// ============================================================
690
691#[allow(unused)]
692#[derive(Clone, uniffi::Record)]
693pub struct ListPeerChannelsResponse {
694    pub channels: Vec<PeerChannel>,
695}
696
697#[allow(unused)]
698#[derive(Clone, uniffi::Record)]
699pub struct PeerChannel {
700    pub peer_id: Vec<u8>,
701    pub peer_connected: bool,
702    pub state: ChannelState,
703    pub short_channel_id: Option<String>,
704    pub channel_id: Option<Vec<u8>>,
705    pub funding_txid: Option<Vec<u8>>,
706    pub funding_outnum: Option<u32>,
707    pub to_us_msat: Option<u64>,
708    pub total_msat: Option<u64>,
709    pub spendable_msat: Option<u64>,
710    pub receivable_msat: Option<u64>,
711}
712
713#[derive(Clone, uniffi::Enum)]
714pub enum ChannelState {
715    Openingd,
716    ChanneldAwaitingLockin,
717    ChanneldNormal,
718    ChanneldShuttingDown,
719    ClosingdSigexchange,
720    ClosingdComplete,
721    AwaitingUnilateral,
722    FundingSpendSeen,
723    Onchain,
724    DualopendOpenInit,
725    DualopendAwaitingLockin,
726    DualopendOpenCommitted,
727    DualopendOpenCommitReady,
728}
729
730impl ChannelState {
731    fn from_i32(value: i32) -> Self {
732        match value {
733            0 => ChannelState::Openingd,
734            1 => ChannelState::ChanneldAwaitingLockin,
735            2 => ChannelState::ChanneldNormal,
736            3 => ChannelState::ChanneldShuttingDown,
737            4 => ChannelState::ClosingdSigexchange,
738            5 => ChannelState::ClosingdComplete,
739            6 => ChannelState::AwaitingUnilateral,
740            7 => ChannelState::FundingSpendSeen,
741            8 => ChannelState::Onchain,
742            9 => ChannelState::DualopendOpenInit,
743            10 => ChannelState::DualopendAwaitingLockin,
744            11 => ChannelState::DualopendOpenCommitted,
745            12 => ChannelState::DualopendOpenCommitReady,
746            _ => ChannelState::Onchain, // Default fallback
747        }
748    }
749}
750
751impl From<clnpb::ListpeerchannelsResponse> for ListPeerChannelsResponse {
752    fn from(other: clnpb::ListpeerchannelsResponse) -> Self {
753        Self {
754            channels: other.channels.into_iter().map(|c| c.into()).collect(),
755        }
756    }
757}
758
759impl From<clnpb::ListpeerchannelsChannels> for PeerChannel {
760    fn from(other: clnpb::ListpeerchannelsChannels) -> Self {
761        let state = ChannelState::from_i32(other.state);
762        Self {
763            peer_id: other.peer_id,
764            peer_connected: other.peer_connected,
765            state,
766            short_channel_id: other.short_channel_id,
767            channel_id: other.channel_id,
768            funding_txid: other.funding_txid,
769            funding_outnum: other.funding_outnum,
770            to_us_msat: other.to_us_msat.map(|a| a.msat),
771            total_msat: other.total_msat.map(|a| a.msat),
772            spendable_msat: other.spendable_msat.map(|a| a.msat),
773            receivable_msat: other.receivable_msat.map(|a| a.msat),
774        }
775    }
776}
777
778// ============================================================
779// ListFunds response types
780// ============================================================
781
782#[allow(unused)]
783#[derive(Clone, uniffi::Record)]
784pub struct ListFundsResponse {
785    pub outputs: Vec<FundOutput>,
786    pub channels: Vec<FundChannel>,
787}
788
789#[allow(unused)]
790#[derive(Clone, uniffi::Record)]
791pub struct FundOutput {
792    pub txid: Vec<u8>,
793    pub output: u32,
794    pub amount_msat: u64,
795    pub status: OutputStatus,
796    pub address: Option<String>,
797    pub blockheight: Option<u32>,
798}
799
800#[derive(Clone, uniffi::Enum)]
801pub enum OutputStatus {
802    Unconfirmed,
803    Confirmed,
804    Spent,
805    Immature,
806}
807
808impl OutputStatus {
809    fn from_i32(value: i32) -> Self {
810        match value {
811            0 => OutputStatus::Unconfirmed,
812            1 => OutputStatus::Confirmed,
813            2 => OutputStatus::Spent,
814            3 => OutputStatus::Immature,
815            _ => OutputStatus::Unconfirmed, // Default fallback
816        }
817    }
818}
819
820#[allow(unused)]
821#[derive(Clone, uniffi::Record)]
822pub struct FundChannel {
823    pub peer_id: Vec<u8>,
824    pub our_amount_msat: u64,
825    pub amount_msat: u64,
826    pub funding_txid: Vec<u8>,
827    pub funding_output: u32,
828    pub connected: bool,
829    pub state: ChannelState,
830    pub short_channel_id: Option<String>,
831    pub channel_id: Option<Vec<u8>>,
832}
833
834impl From<clnpb::ListfundsResponse> for ListFundsResponse {
835    fn from(other: clnpb::ListfundsResponse) -> Self {
836        Self {
837            outputs: other.outputs.into_iter().map(|o| o.into()).collect(),
838            channels: other.channels.into_iter().map(|c| c.into()).collect(),
839        }
840    }
841}
842
843impl From<clnpb::ListfundsOutputs> for FundOutput {
844    fn from(other: clnpb::ListfundsOutputs) -> Self {
845        let status = OutputStatus::from_i32(other.status);
846        Self {
847            txid: other.txid,
848            output: other.output,
849            amount_msat: other.amount_msat.map(|a| a.msat).unwrap_or(0),
850            status,
851            address: other.address,
852            blockheight: other.blockheight,
853        }
854    }
855}
856
857impl From<clnpb::ListfundsChannels> for FundChannel {
858    fn from(other: clnpb::ListfundsChannels) -> Self {
859        let state = ChannelState::from_i32(other.state);
860        Self {
861            peer_id: other.peer_id,
862            our_amount_msat: other.our_amount_msat.map(|a| a.msat).unwrap_or(0),
863            amount_msat: other.amount_msat.map(|a| a.msat).unwrap_or(0),
864            funding_txid: other.funding_txid,
865            funding_output: other.funding_output,
866            connected: other.connected,
867            state,
868            short_channel_id: other.short_channel_id,
869            channel_id: other.channel_id,
870        }
871    }
872}
873
874// ============================================================
875// Shared pagination types
876// ============================================================
877
878/// Index field used by CLN's paginated list RPCs.
879#[derive(Clone, uniffi::Enum)]
880pub enum ListIndex {
881    CREATED,
882    UPDATED,
883}
884
885impl ListIndex {
886    fn to_i32(&self) -> i32 {
887        match self {
888            ListIndex::CREATED => 0,
889            ListIndex::UPDATED => 1,
890        }
891    }
892}
893
894// ============================================================
895// ListInvoices response types
896// ============================================================
897
898#[derive(Clone, uniffi::Enum)]
899pub enum InvoiceStatus {
900    UNPAID,
901    PAID,
902    EXPIRED,
903}
904
905impl From<i32> for InvoiceStatus {
906    fn from(i: i32) -> Self {
907        match i {
908            0 => InvoiceStatus::UNPAID,
909            1 => InvoiceStatus::PAID,
910            2 => InvoiceStatus::EXPIRED,
911            o => panic!("Unknown invoice status {}", o),
912        }
913    }
914}
915
916#[derive(Clone, uniffi::Record)]
917pub struct Invoice {
918    pub label: String,
919    pub description: String,
920    pub payment_hash: Vec<u8>,
921    pub status: InvoiceStatus,
922    pub amount_msat: Option<u64>,
923    pub amount_received_msat: Option<u64>,
924    pub bolt11: Option<String>,
925    pub bolt12: Option<String>,
926    pub paid_at: Option<u64>,
927    pub expires_at: u64,
928    pub payment_preimage: Option<Vec<u8>>,
929    pub destination_pubkey: Option<Vec<u8>>,
930}
931
932/// Extract the payee public key from a BOLT11 invoice string.
933fn pubkey_from_bolt11(bolt11: &str) -> Option<Vec<u8>> {
934    let invoice: Bolt11Invoice = bolt11.parse().ok()?;
935    Some(invoice.recover_payee_pub_key().serialize().to_vec())
936}
937
938impl From<clnpb::ListinvoicesInvoices> for Invoice {
939    fn from(other: clnpb::ListinvoicesInvoices) -> Self {
940        let destination_pubkey = other.bolt11.as_deref().and_then(pubkey_from_bolt11);
941        Self {
942            label: other.label,
943            description: other.description.unwrap_or_default(),
944            payment_hash: other.payment_hash,
945            status: other.status.into(),
946            amount_msat: other.amount_msat.map(|a| a.msat),
947            amount_received_msat: other.amount_received_msat.map(|a| a.msat),
948            bolt11: other.bolt11,
949            bolt12: other.bolt12,
950            paid_at: other.paid_at,
951            expires_at: other.expires_at,
952            payment_preimage: other.payment_preimage,
953            destination_pubkey,
954        }
955    }
956}
957
958#[derive(Clone, uniffi::Record)]
959pub struct ListInvoicesResponse {
960    pub invoices: Vec<Invoice>,
961}
962
963impl From<clnpb::ListinvoicesResponse> for ListInvoicesResponse {
964    fn from(other: clnpb::ListinvoicesResponse) -> Self {
965        Self {
966            invoices: other.invoices.into_iter().map(|i| i.into()).collect(),
967        }
968    }
969}
970
971// ============================================================
972// ListPays response types
973// ============================================================
974
975#[derive(Clone, uniffi::Record)]
976pub struct Pay {
977    pub payment_hash: Vec<u8>,
978    pub status: PayStatus,
979    pub destination_pubkey: Option<Vec<u8>>,
980    pub amount_msat: Option<u64>,
981    pub amount_sent_msat: Option<u64>,
982    pub label: Option<String>,
983    pub bolt11: Option<String>,
984    pub description: Option<String>,
985    pub bolt12: Option<String>,
986    pub preimage: Option<Vec<u8>>,
987    pub created_at: u64,
988    pub completed_at: Option<u64>,
989    pub number_of_parts: Option<u64>,
990}
991
992impl From<clnpb::ListpaysPays> for Pay {
993    fn from(other: clnpb::ListpaysPays) -> Self {
994        let status = match other.status {
995            0 => PayStatus::PENDING,  // ListpaysPaysStatus::PENDING = 0
996            1 => PayStatus::FAILED,   // ListpaysPaysStatus::FAILED = 1
997            2 => PayStatus::COMPLETE, // ListpaysPaysStatus::COMPLETE = 2
998            o => panic!("Unknown listpays status {}", o),
999        };
1000        Self {
1001            payment_hash: other.payment_hash,
1002            status,
1003            destination_pubkey: other.destination,
1004            amount_msat: other.amount_msat.map(|a| a.msat),
1005            amount_sent_msat: other.amount_sent_msat.map(|a| a.msat),
1006            label: other.label,
1007            bolt11: other.bolt11,
1008            description: other.description,
1009            bolt12: other.bolt12,
1010            preimage: other.preimage,
1011            created_at: other.created_at,
1012            completed_at: other.completed_at,
1013            number_of_parts: other.number_of_parts,
1014        }
1015    }
1016}
1017
1018#[derive(Clone, uniffi::Record)]
1019pub struct ListPaysResponse {
1020    pub pays: Vec<Pay>,
1021}
1022
1023impl From<clnpb::ListpaysResponse> for ListPaysResponse {
1024    fn from(other: clnpb::ListpaysResponse) -> Self {
1025        Self {
1026            pays: other.pays.into_iter().map(|p| p.into()).collect(),
1027        }
1028    }
1029}
1030
1031// ============================================================
1032// Unified list_payments request/response types
1033// ============================================================
1034
1035#[derive(Clone, Default, uniffi::Record)]
1036pub struct ListPaymentsRequest {
1037    /// Filter by payment type (Sent, Received). None or empty = all.
1038    pub filters: Option<Vec<PaymentTypeFilter>>,
1039    /// Include only payments after this epoch timestamp (seconds).
1040    pub from_timestamp: Option<u64>,
1041    /// Include only payments before this epoch timestamp (seconds).
1042    pub to_timestamp: Option<u64>,
1043    /// Include failed payments. Default: false.
1044    pub include_failures: Option<bool>,
1045    /// Pagination offset.
1046    pub offset: Option<u32>,
1047    /// Pagination limit.
1048    pub limit: Option<u32>,
1049}
1050
1051#[derive(Clone, uniffi::Enum)]
1052pub enum PaymentTypeFilter {
1053    Sent,
1054    Received,
1055}
1056
1057#[derive(Clone, uniffi::Record)]
1058pub struct Payment {
1059    pub id: String,
1060    pub payment_type: PaymentType,
1061    pub payment_time: u64,
1062    pub amount_msat: u64,
1063    pub fee_msat: u64,
1064    pub status: PaymentStatus,
1065    pub description: Option<String>,
1066    pub bolt11: Option<String>,
1067    pub preimage: Option<Vec<u8>>,
1068    pub destination: Option<Vec<u8>>,
1069}
1070
1071#[derive(Clone, uniffi::Enum)]
1072pub enum PaymentType {
1073    Sent,
1074    Received,
1075}
1076
1077#[derive(Clone, uniffi::Enum)]
1078pub enum PaymentStatus {
1079    Pending,
1080    Complete,
1081    Failed,
1082}
1083
1084impl From<clnpb::ListinvoicesInvoices> for Payment {
1085    fn from(inv: clnpb::ListinvoicesInvoices) -> Self {
1086        let status = match inv.status() {
1087            clnpb::listinvoices_invoices::ListinvoicesInvoicesStatus::Paid => {
1088                PaymentStatus::Complete
1089            }
1090            clnpb::listinvoices_invoices::ListinvoicesInvoicesStatus::Expired => {
1091                PaymentStatus::Failed
1092            }
1093            clnpb::listinvoices_invoices::ListinvoicesInvoicesStatus::Unpaid => {
1094                PaymentStatus::Pending
1095            }
1096        };
1097
1098        let payment_time = inv.paid_at.unwrap_or(inv.expires_at);
1099        let amount_msat = inv
1100            .amount_received_msat
1101            .or(inv.amount_msat)
1102            .map(|a| a.msat)
1103            .unwrap_or(0);
1104
1105        Payment {
1106            id: inv.payment_hash.iter().map(|b| format!("{:02x}", b)).collect::<String>(),
1107            payment_type: PaymentType::Received,
1108            payment_time,
1109            amount_msat,
1110            fee_msat: 0,
1111            status,
1112            description: inv.description,
1113            bolt11: inv.bolt11,
1114            preimage: inv.payment_preimage,
1115            destination: None,
1116        }
1117    }
1118}
1119
1120impl From<clnpb::ListpaysPays> for Payment {
1121    fn from(pay: clnpb::ListpaysPays) -> Self {
1122        let status = match pay.status() {
1123            clnpb::listpays_pays::ListpaysPaysStatus::Complete => PaymentStatus::Complete,
1124            clnpb::listpays_pays::ListpaysPaysStatus::Failed => PaymentStatus::Failed,
1125            clnpb::listpays_pays::ListpaysPaysStatus::Pending => PaymentStatus::Pending,
1126        };
1127
1128        let payment_time = pay.completed_at.unwrap_or(pay.created_at);
1129        let amount_msat = pay.amount_msat.as_ref().map(|a| a.msat).unwrap_or(0);
1130        let amount_sent_msat = pay.amount_sent_msat.as_ref().map(|a| a.msat).unwrap_or(0);
1131        let fee_msat = amount_sent_msat.saturating_sub(amount_msat);
1132
1133        Payment {
1134            id: pay.payment_hash.iter().map(|b| format!("{:02x}", b)).collect::<String>(),
1135            payment_type: PaymentType::Sent,
1136            payment_time,
1137            amount_msat,
1138            fee_msat,
1139            status,
1140            description: pay.description,
1141            bolt11: pay.bolt11,
1142            preimage: pay.preimage,
1143            destination: pay.destination,
1144        }
1145    }
1146}
1147
1148// ============================================================
1149// NodeEvent streaming types
1150// ============================================================
1151
1152/// A stream of node events. Call `next()` to receive the next event.
1153///
1154/// The stream is backed by a gRPC streaming connection to the node.
1155/// Each call to `next()` blocks the calling thread until an event is
1156/// available, but does not block the tokio runtime - other node
1157/// operations can proceed concurrently from other threads.
1158#[derive(uniffi::Object)]
1159pub struct NodeEventStream {
1160    inner: Mutex<tonic::codec::Streaming<glpb::NodeEvent>>,
1161}
1162
1163#[uniffi::export]
1164impl NodeEventStream {
1165    /// Get the next event from the stream.
1166    ///
1167    /// Blocks the calling thread until an event is available or the
1168    /// stream ends. Returns `None` when the stream is exhausted or
1169    /// the connection is lost.
1170    pub fn next(&self) -> Result<Option<NodeEvent>, Error> {
1171        let mut stream = self.inner.lock().map_err(|e| Error::Other(e.to_string()))?;
1172        match exec(stream.message()) {
1173            Ok(Some(event)) => Ok(Some(event.into())),
1174            Ok(None) => Ok(None),
1175            Err(e) if e.code() == tonic::Code::Unknown => Ok(None),
1176            Err(e) => Err(Error::Rpc(e.to_string())),
1177        }
1178    }
1179}
1180
1181/// A real-time event from the node.
1182#[derive(Clone, uniffi::Enum)]
1183pub enum NodeEvent {
1184    /// An invoice was paid.
1185    InvoicePaid { details: InvoicePaidEvent },
1186    /// An unknown event type was received. This can happen if the
1187    /// server sends a new event type that this client doesn't know about.
1188    Unknown,
1189}
1190
1191/// Details of a paid invoice.
1192#[derive(Clone, uniffi::Record)]
1193pub struct InvoicePaidEvent {
1194    /// The payment hash of the paid invoice.
1195    pub payment_hash: Vec<u8>,
1196    /// The bolt11 invoice string.
1197    pub bolt11: String,
1198    /// The preimage that proves payment.
1199    pub preimage: Vec<u8>,
1200    /// The label assigned to the invoice.
1201    pub label: String,
1202    /// Amount received in millisatoshis.
1203    pub amount_msat: u64,
1204}
1205
1206impl From<glpb::NodeEvent> for NodeEvent {
1207    fn from(other: glpb::NodeEvent) -> Self {
1208        match other.event {
1209            Some(glpb::node_event::Event::InvoicePaid(paid)) => NodeEvent::InvoicePaid {
1210                details: InvoicePaidEvent {
1211                    payment_hash: paid.payment_hash,
1212                    bolt11: paid.bolt11,
1213                    preimage: paid.preimage,
1214                    label: paid.label,
1215                    amount_msat: paid.amount_msat,
1216                },
1217            },
1218            None => NodeEvent::Unknown,
1219        }
1220    }
1221}