1use crate::{credentials::Credentials, signer::Handle, util::exec, Error};
2use std::str::FromStr;
3use std::sync::atomic::{AtomicBool, Ordering};
4use gl_client::credentials::NodeIdProvider;
5use gl_client::lnurl::models::LnUrlHttpClient as _;
6use gl_client::node::{Client as GlClient, ClnClient, Node as ClientNode};
7use gl_client::pb::{self as glpb, cln as clnpb};
8use lightning_invoice::Bolt11Invoice;
9use std::sync::{Arc, Mutex};
10use tokio::sync::OnceCell;
11
12#[derive(uniffi::Object)]
15#[allow(unused)]
16pub struct Node {
17 inner: ClientNode,
18 cln_client: OnceCell<ClnClient>,
19 gl_client: OnceCell<GlClient>,
20 stored_credentials: Option<Credentials>,
21 signer_handle: Option<Handle>,
22 disconnected: AtomicBool,
23 event_task: Mutex<Option<tokio::task::JoinHandle<()>>>,
27 network: gl_client::bitcoin::Network,
28}
29
30impl Drop for Node {
31 fn drop(&mut self) {
32 if let Ok(mut guard) = self.event_task.lock() {
33 if let Some(handle) = guard.take() {
34 handle.abort();
35 }
36 }
37 }
38}
39
40impl Node {
41 pub fn signerless(credentials: Credentials) -> Result<Self, Error> {
51 let node_id = credentials
52 .inner
53 .node_id()
54 .map_err(|_e| Error::unparseable_creds())?;
55 let inner = ClientNode::new(node_id, credentials.inner.clone())
56 .expect("infallible client instantiation");
57
58 let cln_client = OnceCell::const_new();
59 let gl_client = OnceCell::const_new();
60 Ok(Node {
61 inner,
62 cln_client,
63 gl_client,
64 stored_credentials: Some(credentials),
65 signer_handle: None,
66 disconnected: AtomicBool::new(false),
67 event_task: Mutex::new(None),
68 network: gl_client::bitcoin::Network::Bitcoin,
69 })
70 }
71}
72
73#[uniffi::export]
74impl Node {
75
76 pub fn stop(&self) -> Result<(), Error> {
78 self.check_connected()?;
79 let mut cln_client = exec(self.get_cln_client())?.clone();
80
81 let req = clnpb::StopRequest {};
82
83 let _ = exec(cln_client.stop(req));
87 Ok(())
88 }
89
90 pub fn credentials(&self) -> Result<Vec<u8>, Error> {
93 match &self.stored_credentials {
94 Some(creds) => creds.save(),
95 None => Err(Error::other(
96 "No credentials stored. Use register/recover/connect to create a Node with credentials.".to_string(),
97 )),
98 }
99 }
100
101 pub fn disconnect(&self) -> Result<(), Error> {
105 self.disconnected.store(true, Ordering::Relaxed);
106 if let Some(ref handle) = self.signer_handle {
107 handle.try_stop();
108 }
109 Ok(())
110 }
111
112 pub fn receive(
123 &self,
124 label: String,
125 description: String,
126 amount_msat: Option<u64>,
127 ) -> Result<ReceiveResponse, Error> {
128 self.check_connected()?;
129 let mut gl_client = exec(self.get_gl_client())?.clone();
130
131 let req = gl_client::pb::LspInvoiceRequest {
132 amount_msat: amount_msat.unwrap_or_default(),
133 description: description,
134 label: label,
135 lsp_id: "".to_owned(),
136 token: "".to_owned(),
137 };
138 let res = exec(gl_client.lsp_invoice(req))
139 .map_err(|s| Error::rpc(s.to_string()))?
140 .into_inner();
141 Ok(ReceiveResponse {
142 bolt11: res.bolt11,
143 opening_fee_msat: res.opening_fee_msat,
144 })
145 }
146
147 pub fn send(&self, invoice: String, amount_msat: Option<u64>) -> Result<SendResponse, Error> {
148 self.check_connected()?;
149 let mut cln_client = exec(self.get_cln_client())?.clone();
150 let req = clnpb::PayRequest {
151 amount_msat: match amount_msat {
152 Some(a) => Some(clnpb::Amount { msat: a }),
153 None => None,
154 },
155
156 bolt11: invoice,
157 description: None,
158 exclude: vec![],
159 exemptfee: None,
160 label: None,
161 localinvreqid: None,
162 maxdelay: None,
163 maxfee: None,
164 maxfeepercent: None,
165 partial_msat: None,
166 retry_for: None,
167 riskfactor: None,
168 };
169 exec(cln_client.pay(req))
170 .map_err(|e| Error::rpc(e.to_string()))
171 .map(|r| r.into_inner().into())
172 }
173
174 pub fn onchain_send(
194 &self,
195 destination: String,
196 amount_or_all: String,
197 sat_per_vbyte: Option<u32>,
198 utxos: Option<Vec<Outpoint>>,
199 ) -> Result<OnchainSendResponse, Error> {
200 self.check_connected()?;
201 let mut cln_client = exec(self.get_cln_client())?.clone();
202
203 let satoshi = parse_amount_or_all(&amount_or_all)?;
204
205 let req = clnpb::WithdrawRequest {
206 destination,
207 minconf: None,
208 feerate: sat_per_vbyte.map(feerate_perkw_from_sat_per_vbyte),
209 satoshi: Some(satoshi),
210 utxos: utxos
211 .unwrap_or_default()
212 .into_iter()
213 .map(outpoint_to_pb)
214 .collect::<Result<Vec<_>, _>>()?,
215 };
216
217 exec(cln_client.withdraw(req))
218 .map_err(|e| Error::rpc(e.to_string()))
219 .map(|r| r.into_inner().into())
220 }
221
222 pub fn prepare_onchain_send(
253 &self,
254 destination: String,
255 amount_or_all: String,
256 sat_per_vbyte: Option<u32>,
257 ) -> Result<PreparedOnchainSend, Error> {
258 self.check_connected()?;
259 let cln_client = exec(self.get_cln_client())?.clone();
260
261 let satoshi = parse_amount_or_all(&amount_or_all)?;
262 let is_sweep = matches!(satoshi.value, Some(clnpb::amount_or_all::Value::All(true)));
263
264 let startweight = BASE_TX_CORE_WEIGHT + output_weight_for_address(&destination);
271
272 let feerate = match sat_per_vbyte {
273 Some(rate) => feerate_perkw_from_sat_per_vbyte(rate),
274 None => clnpb::Feerate {
275 style: Some(clnpb::feerate::Style::Normal(true)),
276 },
277 };
278
279 let req = clnpb::FundpsbtRequest {
280 satoshi: Some(satoshi),
281 feerate: Some(feerate),
282 startweight,
283 reserve: Some(0),
286 minconf: None,
287 locktime: None,
288 min_witness_weight: None,
289 excess_as_change: Some(!is_sweep),
294 nonwrapped: None,
295 opening_anchor_channel: None,
296 };
297
298 let (fund_res, feerates_res) = exec(async {
304 let mut c_fund = cln_client.clone();
305 let mut c_rates = cln_client.clone();
306 tokio::join!(
307 c_fund.fund_psbt(req),
308 c_rates.feerates(clnpb::FeeratesRequest {
309 style: clnpb::feerates_request::FeeratesStyle::Perkw as i32,
310 }),
311 )
312 });
313
314 if let (Some(rate), Ok(rates)) = (sat_per_vbyte, feerates_res.as_ref())
318 && let Some(perkw) = rates.get_ref().perkw.as_ref()
319 {
320 let min_sat_per_vbyte =
321 sat_per_vbyte_from_perkw(perkw.min_acceptable).max(1);
322 if (rate as u64) < min_sat_per_vbyte {
323 return Err(Error::argument(
324 "sat_per_vbyte",
325 format!(
326 "{} sat/vbyte is below the network minimum of {} sat/vbyte",
327 rate, min_sat_per_vbyte
328 ),
329 ));
330 }
331 }
332
333 let res = fund_res
334 .map_err(|e| Error::rpc(e.to_string()))?
335 .into_inner();
336
337 let psbt = bitcoin::Psbt::from_str(&res.psbt)
342 .map_err(|e| Error::rpc(format!("invalid psbt from fund_psbt: {}", e)))?;
343 let utxos: Vec<Outpoint> = psbt
344 .unsigned_tx
345 .input
346 .iter()
347 .map(|tx_in| Outpoint {
348 txid: tx_in.previous_output.txid.to_string(),
349 vout: tx_in.previous_output.vout,
350 })
351 .collect();
352
353 let fee_sat: u64 =
358 (res.estimated_final_weight as u64 * res.feerate_per_kw as u64) / 1000;
359
360 let mut total_input_sat: u64 = 0;
368 for (i, input) in psbt.inputs.iter().enumerate() {
369 let value = if let Some(ref txout) = input.witness_utxo {
370 txout.value
371 } else if let Some(ref tx) = input.non_witness_utxo {
372 let vout = psbt.unsigned_tx.input[i].previous_output.vout as usize;
373 tx.output
374 .get(vout)
375 .map(|o| o.value)
376 .ok_or_else(|| {
377 Error::rpc("psbt non_witness_utxo missing vout")
378 })?
379 } else {
380 return Err(Error::rpc(format!(
381 "psbt input {} has no witness_utxo or non_witness_utxo",
382 i
383 )));
384 };
385 total_input_sat = total_input_sat.saturating_add(value.to_sat());
386 }
387
388 let recipient_sat: u64 = if is_sweep {
389 res.excess_msat.as_ref().map(|a| a.msat).unwrap_or(0) / 1000
395 } else {
396 match parse_amount_or_all(&amount_or_all)?.value {
397 Some(clnpb::amount_or_all::Value::Amount(a)) => a.msat / 1000,
398 _ => 0,
399 }
400 };
401
402 let effective_sat_per_vbyte: u32 =
406 (res.feerate_per_kw as u64).div_ceil(250) as u32;
407
408 Ok(PreparedOnchainSend {
409 utxos,
410 total_input_sat,
411 fee_sat,
412 recipient_sat,
413 sat_per_vbyte: effective_sat_per_vbyte,
414 })
415 }
416
417 pub fn onchain_balance_state(&self) -> Result<OnchainBalanceState, Error> {
440 self.check_connected()?;
441 let cln_client = exec(self.get_cln_client())?.clone();
442
443 let (funds_res, channels_res, probe_res) = exec(async {
448 let mut c_funds = cln_client.clone();
449 let mut c_channels = cln_client.clone();
450 let mut c_probe = cln_client.clone();
451 let probe_req = clnpb::FundpsbtRequest {
452 satoshi: Some(clnpb::AmountOrAll {
453 value: Some(clnpb::amount_or_all::Value::All(true)),
454 }),
455 feerate: Some(clnpb::Feerate {
456 style: Some(clnpb::feerate::Style::Normal(true)),
457 }),
458 startweight: BASE_TX_CORE_WEIGHT + 124,
463 reserve: Some(0),
464 minconf: None,
465 locktime: None,
466 min_witness_weight: None,
467 excess_as_change: Some(false),
468 nonwrapped: None,
469 opening_anchor_channel: None,
470 };
471 tokio::join!(
472 c_funds.list_funds(clnpb::ListfundsRequest { spent: None }),
473 c_channels.list_peer_channels(clnpb::ListpeerchannelsRequest { id: None }),
474 c_probe.fund_psbt(probe_req),
475 )
476 });
477
478 let funds: ListFundsResponse = funds_res
479 .map_err(|e| Error::rpc(e.to_string()))?
480 .into_inner()
481 .into();
482 let channels: ListPeerChannelsResponse = channels_res
483 .map_err(|e| Error::rpc(e.to_string()))?
484 .into_inner()
485 .into();
486
487 let mut confirmed_sat: u64 = 0;
488 let mut unconfirmed_sat: u64 = 0;
489 let mut immature_sat: u64 = 0;
490 for output in &funds.outputs {
491 if output.reserved {
492 continue;
493 }
494 let value_sat = output.amount_msat / 1000;
495 match output.status {
496 OutputStatus::Confirmed => confirmed_sat += value_sat,
497 OutputStatus::Unconfirmed => unconfirmed_sat += value_sat,
498 OutputStatus::Immature => immature_sat += value_sat,
499 OutputStatus::Spent => {}
500 }
501 }
502
503 let mut pending_close_sat: u64 = 0;
504 for ch in &channels.channels {
505 if channel_payout_still_pending(ch) {
506 pending_close_sat += ch.to_us_msat.unwrap_or(0) / 1000;
507 }
508 }
509
510 let reserve_sat = match probe_res {
516 Ok(resp) => {
517 let resp = resp.into_inner();
518 let total_input_sat = bitcoin::Psbt::from_str(&resp.psbt)
522 .ok()
523 .map(|p| {
524 p.inputs
525 .iter()
526 .filter_map(|i| {
527 i.witness_utxo.as_ref().map(|t| t.value.to_sat())
528 })
529 .sum::<u64>()
530 })
531 .unwrap_or(0);
532 let excess_sat = resp
533 .excess_msat
534 .as_ref()
535 .map(|a| a.msat / 1000)
536 .unwrap_or(0);
537 let fee_sat = (resp.estimated_final_weight as u64
538 * resp.feerate_per_kw as u64)
539 / 1000;
540 total_input_sat
541 .saturating_sub(excess_sat)
542 .saturating_sub(fee_sat)
543 }
544 Err(_) => 0,
548 };
549
550 Ok(classify_onchain_balance(
551 confirmed_sat,
552 reserve_sat,
553 unconfirmed_sat,
554 immature_sat,
555 pending_close_sat,
556 ))
557 }
558
559 pub fn onchain_fee_rates(&self) -> Result<OnchainFeeRates, Error> {
567 self.check_connected()?;
568 let mut cln_client = exec(self.get_cln_client())?.clone();
569
570 let req = clnpb::FeeratesRequest {
571 style: clnpb::feerates_request::FeeratesStyle::Perkw as i32,
572 };
573 let res = exec(cln_client.feerates(req))
574 .map_err(|e| Error::rpc(e.to_string()))?
575 .into_inner();
576 Ok(compute_fee_rates(res.perkw.as_ref()))
577 }
578
579 pub fn onchain_receive(&self) -> Result<OnchainReceiveResponse, Error> {
585 self.check_connected()?;
586 let mut cln_client = exec(self.get_cln_client())?.clone();
587
588 let req = clnpb::NewaddrRequest {
589 addresstype: Some(clnpb::newaddr_request::NewaddrAddresstype::All.into()),
590 };
591
592 let res = exec(cln_client.new_addr(req))
593 .map_err(|e| Error::rpc(e.to_string()))?
594 .into_inner();
595 Ok(res.into())
596 }
597
598 pub fn get_info(&self) -> Result<GetInfoResponse, Error> {
603 self.check_connected()?;
604 let mut cln_client = exec(self.get_cln_client())?.clone();
605
606 let req = clnpb::GetinfoRequest {};
607
608 let res = exec(cln_client.getinfo(req))
609 .map_err(|e| Error::rpc(e.to_string()))?
610 .into_inner();
611 Ok(res.into())
612 }
613
614 pub fn list_peers(&self) -> Result<ListPeersResponse, Error> {
619 self.check_connected()?;
620 let mut cln_client = exec(self.get_cln_client())?.clone();
621
622 let req = clnpb::ListpeersRequest {
623 id: None,
624 level: None,
625 };
626
627 let res = exec(cln_client.list_peers(req))
628 .map_err(|e| Error::rpc(e.to_string()))?
629 .into_inner();
630 Ok(res.into())
631 }
632
633 pub fn list_peer_channels(&self) -> Result<ListPeerChannelsResponse, Error> {
638 self.check_connected()?;
639 let mut cln_client = exec(self.get_cln_client())?.clone();
640
641 let req = clnpb::ListpeerchannelsRequest { id: None };
642
643 let res = exec(cln_client.list_peer_channels(req))
644 .map_err(|e| Error::rpc(e.to_string()))?
645 .into_inner();
646 Ok(res.into())
647 }
648
649 pub fn list_funds(&self) -> Result<ListFundsResponse, Error> {
654 self.check_connected()?;
655 let mut cln_client = exec(self.get_cln_client())?.clone();
656
657 let req = clnpb::ListfundsRequest { spent: None };
658
659 let res = exec(cln_client.list_funds(req))
660 .map_err(|e| Error::rpc(e.to_string()))?
661 .into_inner();
662 Ok(res.into())
663 }
664
665 pub fn node_state(&self) -> Result<NodeState, Error> {
670 self.check_connected()?;
671 let cln_client = exec(self.get_cln_client())?.clone();
672
673 let (info_res, channels_res, funds_res) = exec(async {
674 let mut c_info = cln_client.clone();
675 let mut c_channels = cln_client.clone();
676 let mut c_funds = cln_client.clone();
677 tokio::join!(
678 c_info.getinfo(clnpb::GetinfoRequest {}),
679 c_channels.list_peer_channels(clnpb::ListpeerchannelsRequest { id: None }),
680 c_funds.list_funds(clnpb::ListfundsRequest { spent: None }),
681 )
682 });
683
684 let info: GetInfoResponse = info_res
685 .map_err(|e| Error::rpc(e.to_string()))?
686 .into_inner()
687 .into();
688 let channels: ListPeerChannelsResponse = channels_res
689 .map_err(|e| Error::rpc(e.to_string()))?
690 .into_inner()
691 .into();
692 let funds: ListFundsResponse = funds_res
693 .map_err(|e| Error::rpc(e.to_string()))?
694 .into_inner()
695 .into();
696
697 let mut channels_balance_msat: u64 = 0;
698 let mut max_payable_msat: u64 = 0;
699 let mut total_channel_capacity_msat: u64 = 0;
700 let mut max_receivable_single_payment_msat: u64 = 0;
701 let mut total_inbound_liquidity_msat: u64 = 0;
702 let mut pending_onchain_balance_msat: u64 = 0;
703 let mut connected_channel_peer_set: std::collections::HashSet<String> =
704 std::collections::HashSet::new();
705
706 for ch in &channels.channels {
707 if ch.state.is_open() {
708 channels_balance_msat += ch.to_us_msat.unwrap_or(0);
709 max_payable_msat += ch.spendable_msat.unwrap_or(0);
710 total_channel_capacity_msat += ch.total_msat.unwrap_or(0);
711 let receivable = ch.receivable_msat.unwrap_or(0);
712 if receivable > max_receivable_single_payment_msat {
713 max_receivable_single_payment_msat = receivable;
714 }
715 total_inbound_liquidity_msat += receivable;
716 }
717 if channel_payout_still_pending(ch) {
718 pending_onchain_balance_msat += ch.to_us_msat.unwrap_or(0);
719 }
720 if ch.peer_connected {
721 connected_channel_peer_set.insert(ch.peer_id.clone());
722 }
723 }
724
725 let connected_channel_peers: Vec<String> =
726 connected_channel_peer_set.into_iter().collect();
727
728 let max_chan_reserve_msat =
729 channels_balance_msat.saturating_sub(max_payable_msat);
730
731 let mut onchain_balance_msat: u64 = 0;
732 let mut unconfirmed_onchain_balance_msat: u64 = 0;
733 let mut immature_onchain_balance_msat: u64 = 0;
734 let mut utxos: Vec<FundOutput> = Vec::with_capacity(funds.outputs.len());
735 for output in &funds.outputs {
736 if !matches!(output.status, OutputStatus::Spent) {
737 utxos.push(output.clone());
738 }
739 if output.reserved {
740 continue;
741 }
742 match output.status {
743 OutputStatus::Confirmed => onchain_balance_msat += output.amount_msat,
744 OutputStatus::Unconfirmed => {
745 unconfirmed_onchain_balance_msat += output.amount_msat
746 }
747 OutputStatus::Immature => {
748 immature_onchain_balance_msat += output.amount_msat
749 }
750 OutputStatus::Spent => {}
751 }
752 }
753
754 let total_onchain_msat = onchain_balance_msat
755 .saturating_add(unconfirmed_onchain_balance_msat)
756 .saturating_add(immature_onchain_balance_msat);
757 let total_balance_msat = channels_balance_msat
758 .saturating_add(total_onchain_msat)
759 .saturating_add(pending_onchain_balance_msat);
760 let spendable_balance_msat = max_payable_msat.saturating_add(onchain_balance_msat);
761
762
763 Ok(NodeState {
764 id: info.id,
765 block_height: info.blockheight,
766 network: info.network,
767 version: info.version,
768 alias: info.alias,
769 color: info.color,
770 num_active_channels: info.num_active_channels,
771 num_pending_channels: info.num_pending_channels,
772 num_inactive_channels: info.num_inactive_channels,
773 channels_balance_msat,
774 max_payable_msat,
775 total_channel_capacity_msat,
776 max_chan_reserve_msat,
777 onchain_balance_msat,
778 unconfirmed_onchain_balance_msat,
779 immature_onchain_balance_msat,
780 pending_onchain_balance_msat,
781 max_receivable_single_payment_msat,
782 total_inbound_liquidity_msat,
783 connected_channel_peers,
784 utxos,
785 total_onchain_msat,
786 total_balance_msat,
787 spendable_balance_msat,
788 })
789 }
790
791 pub fn list_invoices(
794 &self,
795 label: Option<String>,
796 invstring: Option<String>,
797 payment_hash: Option<Vec<u8>>,
798 offer_id: Option<String>,
799 index: Option<ListIndex>,
800 start: Option<u64>,
801 limit: Option<u32>,
802 ) -> Result<ListInvoicesResponse, Error> {
803 self.check_connected()?;
804 let mut cln_client = exec(self.get_cln_client())?.clone();
805
806 let req = clnpb::ListinvoicesRequest {
807 label,
808 invstring,
809 payment_hash,
810 offer_id,
811 index: index.map(|i| i.to_i32()),
812 start,
813 limit,
814 };
815
816 let res = exec(cln_client.list_invoices(req))
817 .map_err(|e| Error::rpc(e.to_string()))?
818 .into_inner();
819 Ok(res.into())
820 }
821
822 pub fn list_pays(
825 &self,
826 bolt11: Option<String>,
827 payment_hash: Option<Vec<u8>>,
828 status: Option<PayStatus>,
829 index: Option<ListIndex>,
830 start: Option<u64>,
831 limit: Option<u32>,
832 ) -> Result<ListPaysResponse, Error> {
833 self.check_connected()?;
834 let mut cln_client = exec(self.get_cln_client())?.clone();
835
836 let cln_status = status.map(|s| match s {
838 PayStatus::PENDING => 0,
839 PayStatus::COMPLETE => 1,
840 PayStatus::FAILED => 2,
841 });
842
843 let req = clnpb::ListpaysRequest {
844 bolt11,
845 payment_hash,
846 status: cln_status,
847 index: index.map(|i| i.to_i32()),
848 start,
849 limit,
850 };
851
852 let res = exec(cln_client.list_pays(req))
853 .map_err(|e| Error::rpc(e.to_string()))?
854 .into_inner();
855 Ok(res.into())
856 }
857
858 pub fn list_payments(&self, req: ListPaymentsRequest) -> Result<Vec<Payment>, Error> {
865 self.check_connected()?;
866 let mut cln_client = exec(self.get_cln_client())?.clone();
867
868 let invoices = exec(cln_client.list_invoices(clnpb::ListinvoicesRequest::default()))
869 .map_err(|e| Error::rpc(e.to_string()))?
870 .into_inner();
871
872 let mut cln_client = exec(self.get_cln_client())?.clone();
873 let pays = exec(cln_client.list_pays(clnpb::ListpaysRequest::default()))
874 .map_err(|e| Error::rpc(e.to_string()))?
875 .into_inner();
876
877 let mut payments: Vec<Payment> = Vec::new();
878
879 let include_received = req
881 .filters
882 .as_ref()
883 .map(|f| f.is_empty() || f.iter().any(|t| matches!(t, PaymentTypeFilter::Received)))
884 .unwrap_or(true);
885
886 let include_sent = req
888 .filters
889 .as_ref()
890 .map(|f| f.is_empty() || f.iter().any(|t| matches!(t, PaymentTypeFilter::Sent)))
891 .unwrap_or(true);
892
893 if include_received {
894 payments.extend(
898 invoices
899 .invoices
900 .into_iter()
901 .filter(|i| {
902 i.status()
903 == clnpb::listinvoices_invoices::ListinvoicesInvoicesStatus::Paid
904 })
905 .map(|i| -> Payment { i.into() }),
906 );
907 }
908 if include_sent {
909 payments.extend(pays.pays.into_iter().map(|p| -> Payment { p.into() }));
910 }
911
912 let include_failures = req.include_failures.unwrap_or(false);
913
914 payments.retain(|p| {
915 if !include_failures && matches!(p.status, PaymentStatus::Failed) {
916 return false;
917 }
918 if let Some(from) = req.from_timestamp {
919 if p.payment_time < from {
920 return false;
921 }
922 }
923 if let Some(to) = req.to_timestamp {
924 if p.payment_time > to {
925 return false;
926 }
927 }
928 true
929 });
930
931 payments.sort_by(|a, b| b.payment_time.cmp(&a.payment_time));
933
934 let offset = req.offset.unwrap_or(0) as usize;
936 let limit = req.limit.unwrap_or(u32::MAX) as usize;
937 let payments = payments.into_iter().skip(offset).take(limit).collect();
938
939 Ok(payments)
940 }
941
942 pub fn stream_node_events(&self) -> Result<Arc<NodeEventStream>, Error> {
952 self.check_connected()?;
953 let mut gl_client = exec(self.get_gl_client())?.clone();
954 let req = glpb::NodeEventsRequest {};
955 let stream = exec(gl_client.stream_node_events(req))
956 .map_err(|e| Error::rpc(e.to_string()))?
957 .into_inner();
958 Ok(Arc::new(NodeEventStream {
959 inner: Mutex::new(stream),
960 }))
961 }
962
963 pub fn generate_diagnostic_data(&self) -> Result<String, Error> {
974 self.check_connected()?;
975
976 let timestamp = std::time::SystemTime::now()
977 .duration_since(std::time::UNIX_EPOCH)
978 .map(|d| d.as_secs())
979 .unwrap_or(0);
980
981 let getinfo = render_section(self.get_info());
982 let listpeerchannels = render_section(self.list_peer_channels());
983 let listfunds = render_section(self.list_funds());
984 let node_state = render_section(self.node_state());
985
986 build_diagnostic_json(
987 timestamp,
988 env!("CARGO_PKG_VERSION"),
989 getinfo,
990 listpeerchannels,
991 listfunds,
992 node_state,
993 )
994 }
995
996 pub fn lnurl_pay(
1008 &self,
1009 request: crate::lnurl::LnUrlPayRequest,
1010 ) -> Result<crate::lnurl::LnUrlPayResult, Error> {
1011 self.check_connected()?;
1012 validate_lnurl_pay_input(&request)?;
1013
1014 let http_client = gl_client::lnurl::models::LnUrlHttpClearnetClient::new();
1015
1016 let comment = request.comment.as_deref();
1018 let (invoice_str, success_action) = match exec(
1019 gl_client::lnurl::pay::fetch_invoice(
1020 &http_client,
1021 &request.data.callback,
1022 request.amount_msat,
1023 comment,
1024 ),
1025 ) {
1026 Ok(v) => v,
1027 Err(e) => {
1028 let msg = e.to_string();
1029 let reason = msg
1030 .strip_prefix(gl_client::lnurl::pay::LNURL_SERVICE_ERROR_PREFIX)
1031 .unwrap_or(&msg)
1032 .to_string();
1033 return Ok(crate::lnurl::LnUrlPayResult::EndpointError {
1034 data: crate::lnurl::LnUrlErrorData { reason },
1035 });
1036 }
1037 };
1038
1039 if let Some(reason) = invoice_network_mismatch(&invoice_str, self.network) {
1040 return Ok(crate::lnurl::LnUrlPayResult::EndpointError {
1041 data: crate::lnurl::LnUrlErrorData { reason },
1042 });
1043 }
1044
1045 let mut cln_client = exec(self.get_cln_client())?.clone();
1047 let pay_response = match exec(cln_client.pay(clnpb::PayRequest {
1048 bolt11: invoice_str.clone(),
1049 ..Default::default()
1050 })) {
1051 Ok(r) => r.into_inner(),
1052 Err(e) => {
1053 let payment_hash = invoice_str
1054 .parse::<Bolt11Invoice>()
1055 .ok()
1056 .map(|inv| inv.payment_hash().to_string())
1057 .unwrap_or_default();
1058 return Ok(crate::lnurl::LnUrlPayResult::PayError {
1059 data: crate::lnurl::LnUrlPayErrorData {
1060 payment_hash,
1061 reason: e.to_string(),
1062 },
1063 });
1064 }
1065 };
1066
1067 let validate_url = request.validate_success_action_url.unwrap_or(true);
1069 let processed_action = match success_action {
1070 Some(action) => {
1071 let processed = action
1072 .process(&pay_response.payment_preimage)
1073 .map_err(|e| Error::other(e.to_string()))?;
1074 if validate_url {
1075 if let gl_client::lnurl::models::ProcessedSuccessAction::Url {
1076 url, ..
1077 } = &processed
1078 {
1079 if let Some(reason) =
1080 url_action_domain_mismatch(&request.data.callback, url)
1081 {
1082 return Err(Error::other(reason));
1083 }
1084 }
1085 }
1086 Some(processed.into())
1087 }
1088 None => None,
1089 };
1090
1091 Ok(crate::lnurl::LnUrlPayResult::EndpointSuccess {
1092 data: crate::lnurl::LnUrlPaySuccessData {
1093 payment_preimage: hex::encode(&pay_response.payment_preimage),
1094 success_action: processed_action,
1095 },
1096 })
1097 }
1098
1099 pub fn lnurl_withdraw(
1109 &self,
1110 request: crate::lnurl::LnUrlWithdrawRequest,
1111 ) -> Result<crate::lnurl::LnUrlWithdrawResult, Error> {
1112 self.check_connected()?;
1113
1114 let http_client = gl_client::lnurl::models::LnUrlHttpClearnetClient::new();
1115
1116 let description = request
1118 .description
1119 .unwrap_or(request.data.default_description.clone());
1120
1121 let invoice_response = self.receive(
1122 format!("lnurl-withdraw-{}", request.data.k1),
1123 description,
1124 Some(request.amount_msat),
1125 )?;
1126
1127 let callback_url = gl_client::lnurl::withdraw::build_withdraw_callback_url(
1129 &request.data.callback,
1130 &request.data.k1,
1131 &invoice_response.bolt11,
1132 )
1133 .map_err(|e| Error::other(e.to_string()))?;
1134
1135 match exec(http_client.send_invoice_for_withdraw_request(&callback_url)) {
1137 Ok(_) => Ok(crate::lnurl::LnUrlWithdrawResult::Ok {
1138 data: crate::lnurl::LnUrlWithdrawSuccessData {
1139 invoice: invoice_response.bolt11,
1140 },
1141 }),
1142 Err(e) => Ok(crate::lnurl::LnUrlWithdrawResult::ErrorStatus {
1143 data: crate::lnurl::LnUrlErrorData {
1144 reason: e.to_string(),
1145 },
1146 }),
1147 }
1148 }
1149}
1150
1151fn render_section<T: serde::Serialize>(result: Result<T, Error>) -> serde_json::Value {
1152 match result {
1153 Ok(v) => serde_json::to_value(&v)
1154 .unwrap_or_else(|e| serde_json::json!({ "error": e.to_string() })),
1155 Err(e) => serde_json::json!({ "error": e.to_string() }),
1156 }
1157}
1158
1159fn build_diagnostic_json(
1160 timestamp: u64,
1161 sdk_version: &str,
1162 getinfo: serde_json::Value,
1163 listpeerchannels: serde_json::Value,
1164 listfunds: serde_json::Value,
1165 node_state: serde_json::Value,
1166) -> Result<String, Error> {
1167 let envelope = serde_json::json!({
1168 "timestamp": timestamp,
1169 "node": {
1170 "getinfo": getinfo,
1171 "listpeerchannels": listpeerchannels,
1172 "listfunds": listfunds,
1173 },
1174 "sdk": {
1175 "version": sdk_version,
1176 "node_state": node_state,
1177 }
1178 });
1179 serde_json::to_string_pretty(&envelope).map_err(|e| Error::other(e.to_string()))
1180}
1181
1182fn invoice_network_mismatch(
1190 invoice_str: &str,
1191 node_network: gl_client::bitcoin::Network,
1192) -> Option<String> {
1193 use lightning_invoice::Currency;
1194 let invoice = invoice_str.parse::<Bolt11Invoice>().ok()?;
1195 let expected = match node_network {
1196 gl_client::bitcoin::Network::Bitcoin => Currency::Bitcoin,
1197 gl_client::bitcoin::Network::Testnet => Currency::BitcoinTestnet,
1198 gl_client::bitcoin::Network::Signet => Currency::Signet,
1199 gl_client::bitcoin::Network::Regtest => Currency::Regtest,
1200 _ => return None,
1201 };
1202 if invoice.currency() == expected {
1203 None
1204 } else {
1205 Some(format!(
1206 "invoice is for {:?}, but this node is on {:?}",
1207 invoice.currency(),
1208 node_network
1209 ))
1210 }
1211}
1212
1213fn url_action_domain_mismatch(callback_url: &str, action_url: &str) -> Option<String> {
1214 let cb = url::Url::parse(callback_url).ok()?;
1215 let action = url::Url::parse(action_url).ok()?;
1216 let cb_domain = cb.domain()?;
1217 let action_domain = action.domain()?;
1218 if cb_domain == action_domain {
1219 None
1220 } else {
1221 Some(format!(
1222 "success action URL domain ({}) does not match the callback domain ({})",
1223 action_domain, cb_domain
1224 ))
1225 }
1226}
1227
1228fn validate_lnurl_pay_input(request: &crate::lnurl::LnUrlPayRequest) -> Result<(), Error> {
1229 let data = &request.data;
1230 if request.amount_msat < data.min_sendable {
1231 return Err(Error::other(format!(
1232 "amount_msat {} is below the service's min_sendable ({})",
1233 request.amount_msat, data.min_sendable
1234 )));
1235 }
1236 if request.amount_msat > data.max_sendable {
1237 return Err(Error::other(format!(
1238 "amount_msat {} is above the service's max_sendable ({})",
1239 request.amount_msat, data.max_sendable
1240 )));
1241 }
1242 if let Some(comment) = request.comment.as_deref() {
1243 if data.comment_allowed == 0 && !comment.is_empty() {
1244 return Err(Error::other(
1245 "this LNURL service does not accept comments".to_string(),
1246 ));
1247 }
1248 if (comment.len() as u64) > data.comment_allowed {
1249 return Err(Error::other(format!(
1250 "comment length {} exceeds the service's comment_allowed ({})",
1251 comment.len(),
1252 data.comment_allowed
1253 )));
1254 }
1255 }
1256 Ok(())
1257}
1258
1259impl Node {
1261 pub(crate) fn set_event_listener(
1272 &self,
1273 listener: std::sync::Arc<dyn NodeEventListener>,
1274 ) -> Result<(), Error> {
1275 self.check_connected()?;
1276 let mut gl_client = exec(self.get_gl_client())?.clone();
1277 let req = glpb::NodeEventsRequest {};
1278 let stream = exec(gl_client.stream_node_events(req))
1279 .map_err(|e| Error::rpc(e.to_string()))?
1280 .into_inner();
1281
1282 let mut guard = self
1283 .event_task
1284 .lock()
1285 .map_err(|e| Error::other(e.to_string()))?;
1286 if let Some(prev) = guard.take() {
1287 prev.abort();
1288 }
1289
1290 let task = crate::util::get_runtime().spawn(async move {
1291 let mut stream = stream;
1292 loop {
1293 match stream.message().await {
1294 Ok(Some(raw)) => {
1295 if let Some(event) = node_event_from_pb(raw) {
1296 listener.on_event(event);
1297 }
1298 }
1299 Ok(None) => break,
1300 Err(e) if e.code() == tonic::Code::Unknown => break,
1301 Err(_) => break,
1302 }
1303 }
1304 });
1305 *guard = Some(task);
1306 Ok(())
1307 }
1308
1309 fn check_connected(&self) -> Result<(), Error> {
1310 if self.disconnected.load(Ordering::Relaxed) {
1311 return Err(Error::other("Node is disconnected".to_string()));
1312 }
1313 Ok(())
1314 }
1315
1316 pub(crate) fn with_signer(
1319 credentials: Credentials,
1320 handle: Handle,
1321 network: gl_client::bitcoin::Network,
1322 ) -> Result<Self, Error> {
1323 let node_id = credentials
1324 .inner
1325 .node_id()
1326 .map_err(|_e| Error::unparseable_creds())?;
1327 let inner = ClientNode::new(node_id, credentials.inner.clone())
1328 .expect("infallible client instantiation");
1329
1330 let cln_client = OnceCell::const_new();
1331 let gl_client = OnceCell::const_new();
1332 Ok(Node {
1333 inner,
1334 cln_client,
1335 gl_client,
1336 stored_credentials: Some(credentials),
1337 signer_handle: Some(handle),
1338 disconnected: AtomicBool::new(false),
1339 event_task: Mutex::new(None),
1340 network,
1341 })
1342 }
1343
1344 async fn get_gl_client<'a>(&'a self) -> Result<&'a GlClient, Error> {
1345 let inner = self.inner.clone();
1346 self.gl_client
1347 .get_or_try_init(|| async { inner.schedule::<GlClient>().await })
1348 .await
1349 .map_err(|e| Error::rpc(e.to_string()))
1350 }
1351
1352 async fn get_cln_client<'a>(&'a self) -> Result<&'a ClnClient, Error> {
1353 let inner = self.inner.clone();
1354
1355 self.cln_client
1356 .get_or_try_init(|| async { inner.schedule::<ClnClient>().await })
1357 .await
1358 .map_err(|e| Error::rpc(e.to_string()))
1359 }
1360}
1361
1362#[derive(Clone, uniffi::Record)]
1364pub struct Outpoint {
1365 pub txid: String,
1367 pub vout: u32,
1369}
1370
1371#[derive(Clone, uniffi::Record)]
1375pub struct OnchainFeeRates {
1376 pub next_block_sat_per_vbyte: u64,
1378 pub half_hour_sat_per_vbyte: u64,
1380 pub hour_sat_per_vbyte: u64,
1382 pub day_sat_per_vbyte: u64,
1385 pub minimum_relay_sat_per_vbyte: u64,
1389}
1390
1391fn sat_per_vbyte_from_perkw(perkw: u32) -> u64 {
1394 (perkw as u64).div_ceil(250)
1395}
1396
1397fn pick_perkw_for_target(
1402 estimates: &[clnpb::FeeratesPerkwEstimates],
1403 target_blocks: u32,
1404) -> Option<u32> {
1405 let above = estimates
1406 .iter()
1407 .filter(|e| e.blockcount >= target_blocks)
1408 .min_by_key(|e| e.blockcount)
1409 .map(|e| e.feerate);
1410 above.or_else(|| {
1411 estimates
1412 .iter()
1413 .max_by_key(|e| e.blockcount)
1414 .map(|e| e.feerate)
1415 })
1416}
1417
1418fn compute_fee_rates(perkw: Option<&clnpb::FeeratesPerkw>) -> OnchainFeeRates {
1423 const FALLBACK_SAT_PER_VBYTE: u64 = 1;
1426
1427 let Some(p) = perkw else {
1428 return OnchainFeeRates {
1429 next_block_sat_per_vbyte: FALLBACK_SAT_PER_VBYTE,
1430 half_hour_sat_per_vbyte: FALLBACK_SAT_PER_VBYTE,
1431 hour_sat_per_vbyte: FALLBACK_SAT_PER_VBYTE,
1432 day_sat_per_vbyte: FALLBACK_SAT_PER_VBYTE,
1433 minimum_relay_sat_per_vbyte: FALLBACK_SAT_PER_VBYTE,
1434 };
1435 };
1436
1437 let minimum_relay_sat_per_vbyte =
1438 sat_per_vbyte_from_perkw(p.min_acceptable).max(FALLBACK_SAT_PER_VBYTE);
1439
1440 let bucket = |target_blocks: u32| -> u64 {
1441 pick_perkw_for_target(&p.estimates, target_blocks)
1442 .map(sat_per_vbyte_from_perkw)
1443 .unwrap_or(minimum_relay_sat_per_vbyte)
1444 .max(minimum_relay_sat_per_vbyte)
1445 };
1446
1447 OnchainFeeRates {
1448 next_block_sat_per_vbyte: bucket(1),
1449 half_hour_sat_per_vbyte: bucket(3),
1450 hour_sat_per_vbyte: bucket(6),
1451 day_sat_per_vbyte: bucket(144),
1452 minimum_relay_sat_per_vbyte,
1453 }
1454}
1455
1456#[derive(Clone, uniffi::Enum)]
1460pub enum OnchainBalanceState {
1461 Unavailable,
1465
1466 Available {
1469 withdrawable_sat: u64,
1472 emergency_reserve_sat: u64,
1475 unconfirmed_sat: u64,
1478 },
1479
1480 ReserveOnly { reserve_sat: u64 },
1485
1486 PendingConfirmation { unconfirmed_sat: u64 },
1489
1490 Immature { immature_sat: u64 },
1495}
1496
1497fn classify_onchain_balance(
1502 confirmed_sat: u64,
1503 reserve_sat: u64,
1504 unconfirmed_sat: u64,
1505 immature_sat: u64,
1506 pending_close_sat: u64,
1507) -> OnchainBalanceState {
1508 let withdrawable_sat = confirmed_sat.saturating_sub(reserve_sat);
1509
1510 if confirmed_sat == 0
1511 && unconfirmed_sat == 0
1512 && immature_sat == 0
1513 && pending_close_sat == 0
1514 {
1515 return OnchainBalanceState::Unavailable;
1516 }
1517 if withdrawable_sat > ONCHAIN_DUST_THRESHOLD_SAT {
1518 return OnchainBalanceState::Available {
1519 withdrawable_sat,
1520 emergency_reserve_sat: reserve_sat,
1521 unconfirmed_sat,
1522 };
1523 }
1524 if confirmed_sat > 0 && reserve_sat > 0 {
1525 return OnchainBalanceState::ReserveOnly { reserve_sat };
1526 }
1527 if unconfirmed_sat > 0 {
1528 return OnchainBalanceState::PendingConfirmation { unconfirmed_sat };
1529 }
1530 OnchainBalanceState::Immature { immature_sat }
1531}
1532
1533#[derive(uniffi::Record)]
1544pub struct PreparedOnchainSend {
1545 pub utxos: Vec<Outpoint>,
1547 pub total_input_sat: u64,
1549 pub fee_sat: u64,
1551 pub recipient_sat: u64,
1555 pub sat_per_vbyte: u32,
1561}
1562
1563#[derive(uniffi::Record)]
1565pub struct OnchainSendResponse {
1566 pub tx: Vec<u8>,
1568 pub txid: String,
1570 pub psbt: String,
1572}
1573
1574fn parse_amount_or_all(amount_or_all: &str) -> Result<clnpb::AmountOrAll, Error> {
1577 let (num, suffix): (String, String) =
1578 amount_or_all.chars().partition(|c| c.is_ascii_digit());
1579
1580 let num = if num.is_empty() {
1581 0
1582 } else {
1583 num.parse::<u64>()
1584 .map_err(|_| Error::argument("amount_or_all", amount_or_all))?
1585 };
1586
1587 match (num, suffix.as_str()) {
1588 (n, "") | (n, "sat") => Ok(clnpb::AmountOrAll {
1589 value: Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount {
1590 msat: n * 1000,
1591 })),
1592 }),
1593 (n, "msat") => Ok(clnpb::AmountOrAll {
1594 value: Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount { msat: n })),
1595 }),
1596 (0, "all") => Ok(clnpb::AmountOrAll {
1597 value: Some(clnpb::amount_or_all::Value::All(true)),
1598 }),
1599 _ => Err(Error::argument("amount_or_all", amount_or_all)),
1600 }
1601}
1602
1603fn feerate_perkw_from_sat_per_vbyte(sat_per_vbyte: u32) -> clnpb::Feerate {
1607 clnpb::Feerate {
1608 style: Some(clnpb::feerate::Style::Perkw(sat_per_vbyte * 250)),
1609 }
1610}
1611
1612fn outpoint_to_pb(o: Outpoint) -> Result<clnpb::Outpoint, Error> {
1615 let txid = hex::decode(&o.txid)
1616 .map_err(|_| Error::argument("utxos.txid", o.txid.clone()))?;
1617 Ok(clnpb::Outpoint {
1618 txid,
1619 outnum: o.vout,
1620 })
1621}
1622
1623const BASE_TX_CORE_WEIGHT: u32 = 42;
1631
1632const ONCHAIN_DUST_THRESHOLD_SAT: u64 = 546;
1640
1641fn output_weight_for_address(addr: &str) -> u32 {
1653 match bitcoin::Address::from_str(addr) {
1654 Ok(a) => {
1655 let spk_len = a.assume_checked().script_pubkey().len();
1656 let varint_len = if spk_len < 0xfd { 1 } else { 3 };
1657 ((8 + varint_len + spk_len) * 4) as u32
1658 }
1659 Err(_) => 172,
1660 }
1661}
1662
1663impl From<clnpb::WithdrawResponse> for OnchainSendResponse {
1664 fn from(other: clnpb::WithdrawResponse) -> Self {
1665 Self {
1666 tx: other.tx,
1667 txid: hex::encode(&other.txid),
1668 psbt: other.psbt,
1669 }
1670 }
1671}
1672
1673#[derive(uniffi::Record)]
1675pub struct OnchainReceiveResponse {
1676 pub bech32: String,
1678 pub p2tr: String,
1680}
1681
1682impl From<clnpb::NewaddrResponse> for OnchainReceiveResponse {
1683 fn from(other: clnpb::NewaddrResponse) -> Self {
1684 OnchainReceiveResponse {
1685 bech32: other.bech32.unwrap_or_default(),
1686 p2tr: other.p2tr.unwrap_or_default(),
1687 }
1688 }
1689}
1690
1691#[derive(uniffi::Record)]
1692pub struct SendResponse {
1693 pub status: PayStatus,
1694 pub preimage: String,
1696 pub payment_hash: String,
1698 pub destination_pubkey: Option<String>,
1700 pub amount_msat: u64,
1701 pub amount_sent_msat: u64,
1702 pub parts: u32,
1703}
1704
1705impl From<clnpb::PayResponse> for SendResponse {
1706 fn from(other: clnpb::PayResponse) -> Self {
1707 Self {
1708 status: other.status.into(),
1709 preimage: hex::encode(&other.payment_preimage),
1710 payment_hash: hex::encode(&other.payment_hash),
1711 destination_pubkey: other.destination.as_deref().map(hex::encode),
1712 amount_msat: other.amount_msat.unwrap().msat,
1713 amount_sent_msat: other.amount_sent_msat.unwrap().msat,
1714 parts: other.parts,
1715 }
1716 }
1717}
1718
1719#[derive(uniffi::Record)]
1720pub struct ReceiveResponse {
1721 pub bolt11: String,
1722 pub opening_fee_msat: u64,
1725}
1726
1727#[derive(uniffi::Enum, Clone, serde::Serialize)]
1728pub enum PayStatus {
1729 COMPLETE = 0,
1730 PENDING = 1,
1731 FAILED = 2,
1732}
1733
1734impl From<clnpb::pay_response::PayStatus> for PayStatus {
1735 fn from(other: clnpb::pay_response::PayStatus) -> Self {
1736 match other {
1737 clnpb::pay_response::PayStatus::Complete => PayStatus::COMPLETE,
1738 clnpb::pay_response::PayStatus::Failed => PayStatus::FAILED,
1739 clnpb::pay_response::PayStatus::Pending => PayStatus::PENDING,
1740 }
1741 }
1742}
1743
1744impl From<i32> for PayStatus {
1745 fn from(i: i32) -> Self {
1746 match i {
1747 0 => PayStatus::COMPLETE,
1748 1 => PayStatus::PENDING,
1749 2 => PayStatus::FAILED,
1750 o => panic!("Unknown pay_status {}", o),
1751 }
1752 }
1753}
1754
1755#[allow(unused)]
1760#[derive(Clone, serde::Serialize, uniffi::Record)]
1761pub struct GetInfoResponse {
1762 pub id: String,
1764 pub alias: Option<String>,
1765 pub color: String,
1767 pub num_peers: u32,
1768 pub num_pending_channels: u32,
1769 pub num_active_channels: u32,
1770 pub num_inactive_channels: u32,
1771 pub version: String,
1772 pub lightning_dir: String,
1773 pub blockheight: u32,
1774 pub network: String,
1775 pub fees_collected_msat: u64,
1776}
1777
1778impl From<clnpb::GetinfoResponse> for GetInfoResponse {
1779 fn from(other: clnpb::GetinfoResponse) -> Self {
1780 Self {
1781 id: hex::encode(&other.id),
1782 alias: other.alias,
1783 color: hex::encode(&other.color),
1784 num_peers: other.num_peers,
1785 num_pending_channels: other.num_pending_channels,
1786 num_active_channels: other.num_active_channels,
1787 num_inactive_channels: other.num_inactive_channels,
1788 version: other.version,
1789 lightning_dir: other.lightning_dir,
1790 blockheight: other.blockheight,
1791 network: other.network,
1792 fees_collected_msat: other.fees_collected_msat.map(|a| a.msat).unwrap_or(0),
1793 }
1794 }
1795}
1796
1797#[allow(unused)]
1802#[derive(Clone, uniffi::Record)]
1803pub struct ListPeersResponse {
1804 pub peers: Vec<Peer>,
1805}
1806
1807#[allow(unused)]
1808#[derive(Clone, uniffi::Record)]
1809pub struct Peer {
1810 pub id: String,
1812 pub connected: bool,
1813 pub num_channels: Option<u32>,
1814 pub netaddr: Vec<String>,
1815 pub remote_addr: Option<String>,
1816 pub features: Option<Vec<u8>>,
1817}
1818
1819impl From<clnpb::ListpeersResponse> for ListPeersResponse {
1820 fn from(other: clnpb::ListpeersResponse) -> Self {
1821 Self {
1822 peers: other.peers.into_iter().map(|p| p.into()).collect(),
1823 }
1824 }
1825}
1826
1827impl From<clnpb::ListpeersPeers> for Peer {
1828 fn from(other: clnpb::ListpeersPeers) -> Self {
1829 Self {
1830 id: hex::encode(&other.id),
1831 connected: other.connected,
1832 num_channels: other.num_channels,
1833 netaddr: other.netaddr,
1834 remote_addr: other.remote_addr,
1835 features: other.features,
1836 }
1837 }
1838}
1839
1840#[allow(unused)]
1845#[derive(Clone, serde::Serialize, uniffi::Record)]
1846pub struct ListPeerChannelsResponse {
1847 pub channels: Vec<PeerChannel>,
1848}
1849
1850#[allow(unused)]
1851#[derive(Clone, serde::Serialize, uniffi::Record)]
1852pub struct PeerChannel {
1853 pub peer_id: String,
1855 pub peer_connected: bool,
1856 pub state: ChannelState,
1857 pub short_channel_id: Option<String>,
1858 pub channel_id: Option<String>,
1860 pub funding_txid: Option<String>,
1862 pub funding_outnum: Option<u32>,
1863 pub to_us_msat: Option<u64>,
1864 pub total_msat: Option<u64>,
1865 pub spendable_msat: Option<u64>,
1866 pub receivable_msat: Option<u64>,
1867 pub closer: Option<ChannelSide>,
1869 pub status: Vec<String>,
1874}
1875
1876#[derive(Clone, serde::Serialize, uniffi::Enum)]
1878pub enum ChannelSide {
1879 Local,
1880 Remote,
1881}
1882
1883impl ChannelSide {
1884 fn from_i32(value: i32) -> Option<Self> {
1885 match value {
1886 0 => Some(ChannelSide::Local),
1887 1 => Some(ChannelSide::Remote),
1888 _ => None,
1889 }
1890 }
1891}
1892
1893#[derive(Clone, serde::Serialize, uniffi::Enum)]
1894pub enum ChannelState {
1895 Openingd,
1896 ChanneldAwaitingLockin,
1897 ChanneldNormal,
1898 ChanneldShuttingDown,
1899 ClosingdSigexchange,
1900 ClosingdComplete,
1901 AwaitingUnilateral,
1902 FundingSpendSeen,
1903 Onchain,
1904 DualopendOpenInit,
1905 DualopendAwaitingLockin,
1906 DualopendOpenCommitted,
1907 DualopendOpenCommitReady,
1908 Unknown,
1912}
1913
1914impl ChannelState {
1915 fn from_i32(value: i32) -> Self {
1916 match value {
1917 0 => ChannelState::Openingd,
1918 1 => ChannelState::ChanneldAwaitingLockin,
1919 2 => ChannelState::ChanneldNormal,
1920 3 => ChannelState::ChanneldShuttingDown,
1921 4 => ChannelState::ClosingdSigexchange,
1922 5 => ChannelState::ClosingdComplete,
1923 6 => ChannelState::AwaitingUnilateral,
1924 7 => ChannelState::FundingSpendSeen,
1925 8 => ChannelState::Onchain,
1926 9 => ChannelState::DualopendOpenInit,
1927 10 => ChannelState::DualopendAwaitingLockin,
1928 11 => ChannelState::DualopendOpenCommitted,
1929 12 => ChannelState::DualopendOpenCommitReady,
1930 _ => ChannelState::Unknown,
1931 }
1932 }
1933
1934 fn is_open(&self) -> bool {
1935 matches!(self, ChannelState::ChanneldNormal)
1936 }
1937
1938}
1939
1940fn channel_payout_still_pending(ch: &PeerChannel) -> bool {
1954 match ch.state {
1955 ChannelState::ChanneldShuttingDown
1956 | ChannelState::ClosingdSigexchange
1957 | ChannelState::ClosingdComplete
1958 | ChannelState::AwaitingUnilateral
1959 | ChannelState::FundingSpendSeen => true,
1960 ChannelState::Onchain => {
1961 matches!(ch.closer, Some(ChannelSide::Local))
1962 && ch
1963 .status
1964 .last()
1965 .is_some_and(|s| s.contains("DELAYED_OUTPUT_TO_US"))
1966 }
1967 _ => false,
1968 }
1969}
1970
1971impl From<clnpb::ListpeerchannelsResponse> for ListPeerChannelsResponse {
1972 fn from(other: clnpb::ListpeerchannelsResponse) -> Self {
1973 Self {
1974 channels: other.channels.into_iter().map(|c| c.into()).collect(),
1975 }
1976 }
1977}
1978
1979impl From<clnpb::ListpeerchannelsChannels> for PeerChannel {
1980 fn from(other: clnpb::ListpeerchannelsChannels) -> Self {
1981 let state = ChannelState::from_i32(other.state);
1982 let closer = other.closer.and_then(ChannelSide::from_i32);
1983 Self {
1984 peer_id: hex::encode(&other.peer_id),
1985 peer_connected: other.peer_connected,
1986 state,
1987 short_channel_id: other.short_channel_id,
1988 channel_id: other.channel_id.as_deref().map(hex::encode),
1989 funding_txid: other.funding_txid.as_deref().map(hex::encode),
1990 funding_outnum: other.funding_outnum,
1991 to_us_msat: other.to_us_msat.map(|a| a.msat),
1992 total_msat: other.total_msat.map(|a| a.msat),
1993 spendable_msat: other.spendable_msat.map(|a| a.msat),
1994 receivable_msat: other.receivable_msat.map(|a| a.msat),
1995 closer,
1996 status: other.status,
1997 }
1998 }
1999}
2000
2001#[allow(unused)]
2006#[derive(Clone, serde::Serialize, uniffi::Record)]
2007pub struct ListFundsResponse {
2008 pub outputs: Vec<FundOutput>,
2009 pub channels: Vec<FundChannel>,
2010}
2011
2012#[allow(unused)]
2013#[derive(Clone, serde::Serialize, uniffi::Record)]
2014pub struct FundOutput {
2015 pub txid: String,
2017 pub output: u32,
2018 pub amount_msat: u64,
2019 pub status: OutputStatus,
2020 pub address: Option<String>,
2021 pub blockheight: Option<u32>,
2022 pub reserved: bool,
2027}
2028
2029#[derive(Clone, serde::Serialize, uniffi::Enum)]
2030pub enum OutputStatus {
2031 Unconfirmed,
2032 Confirmed,
2033 Spent,
2034 Immature,
2035}
2036
2037impl OutputStatus {
2038 fn from_i32(value: i32) -> Self {
2039 match value {
2040 0 => OutputStatus::Unconfirmed,
2041 1 => OutputStatus::Confirmed,
2042 2 => OutputStatus::Spent,
2043 3 => OutputStatus::Immature,
2044 _ => OutputStatus::Unconfirmed, }
2046 }
2047}
2048
2049#[allow(unused)]
2050#[derive(Clone, serde::Serialize, uniffi::Record)]
2051pub struct FundChannel {
2052 pub peer_id: String,
2054 pub our_amount_msat: u64,
2055 pub amount_msat: u64,
2056 pub funding_txid: String,
2058 pub funding_output: u32,
2059 pub connected: bool,
2060 pub state: ChannelState,
2061 pub short_channel_id: Option<String>,
2062 pub channel_id: Option<String>,
2064}
2065
2066impl From<clnpb::ListfundsResponse> for ListFundsResponse {
2067 fn from(other: clnpb::ListfundsResponse) -> Self {
2068 Self {
2069 outputs: other.outputs.into_iter().map(|o| o.into()).collect(),
2070 channels: other.channels.into_iter().map(|c| c.into()).collect(),
2071 }
2072 }
2073}
2074
2075impl From<clnpb::ListfundsOutputs> for FundOutput {
2076 fn from(other: clnpb::ListfundsOutputs) -> Self {
2077 let status = OutputStatus::from_i32(other.status);
2078 Self {
2079 txid: hex::encode(&other.txid),
2080 output: other.output,
2081 amount_msat: other.amount_msat.map(|a| a.msat).unwrap_or(0),
2082 status,
2083 address: other.address,
2084 blockheight: other.blockheight,
2085 reserved: other.reserved,
2086 }
2087 }
2088}
2089
2090impl From<clnpb::ListfundsChannels> for FundChannel {
2091 fn from(other: clnpb::ListfundsChannels) -> Self {
2092 let state = ChannelState::from_i32(other.state);
2093 Self {
2094 peer_id: hex::encode(&other.peer_id),
2095 our_amount_msat: other.our_amount_msat.map(|a| a.msat).unwrap_or(0),
2096 amount_msat: other.amount_msat.map(|a| a.msat).unwrap_or(0),
2097 funding_txid: hex::encode(&other.funding_txid),
2098 funding_output: other.funding_output,
2099 connected: other.connected,
2100 state,
2101 short_channel_id: other.short_channel_id,
2102 channel_id: other.channel_id.as_deref().map(hex::encode),
2103 }
2104 }
2105}
2106
2107#[derive(Clone, uniffi::Enum)]
2113pub enum ListIndex {
2114 CREATED,
2115 UPDATED,
2116}
2117
2118impl ListIndex {
2119 fn to_i32(&self) -> i32 {
2120 match self {
2121 ListIndex::CREATED => 0,
2122 ListIndex::UPDATED => 1,
2123 }
2124 }
2125}
2126
2127#[derive(Clone, serde::Serialize, uniffi::Enum)]
2132pub enum InvoiceStatus {
2133 UNPAID,
2134 PAID,
2135 EXPIRED,
2136}
2137
2138impl From<i32> for InvoiceStatus {
2139 fn from(i: i32) -> Self {
2140 match i {
2141 0 => InvoiceStatus::UNPAID,
2142 1 => InvoiceStatus::PAID,
2143 2 => InvoiceStatus::EXPIRED,
2144 o => panic!("Unknown invoice status {}", o),
2145 }
2146 }
2147}
2148
2149#[derive(Clone, serde::Serialize, uniffi::Record)]
2150pub struct Invoice {
2151 pub label: String,
2152 pub description: String,
2153 pub payment_hash: String,
2155 pub status: InvoiceStatus,
2156 pub amount_msat: Option<u64>,
2157 pub amount_received_msat: Option<u64>,
2158 pub bolt11: Option<String>,
2159 pub bolt12: Option<String>,
2160 pub paid_at: Option<u64>,
2161 pub expires_at: u64,
2162 pub payment_preimage: Option<String>,
2164 pub destination_pubkey: Option<String>,
2166}
2167
2168fn pubkey_from_bolt11(bolt11: &str) -> Option<String> {
2170 let invoice: Bolt11Invoice = bolt11.parse().ok()?;
2171 Some(hex::encode(invoice.recover_payee_pub_key().serialize()))
2172}
2173
2174impl From<clnpb::ListinvoicesInvoices> for Invoice {
2175 fn from(other: clnpb::ListinvoicesInvoices) -> Self {
2176 let destination_pubkey = other.bolt11.as_deref().and_then(pubkey_from_bolt11);
2177 Self {
2178 label: other.label,
2179 description: other.description.unwrap_or_default(),
2180 payment_hash: hex::encode(&other.payment_hash),
2181 status: other.status.into(),
2182 amount_msat: other.amount_msat.map(|a| a.msat),
2183 amount_received_msat: other.amount_received_msat.map(|a| a.msat),
2184 bolt11: other.bolt11,
2185 bolt12: other.bolt12,
2186 paid_at: other.paid_at,
2187 expires_at: other.expires_at,
2188 payment_preimage: other.payment_preimage.as_deref().map(hex::encode),
2189 destination_pubkey,
2190 }
2191 }
2192}
2193
2194#[derive(Clone, serde::Serialize, uniffi::Record)]
2195pub struct ListInvoicesResponse {
2196 pub invoices: Vec<Invoice>,
2197}
2198
2199impl From<clnpb::ListinvoicesResponse> for ListInvoicesResponse {
2200 fn from(other: clnpb::ListinvoicesResponse) -> Self {
2201 Self {
2202 invoices: other.invoices.into_iter().map(|i| i.into()).collect(),
2203 }
2204 }
2205}
2206
2207#[derive(Clone, serde::Serialize, uniffi::Record)]
2212pub struct Pay {
2213 pub payment_hash: String,
2215 pub status: PayStatus,
2216 pub destination_pubkey: Option<String>,
2218 pub amount_msat: Option<u64>,
2219 pub amount_sent_msat: Option<u64>,
2220 pub label: Option<String>,
2221 pub bolt11: Option<String>,
2222 pub description: Option<String>,
2223 pub bolt12: Option<String>,
2224 pub preimage: Option<String>,
2226 pub created_at: u64,
2227 pub completed_at: Option<u64>,
2228 pub number_of_parts: Option<u64>,
2229}
2230
2231impl From<clnpb::ListpaysPays> for Pay {
2232 fn from(other: clnpb::ListpaysPays) -> Self {
2233 let status = match other.status {
2234 0 => PayStatus::PENDING, 1 => PayStatus::FAILED, 2 => PayStatus::COMPLETE, o => panic!("Unknown listpays status {}", o),
2238 };
2239 Self {
2240 payment_hash: hex::encode(&other.payment_hash),
2241 status,
2242 destination_pubkey: other.destination.as_deref().map(hex::encode),
2243 amount_msat: other.amount_msat.map(|a| a.msat),
2244 amount_sent_msat: other.amount_sent_msat.map(|a| a.msat),
2245 label: other.label,
2246 bolt11: other.bolt11,
2247 description: other.description,
2248 bolt12: other.bolt12,
2249 preimage: other.preimage.as_deref().map(hex::encode),
2250 created_at: other.created_at,
2251 completed_at: other.completed_at,
2252 number_of_parts: other.number_of_parts,
2253 }
2254 }
2255}
2256
2257#[derive(Clone, serde::Serialize, uniffi::Record)]
2258pub struct ListPaysResponse {
2259 pub pays: Vec<Pay>,
2260}
2261
2262impl From<clnpb::ListpaysResponse> for ListPaysResponse {
2263 fn from(other: clnpb::ListpaysResponse) -> Self {
2264 Self {
2265 pays: other.pays.into_iter().map(|p| p.into()).collect(),
2266 }
2267 }
2268}
2269
2270#[derive(Clone, Default, uniffi::Record)]
2275pub struct ListPaymentsRequest {
2276 pub filters: Option<Vec<PaymentTypeFilter>>,
2278 pub from_timestamp: Option<u64>,
2280 pub to_timestamp: Option<u64>,
2282 pub include_failures: Option<bool>,
2284 pub offset: Option<u32>,
2286 pub limit: Option<u32>,
2288}
2289
2290#[derive(Clone, uniffi::Enum)]
2291pub enum PaymentTypeFilter {
2292 Sent,
2293 Received,
2294}
2295
2296#[derive(Clone, uniffi::Record)]
2297pub struct Payment {
2298 pub id: String,
2299 pub payment_type: PaymentType,
2300 pub payment_time: u64,
2301 pub amount_msat: u64,
2302 pub fee_msat: u64,
2303 pub status: PaymentStatus,
2304 pub description: Option<String>,
2305 pub bolt11: Option<String>,
2306 pub preimage: Option<String>,
2308 pub destination: Option<String>,
2321}
2322
2323#[derive(Clone, uniffi::Enum)]
2324pub enum PaymentType {
2325 Sent,
2326 Received,
2327}
2328
2329#[derive(Clone, uniffi::Enum)]
2330pub enum PaymentStatus {
2331 Pending,
2332 Complete,
2333 Failed,
2334}
2335
2336impl From<clnpb::ListinvoicesInvoices> for Payment {
2337 fn from(inv: clnpb::ListinvoicesInvoices) -> Self {
2338 let status = match inv.status() {
2339 clnpb::listinvoices_invoices::ListinvoicesInvoicesStatus::Paid => {
2340 PaymentStatus::Complete
2341 }
2342 clnpb::listinvoices_invoices::ListinvoicesInvoicesStatus::Expired => {
2343 PaymentStatus::Failed
2344 }
2345 clnpb::listinvoices_invoices::ListinvoicesInvoicesStatus::Unpaid => {
2346 PaymentStatus::Pending
2347 }
2348 };
2349
2350 let payment_time = inv.paid_at.unwrap_or(inv.expires_at);
2351 let amount_msat = inv
2352 .amount_received_msat
2353 .or(inv.amount_msat)
2354 .map(|a| a.msat)
2355 .unwrap_or(0);
2356
2357 Payment {
2358 id: hex::encode(&inv.payment_hash),
2359 payment_type: PaymentType::Received,
2360 payment_time,
2361 amount_msat,
2362 fee_msat: 0,
2363 status,
2364 description: inv.description,
2365 bolt11: inv.bolt11,
2366 preimage: inv.payment_preimage.as_deref().map(hex::encode),
2367 destination: None,
2368 }
2369 }
2370}
2371
2372impl From<clnpb::ListpaysPays> for Payment {
2373 fn from(pay: clnpb::ListpaysPays) -> Self {
2374 let status = match pay.status() {
2375 clnpb::listpays_pays::ListpaysPaysStatus::Complete => PaymentStatus::Complete,
2376 clnpb::listpays_pays::ListpaysPaysStatus::Failed => PaymentStatus::Failed,
2377 clnpb::listpays_pays::ListpaysPaysStatus::Pending => PaymentStatus::Pending,
2378 };
2379
2380 let payment_time = pay.completed_at.unwrap_or(pay.created_at);
2381 let amount_msat = pay.amount_msat.as_ref().map(|a| a.msat).unwrap_or(0);
2382 let amount_sent_msat = pay.amount_sent_msat.as_ref().map(|a| a.msat).unwrap_or(0);
2383 let fee_msat = amount_sent_msat.saturating_sub(amount_msat);
2384
2385 Payment {
2386 id: hex::encode(&pay.payment_hash),
2387 payment_type: PaymentType::Sent,
2388 payment_time,
2389 amount_msat,
2390 fee_msat,
2391 status,
2392 description: pay.description,
2393 bolt11: pay.bolt11,
2394 preimage: pay.preimage.as_deref().map(hex::encode),
2395 destination: pay.destination.as_deref().map(hex::encode),
2396 }
2397 }
2398}
2399
2400#[derive(Clone, serde::Serialize, uniffi::Record)]
2409pub struct NodeState {
2410 pub id: String,
2412 pub block_height: u32,
2414 pub network: String,
2416 pub version: String,
2418 pub alias: Option<String>,
2420 pub color: String,
2422 pub num_active_channels: u32,
2427 pub num_pending_channels: u32,
2432 pub num_inactive_channels: u32,
2439 pub channels_balance_msat: u64,
2449 pub max_payable_msat: u64,
2460 pub total_channel_capacity_msat: u64,
2462 pub max_chan_reserve_msat: u64,
2466 pub onchain_balance_msat: u64,
2468 pub unconfirmed_onchain_balance_msat: u64,
2470 pub immature_onchain_balance_msat: u64,
2473 pub pending_onchain_balance_msat: u64,
2476 pub max_receivable_single_payment_msat: u64,
2480 pub total_inbound_liquidity_msat: u64,
2482 pub connected_channel_peers: Vec<String>,
2487 pub utxos: Vec<FundOutput>,
2491
2492 pub total_onchain_msat: u64,
2502 pub total_balance_msat: u64,
2507 pub spendable_balance_msat: u64,
2512}
2513
2514#[uniffi::export(callback_interface)]
2529pub trait NodeEventListener: Send + Sync {
2530 fn on_event(&self, event: NodeEvent);
2531}
2532
2533#[derive(uniffi::Object)]
2540pub struct NodeEventStream {
2541 inner: Mutex<tonic::codec::Streaming<glpb::NodeEvent>>,
2542}
2543
2544#[uniffi::export]
2545impl NodeEventStream {
2546 pub fn next(&self) -> Result<Option<NodeEvent>, Error> {
2552 let mut stream = self.inner.lock().map_err(|e| Error::other(e.to_string()))?;
2553 loop {
2558 match exec(stream.message()) {
2559 Ok(Some(raw)) => {
2560 if let Some(event) = node_event_from_pb(raw) {
2561 return Ok(Some(event));
2562 }
2563 }
2565 Ok(None) => return Ok(None),
2566 Err(e) if e.code() == tonic::Code::Unknown => return Ok(None),
2567 Err(e) => return Err(Error::rpc(e.to_string())),
2568 }
2569 }
2570 }
2571}
2572
2573#[derive(Clone, uniffi::Enum)]
2575pub enum NodeEvent {
2576 InvoicePaid { details: InvoicePaidEvent },
2578}
2579
2580#[derive(Clone, uniffi::Record)]
2582pub struct InvoicePaidEvent {
2583 pub payment_hash: String,
2585 pub bolt11: String,
2587 pub preimage: String,
2589 pub label: String,
2591 pub amount_msat: u64,
2593}
2594
2595fn node_event_from_pb(other: glpb::NodeEvent) -> Option<NodeEvent> {
2602 match other.event {
2603 Some(glpb::node_event::Event::InvoicePaid(paid)) => Some(NodeEvent::InvoicePaid {
2604 details: InvoicePaidEvent {
2605 payment_hash: hex::encode(&paid.payment_hash),
2606 bolt11: paid.bolt11,
2607 preimage: hex::encode(&paid.preimage),
2608 label: paid.label,
2609 amount_msat: paid.amount_msat,
2610 },
2611 }),
2612 None => None,
2613 }
2614}
2615
2616#[cfg(test)]
2617mod tests {
2618 use super::*;
2619
2620 #[test]
2621 fn parse_amount_or_all_handles_all_variants() {
2622 let all = parse_amount_or_all("all").unwrap();
2623 assert!(matches!(all.value, Some(clnpb::amount_or_all::Value::All(true))));
2624
2625 let plain = parse_amount_or_all("50000").unwrap();
2626 assert!(matches!(
2627 plain.value,
2628 Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount { msat: 50_000_000 }))
2629 ));
2630
2631 let sat = parse_amount_or_all("50000sat").unwrap();
2632 assert!(matches!(
2633 sat.value,
2634 Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount { msat: 50_000_000 }))
2635 ));
2636
2637 let msat = parse_amount_or_all("50000msat").unwrap();
2638 assert!(matches!(
2639 msat.value,
2640 Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount { msat: 50_000 }))
2641 ));
2642
2643 assert!(parse_amount_or_all("notanumber").is_err());
2644 assert!(parse_amount_or_all("50000btc").is_err());
2645 }
2646
2647 #[test]
2648 fn classify_onchain_balance_unavailable_when_empty() {
2649 assert!(matches!(
2650 classify_onchain_balance(0, 0, 0, 0, 0),
2651 OnchainBalanceState::Unavailable
2652 ));
2653 }
2654
2655 #[test]
2656 fn classify_onchain_balance_available_with_room_above_dust() {
2657 match classify_onchain_balance(100_000, 25_000, 5_000, 0, 0) {
2659 OnchainBalanceState::Available {
2660 withdrawable_sat,
2661 emergency_reserve_sat,
2662 unconfirmed_sat,
2663 } => {
2664 assert_eq!(withdrawable_sat, 75_000);
2665 assert_eq!(emergency_reserve_sat, 25_000);
2666 assert_eq!(unconfirmed_sat, 5_000);
2667 }
2668 other => panic!("expected Available, got {:?}", std::mem::discriminant(&other)),
2669 }
2670 }
2671
2672 #[test]
2673 fn classify_onchain_balance_reserve_only_when_balance_equals_reserve() {
2674 match classify_onchain_balance(25_000, 25_000, 0, 0, 0) {
2676 OnchainBalanceState::ReserveOnly { reserve_sat } => {
2677 assert_eq!(reserve_sat, 25_000);
2678 }
2679 other => panic!(
2680 "expected ReserveOnly, got {:?}",
2681 std::mem::discriminant(&other)
2682 ),
2683 }
2684 }
2685
2686 #[test]
2687 fn classify_onchain_balance_pending_when_only_unconfirmed() {
2688 match classify_onchain_balance(0, 0, 50_000, 0, 0) {
2689 OnchainBalanceState::PendingConfirmation { unconfirmed_sat } => {
2690 assert_eq!(unconfirmed_sat, 50_000);
2691 }
2692 other => panic!(
2693 "expected PendingConfirmation, got {:?}",
2694 std::mem::discriminant(&other)
2695 ),
2696 }
2697 }
2698
2699 #[test]
2700 fn classify_onchain_balance_immature_when_only_immature() {
2701 match classify_onchain_balance(0, 0, 0, 100_000, 0) {
2702 OnchainBalanceState::Immature { immature_sat } => {
2703 assert_eq!(immature_sat, 100_000);
2704 }
2705 other => panic!("expected Immature, got {:?}", std::mem::discriminant(&other)),
2706 }
2707 }
2708
2709 #[test]
2710 fn classify_onchain_balance_real_wallet_small_onchain_with_active_channels() {
2711 match classify_onchain_balance(1_228, 25_000, 0, 0, 0) {
2715 OnchainBalanceState::ReserveOnly { reserve_sat } => {
2716 assert_eq!(reserve_sat, 25_000);
2717 }
2718 other => panic!(
2719 "expected ReserveOnly, got {:?}",
2720 std::mem::discriminant(&other)
2721 ),
2722 }
2723 }
2724
2725 #[test]
2726 fn classify_onchain_balance_real_wallet_onchain_just_above_reserve() {
2727 match classify_onchain_balance(28_228, 25_000, 0, 0, 0) {
2731 OnchainBalanceState::Available {
2732 withdrawable_sat,
2733 emergency_reserve_sat,
2734 unconfirmed_sat,
2735 } => {
2736 assert_eq!(withdrawable_sat, 3_228);
2737 assert_eq!(emergency_reserve_sat, 25_000);
2738 assert_eq!(unconfirmed_sat, 0);
2739 }
2740 other => panic!(
2741 "expected Available, got {:?}",
2742 std::mem::discriminant(&other)
2743 ),
2744 }
2745 }
2746
2747 #[test]
2748 fn classify_onchain_balance_dust_only_above_reserve_is_not_available() {
2749 match classify_onchain_balance(25_100, 25_000, 0, 0, 0) {
2752 OnchainBalanceState::ReserveOnly { reserve_sat } => {
2753 assert_eq!(reserve_sat, 25_000);
2754 }
2755 other => panic!(
2756 "expected ReserveOnly, got {:?}",
2757 std::mem::discriminant(&other)
2758 ),
2759 }
2760 }
2761
2762 #[test]
2763 fn classify_onchain_balance_real_user_no_anchor_no_reserve() {
2764 match classify_onchain_balance(28_228, 0, 0, 0, 0) {
2768 OnchainBalanceState::Available {
2769 withdrawable_sat,
2770 emergency_reserve_sat,
2771 unconfirmed_sat,
2772 } => {
2773 assert_eq!(withdrawable_sat, 28_228);
2774 assert_eq!(emergency_reserve_sat, 0);
2775 assert_eq!(unconfirmed_sat, 0);
2776 }
2777 other => panic!(
2778 "expected Available, got {:?}",
2779 std::mem::discriminant(&other)
2780 ),
2781 }
2782 }
2783
2784 fn perkw_with(estimates: Vec<(u32, u32)>, min_acceptable: u32) -> clnpb::FeeratesPerkw {
2785 clnpb::FeeratesPerkw {
2786 min_acceptable,
2787 max_acceptable: 0,
2788 opening: None,
2789 mutual_close: None,
2790 unilateral_close: None,
2791 unilateral_anchor_close: None,
2792 delayed_to_us: None,
2793 htlc_resolution: None,
2794 penalty: None,
2795 estimates: estimates
2796 .into_iter()
2797 .map(|(blockcount, feerate)| clnpb::FeeratesPerkwEstimates {
2798 blockcount,
2799 feerate,
2800 smoothed_feerate: feerate,
2801 })
2802 .collect(),
2803 floor: None,
2804 }
2805 }
2806
2807 #[test]
2808 fn fee_rates_maps_perkw_to_buckets() {
2809 let perkw = perkw_with(
2812 vec![(2, 5000), (6, 2000), (12, 1500), (144, 500)],
2813 253, );
2815 let r = compute_fee_rates(Some(&perkw));
2816 assert_eq!(r.next_block_sat_per_vbyte, 20);
2818 assert_eq!(r.half_hour_sat_per_vbyte, 8);
2820 assert_eq!(r.hour_sat_per_vbyte, 8);
2822 assert_eq!(r.day_sat_per_vbyte, 2);
2824 assert_eq!(r.minimum_relay_sat_per_vbyte, 2);
2826 }
2827
2828 #[test]
2829 fn fee_rates_fall_back_to_minimum_when_no_estimates() {
2830 let perkw = perkw_with(vec![], 750); let r = compute_fee_rates(Some(&perkw));
2832 assert_eq!(r.minimum_relay_sat_per_vbyte, 3);
2833 assert_eq!(r.next_block_sat_per_vbyte, 3);
2835 assert_eq!(r.half_hour_sat_per_vbyte, 3);
2836 assert_eq!(r.hour_sat_per_vbyte, 3);
2837 assert_eq!(r.day_sat_per_vbyte, 3);
2838 }
2839
2840 #[test]
2841 fn fee_rates_no_perkw_at_all_returns_safe_floor() {
2842 let r = compute_fee_rates(None);
2843 assert_eq!(r.minimum_relay_sat_per_vbyte, 1);
2844 assert_eq!(r.next_block_sat_per_vbyte, 1);
2845 assert_eq!(r.day_sat_per_vbyte, 1);
2846 }
2847
2848 #[test]
2849 fn fee_rates_buckets_never_below_minimum() {
2850 let perkw = perkw_with(
2853 vec![(2, 1500), (144, 250)], 1000,
2855 );
2856 let r = compute_fee_rates(Some(&perkw));
2857 assert_eq!(r.minimum_relay_sat_per_vbyte, 4);
2858 assert_eq!(r.day_sat_per_vbyte, 4);
2860 }
2861
2862 #[test]
2863 fn fee_rates_target_above_all_estimates_uses_largest() {
2864 let perkw = perkw_with(vec![(2, 5000), (6, 2500)], 250);
2867 let r = compute_fee_rates(Some(&perkw));
2868 assert_eq!(r.day_sat_per_vbyte, 10);
2870 }
2871
2872 #[test]
2873 fn output_weight_for_address_per_script_type() {
2874 assert_eq!(
2877 output_weight_for_address("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"),
2878 124
2879 );
2880
2881 assert_eq!(
2884 output_weight_for_address(
2885 "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0"
2886 ),
2887 172
2888 );
2889
2890 assert_eq!(output_weight_for_address("3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy"), 128);
2892
2893 assert_eq!(output_weight_for_address("1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2"), 136);
2895
2896 assert_eq!(output_weight_for_address("not-an-address"), 172);
2898 }
2899}