1#![deny(clippy::pedantic)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::missing_panics_doc)]
4#![allow(clippy::module_name_repetitions)]
5#![allow(clippy::must_use_candidate)]
6
7pub use fedimint_lnv2_common as common;
8
9mod api;
10#[cfg(feature = "cli")]
11mod cli;
12pub mod db;
13pub mod events;
14mod receive_sm;
15mod send_sm;
16
17use std::collections::{BTreeMap, BTreeSet};
18use std::sync::Arc;
19
20use async_stream::stream;
21use bitcoin::hashes::{Hash, sha256};
22use bitcoin::secp256k1;
23use db::{DbKeyPrefix, GatewayKey, IncomingContractStreamIndexKey};
24use fedimint_api_client::api::DynModuleApi;
25use fedimint_client_module::error::{
26 ClientModuleError, OperationLookupError, TransactionSubmitError,
27};
28use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
29use fedimint_client_module::module::recovery::NoModuleBackup;
30use fedimint_client_module::module::{ClientContext, ClientModule, OutPointRange};
31use fedimint_client_module::oplog::UpdateStreamOrOutcome;
32use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
33use fedimint_client_module::transaction::{
34 ClientOutput, ClientOutputBundle, ClientOutputSM, FeeQuote, FeeQuoteRequest,
35 TransactionBuilder, max_affordable_send_amount,
36};
37use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
38use fedimint_core::config::FederationId;
39use fedimint_core::core::{IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
40use fedimint_core::db::{DatabaseTransaction, IDatabaseTransactionOpsCoreTyped};
41use fedimint_core::encoding::{Decodable, Encodable};
42use fedimint_core::module::{
43 Amounts, ApiAuth, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
44};
45use fedimint_core::secp256k1::SECP256K1;
46use fedimint_core::task::TaskGroup;
47use fedimint_core::time::duration_since_epoch;
48use fedimint_core::util::{FmtCompact as _, SafeUrl};
49use fedimint_core::{Amount, PeerId, apply, async_trait_maybe_send};
50use fedimint_derive_secret::{ChildId, DerivableSecret};
51use fedimint_lnv2_common::config::LightningClientConfig;
52use fedimint_lnv2_common::contracts::{IncomingContract, OutgoingContract, PaymentImage};
53use fedimint_lnv2_common::gateway_api::{
54 GatewayConnection, MAX_INVOICE_EXPIRY_SECS, PaymentFee, RealGatewayConnection, RoutingInfo,
55};
56use fedimint_lnv2_common::{
57 Bolt11InvoiceDescription, GatewayApi, KIND, LightningCommonInit, LightningInvoice,
58 LightningModuleTypes, LightningOutput, LightningOutputV0, lnurl, tweak,
59};
60use fedimint_logging::LOG_CLIENT_MODULE_LNV2;
61use futures::StreamExt;
62use lightning_invoice::{Bolt11Invoice, Currency};
63use secp256k1::{Keypair, Scalar, SecretKey, ecdh};
64use serde::{Deserialize, Serialize};
65use serde_json::Value;
66use strum::IntoEnumIterator as _;
67use thiserror::Error;
68use tpe::{AggregateDecryptionKey, derive_agg_dk};
69use tracing::warn;
70
71use crate::api::LightningFederationApi;
72use crate::events::SendPaymentEvent;
73use crate::receive_sm::{ReceiveSMCommon, ReceiveSMState, ReceiveStateMachine};
74use crate::send_sm::{SendSMCommon, SendSMState, SendStateMachine};
75
76const EXPIRATION_DELTA_LIMIT: u64 = 1440;
79
80const CONTRACT_CONFIRMATION_BUFFER: u64 = 12;
82
83#[allow(clippy::large_enum_variant)]
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub enum LightningOperationMeta {
86 Send(SendOperationMeta),
87 Receive(ReceiveOperationMeta),
88 LnurlReceive(LnurlReceiveOperationMeta),
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct SendOperationMeta {
93 pub change_outpoint_range: OutPointRange,
94 pub gateway: SafeUrl,
95 pub contract: OutgoingContract,
96 pub invoice: LightningInvoice,
97 pub custom_meta: Value,
98}
99
100impl SendOperationMeta {
101 pub fn gateway_fee(&self) -> Amount {
103 match &self.invoice {
104 LightningInvoice::Bolt11(invoice) => self.contract.amount.saturating_sub(
105 Amount::from_msats(invoice.amount_milli_satoshis().expect("Invoice has amount")),
106 ),
107 }
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ReceiveOperationMeta {
113 pub gateway: SafeUrl,
114 pub contract: IncomingContract,
115 pub invoice: LightningInvoice,
116 pub custom_meta: Value,
117}
118
119impl ReceiveOperationMeta {
120 pub fn gateway_fee(&self) -> Amount {
122 match &self.invoice {
123 LightningInvoice::Bolt11(invoice) => {
124 Amount::from_msats(invoice.amount_milli_satoshis().expect("Invoice has amount"))
125 .saturating_sub(self.contract.commitment.amount)
126 }
127 }
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct LnurlReceiveOperationMeta {
133 pub contract: IncomingContract,
134 pub custom_meta: Value,
135}
136
137#[cfg_attr(doc, aquamarine::aquamarine)]
138#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
156pub enum SendOperationState {
157 Funding,
159 Funded,
161 Success([u8; 32]),
163 Refunding,
165 Refunded,
167 Failure,
169}
170
171#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
173pub enum FinalSendOperationState {
174 Success(#[serde(with = "fedimint_core::hex::serde")] [u8; 32]),
177 Refunded,
179 Failure,
181}
182
183pub type SendResult = Result<OperationId, SendPaymentError>;
184
185#[cfg_attr(doc, aquamarine::aquamarine)]
186#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
198pub enum ReceiveOperationState {
199 Pending,
201 Expired,
203 Claiming,
205 Claimed,
207 Failure,
209 Uneconomical,
213}
214
215#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
217pub enum FinalReceiveOperationState {
218 Expired,
220 Claimed,
222 Failure,
224 Uneconomical,
227}
228
229pub type ReceiveResult = Result<(Bolt11Invoice, OperationId), ReceiveError>;
230
231#[derive(Clone)]
232pub struct LightningClientInit {
233 pub gateway_conn: Option<Arc<dyn GatewayConnection + Send + Sync>>,
234 pub custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
235}
236
237impl std::fmt::Debug for LightningClientInit {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 f.debug_struct("LightningClientInit")
240 .field("gateway_conn", &self.gateway_conn)
241 .field("custom_meta_fn", &"<function>")
242 .finish()
243 }
244}
245
246impl Default for LightningClientInit {
247 fn default() -> Self {
248 LightningClientInit {
249 gateway_conn: None,
250 custom_meta_fn: Arc::new(|| Value::Null),
251 }
252 }
253}
254
255impl ModuleInit for LightningClientInit {
256 type Common = LightningCommonInit;
257
258 async fn dump_database(
259 &self,
260 _dbtx: &mut DatabaseTransaction<'_>,
261 _prefix_names: Vec<String>,
262 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
263 Box::new(BTreeMap::new().into_iter())
264 }
265}
266
267#[apply(async_trait_maybe_send!)]
268impl ClientModuleInit for LightningClientInit {
269 type Module = LightningClientModule;
270
271 fn supported_api_versions(&self) -> MultiApiVersion {
272 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
273 .expect("no version conflicts")
274 }
275
276 async fn init(
277 &self,
278 args: &ClientModuleInitArgs<Self>,
279 ) -> Result<Self::Module, ClientModuleError> {
280 let gateway_conn = if let Some(gateway_conn) = self.gateway_conn.clone() {
281 gateway_conn
282 } else {
283 let api = GatewayApi::new(None, args.connector_registry.clone());
284 Arc::new(RealGatewayConnection { api })
285 };
286 Ok(LightningClientModule::new(
287 *args.federation_id(),
288 args.cfg().clone(),
289 args.notifier().clone(),
290 args.context(),
291 args.module_api().clone(),
292 args.module_root_secret(),
293 gateway_conn,
294 self.custom_meta_fn.clone(),
295 args.admin_auth().cloned(),
296 args.task_group(),
297 args.client_span(),
298 ))
299 }
300
301 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
302 Some(
303 DbKeyPrefix::iter()
304 .map(|p| p as u8)
305 .chain(
306 DbKeyPrefix::ExternalReservedStart as u8
307 ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
308 )
309 .collect(),
310 )
311 }
312}
313
314#[derive(Debug, Clone)]
315pub struct LightningClientContext {
316 federation_id: FederationId,
317 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
318 pub(crate) client_ctx: ClientContext<LightningClientModule>,
319}
320
321impl Context for LightningClientContext {
322 const KIND: Option<ModuleKind> = Some(KIND);
323}
324
325#[derive(Debug, Clone)]
326pub struct LightningClientModule {
327 federation_id: FederationId,
328 cfg: LightningClientConfig,
329 notifier: ModuleNotifier<LightningClientStateMachines>,
330 client_ctx: ClientContext<Self>,
331 module_api: DynModuleApi,
332 keypair: Keypair,
333 lnurl_keypair: Keypair,
334 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
335 #[allow(unused)] admin_auth: Option<ApiAuth>,
337}
338
339#[apply(async_trait_maybe_send!)]
340impl ClientModule for LightningClientModule {
341 type Init = LightningClientInit;
342 type Common = LightningModuleTypes;
343 type Backup = NoModuleBackup;
344 type ModuleStateMachineContext = LightningClientContext;
345 type States = LightningClientStateMachines;
346
347 fn context(&self) -> Self::ModuleStateMachineContext {
348 LightningClientContext {
349 federation_id: self.federation_id,
350 gateway_conn: self.gateway_conn.clone(),
351 client_ctx: self.client_ctx.clone(),
352 }
353 }
354
355 fn input_fee(
356 &self,
357 amounts: &Amounts,
358 _input: &<Self::Common as ModuleCommon>::Input,
359 ) -> Option<Amounts> {
360 Some(Amounts::new_bitcoin(
361 self.cfg.fee_consensus.fee(amounts.expect_only_bitcoin()),
362 ))
363 }
364
365 fn output_fee(
366 &self,
367 amounts: &Amounts,
368 _output: &<Self::Common as ModuleCommon>::Output,
369 ) -> Option<Amounts> {
370 Some(Amounts::new_bitcoin(
371 self.cfg.fee_consensus.fee(amounts.expect_only_bitcoin()),
372 ))
373 }
374
375 #[cfg(feature = "cli")]
376 async fn handle_cli_command(
377 &self,
378 args: &[std::ffi::OsString],
379 ) -> Result<serde_json::Value, ClientModuleError> {
380 cli::handle_cli_command(self, args)
381 .await
382 .map_err(ClientModuleError::other)
383 }
384}
385
386impl LightningClientModule {
387 #[allow(clippy::too_many_arguments)]
388 fn new(
389 federation_id: FederationId,
390 cfg: LightningClientConfig,
391 notifier: ModuleNotifier<LightningClientStateMachines>,
392 client_ctx: ClientContext<Self>,
393 module_api: DynModuleApi,
394 module_root_secret: &DerivableSecret,
395 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
396 custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
397 admin_auth: Option<ApiAuth>,
398 task_group: &TaskGroup,
399 client_span: &tracing::Span,
400 ) -> Self {
401 let module = Self {
402 federation_id,
403 cfg,
404 notifier,
405 client_ctx,
406 module_api,
407 keypair: module_root_secret
408 .child_key(ChildId(0))
409 .to_secp_key(SECP256K1),
410 lnurl_keypair: module_root_secret
411 .child_key(ChildId(1))
412 .to_secp_key(SECP256K1),
413 gateway_conn,
414 admin_auth,
415 };
416
417 module.spawn_receive_lnurl_task(custom_meta_fn, task_group, client_span);
418
419 module.spawn_gateway_map_update_task(task_group, client_span);
420
421 module
422 }
423
424 fn spawn_gateway_map_update_task(&self, task_group: &TaskGroup, client_span: &tracing::Span) {
425 let module = self.clone();
426 let api = self.module_api.clone();
427
428 task_group.spawn_cancellable_with_span(
429 client_span.clone(),
430 "gateway_map_update_task",
431 async move {
432 api.wait_for_initialized_connections().await;
433 module.update_gateway_map().await;
434 },
435 );
436 }
437
438 async fn update_gateway_map(&self) {
439 if let Ok(gateways) = self.module_api.gateways().await {
446 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
447
448 for gateway in gateways {
449 if let Ok(Some(routing_info)) = self
450 .gateway_conn
451 .routing_info(gateway.clone(), &self.federation_id)
452 .await
453 {
454 dbtx.insert_entry(&GatewayKey(routing_info.lightning_public_key), &gateway)
455 .await;
456 }
457 }
458
459 if let Err(e) = dbtx.commit_tx_result().await {
460 warn!("Failed to commit the updated gateway mapping to the database: {e}");
461 }
462 }
463 }
464
465 pub async fn select_gateway(
470 &self,
471 invoice: Option<Bolt11Invoice>,
472 ) -> Result<(SafeUrl, RoutingInfo), SelectGatewayError> {
473 let gateways = self
474 .module_api
475 .gateways()
476 .await
477 .map_err(|e| SelectGatewayError::FailedToRequestGateways(e.to_string()))?;
478
479 if gateways.is_empty() {
480 return Err(SelectGatewayError::NoGatewaysAvailable);
481 }
482
483 if let Some(invoice) = invoice
484 && let Some(gateway) = self
485 .client_ctx
486 .module_db()
487 .begin_transaction_nc()
488 .await
489 .get_value(&GatewayKey(invoice.recover_payee_pub_key()))
490 .await
491 .filter(|gateway| gateways.contains(gateway))
492 && let Ok(Some(routing_info)) = self.routing_info(&gateway).await
493 {
494 return Ok((gateway, routing_info));
495 }
496
497 for gateway in gateways {
498 if let Ok(Some(routing_info)) = self.routing_info(&gateway).await {
499 return Ok((gateway, routing_info));
500 }
501 }
502
503 Err(SelectGatewayError::GatewaysUnresponsive)
504 }
505
506 pub async fn list_gateways(
509 &self,
510 peer: Option<PeerId>,
511 ) -> Result<Vec<SafeUrl>, ListGatewaysError> {
512 if let Some(peer) = peer {
513 self.module_api
514 .gateways_from_peer(peer)
515 .await
516 .map_err(|_| ListGatewaysError::FailedToListGateways)
517 } else {
518 self.module_api
519 .gateways()
520 .await
521 .map_err(|_| ListGatewaysError::FailedToListGateways)
522 }
523 }
524
525 pub async fn routing_info(
528 &self,
529 gateway: &SafeUrl,
530 ) -> Result<Option<RoutingInfo>, RoutingInfoError> {
531 self.gateway_conn
532 .routing_info(gateway.clone(), &self.federation_id)
533 .await
534 .map_err(|_| RoutingInfoError::FailedToRequestRoutingInfo)
535 }
536
537 pub async fn send(
554 &self,
555 invoice: Bolt11Invoice,
556 gateway: Option<SafeUrl>,
557 custom_meta: Value,
558 ) -> Result<OperationId, SendPaymentError> {
559 let (amount, operation_id) = self.validate_send_invoice(&invoice).await?;
560
561 let (gateway_api, routing_info) = self.resolve_send_gateway(&invoice, gateway).await?;
562
563 self.fund_outgoing_contract(
564 invoice,
565 amount,
566 operation_id,
567 gateway_api,
568 routing_info,
569 custom_meta,
570 )
571 .await
572 }
573
574 pub async fn send_with_terms(
590 &self,
591 invoice: Bolt11Invoice,
592 gateway: SafeUrl,
593 send_fee: PaymentFee,
594 expiration_delta: u64,
595 custom_meta: Value,
596 ) -> Result<OperationId, SendWithTermsError> {
597 let (amount, operation_id) = self.validate_send_invoice(&invoice).await?;
598
599 let (gateway_api, routing_info) =
600 self.resolve_send_gateway(&invoice, Some(gateway)).await?;
601
602 let current = routing_info.send_parameters(&invoice);
603
604 if current != (send_fee, expiration_delta) {
605 return Err(SendWithTermsError::TermsChanged {
606 send_fee: current.0,
607 expiration_delta: current.1,
608 });
609 }
610
611 let operation_id = self
612 .fund_outgoing_contract(
613 invoice,
614 amount,
615 operation_id,
616 gateway_api,
617 routing_info,
618 custom_meta,
619 )
620 .await?;
621
622 Ok(operation_id)
623 }
624
625 async fn validate_send_invoice(
629 &self,
630 invoice: &Bolt11Invoice,
631 ) -> Result<(u64, OperationId), SendPaymentError> {
632 let amount = invoice
633 .amount_milli_satoshis()
634 .ok_or(SendPaymentError::InvoiceMissingAmount)?;
635
636 if invoice.is_expired() {
637 return Err(SendPaymentError::InvoiceExpired);
638 }
639
640 if self.cfg.network != invoice.currency().into() {
641 return Err(SendPaymentError::WrongCurrency {
642 invoice_currency: invoice.currency(),
643 federation_currency: self.cfg.network.into(),
644 });
645 }
646
647 let operation_id = OperationId::from_encodable(&(invoice.clone(), 0u64));
651
652 if self.client_ctx.operation_exists(operation_id).await {
653 return Err(SendPaymentError::DuplicatePaymentAttempt(operation_id));
654 }
655
656 Ok((amount, operation_id))
657 }
658
659 async fn resolve_send_gateway(
662 &self,
663 invoice: &Bolt11Invoice,
664 gateway: Option<SafeUrl>,
665 ) -> Result<(SafeUrl, RoutingInfo), SendPaymentError> {
666 match gateway {
667 Some(gateway_api) => Ok((
668 gateway_api.clone(),
669 self.routing_info(&gateway_api)
670 .await
671 .map_err(|e| SendPaymentError::FailedToConnectToGateway(e.to_string()))?
672 .ok_or(SendPaymentError::FederationNotSupported)?,
673 )),
674 None => self
675 .select_gateway(Some(invoice.clone()))
676 .await
677 .map_err(SendPaymentError::SelectGateway),
678 }
679 }
680
681 #[allow(clippy::too_many_lines)]
684 async fn fund_outgoing_contract(
685 &self,
686 invoice: Bolt11Invoice,
687 amount: u64,
688 operation_id: OperationId,
689 gateway_api: SafeUrl,
690 routing_info: RoutingInfo,
691 custom_meta: Value,
692 ) -> Result<OperationId, SendPaymentError> {
693 let (ephemeral_tweak, ephemeral_pk) = tweak::generate(self.keypair.public_key());
694
695 let refund_keypair = SecretKey::from_slice(&ephemeral_tweak)
696 .expect("32 bytes, within curve order")
697 .keypair(secp256k1::SECP256K1);
698
699 let (send_fee, expiration_delta) = routing_info.send_parameters(&invoice);
700
701 if !send_fee.is_within(&PaymentFee::SEND_FEE_LIMIT) {
702 return Err(SendPaymentError::GatewayFeeExceedsLimit);
703 }
704
705 if EXPIRATION_DELTA_LIMIT < expiration_delta {
706 return Err(SendPaymentError::GatewayExpirationExceedsLimit);
707 }
708
709 let consensus_block_count = self
710 .module_api
711 .consensus_block_count()
712 .await
713 .map_err(|e| SendPaymentError::FailedToRequestBlockCount(e.to_string()))?;
714
715 let contract = OutgoingContract {
716 payment_image: PaymentImage::Hash(*invoice.payment_hash()),
717 amount: send_fee.add_to(amount),
718 expiration: consensus_block_count + expiration_delta + CONTRACT_CONFIRMATION_BUFFER,
719 claim_pk: routing_info.module_public_key,
720 refund_pk: refund_keypair.public_key(),
721 ephemeral_pk,
722 };
723
724 let contract_clone = contract.clone();
725 let gateway_api_clone = gateway_api.clone();
726 let invoice_clone = invoice.clone();
727
728 let client_output = ClientOutput::<LightningOutput> {
729 output: LightningOutput::V0(LightningOutputV0::Outgoing(contract.clone())),
730 amounts: Amounts::new_bitcoin(contract.amount),
731 };
732
733 let client_output_sm = ClientOutputSM::<LightningClientStateMachines> {
734 state_machines: Arc::new(move |range: OutPointRange| {
735 vec![LightningClientStateMachines::Send(SendStateMachine {
736 common: SendSMCommon {
737 operation_id,
738 outpoint: range.into_iter().next().unwrap(),
739 contract: contract_clone.clone(),
740 gateway_api: Some(gateway_api_clone.clone()),
741 invoice: Some(LightningInvoice::Bolt11(invoice_clone.clone())),
742 refund_keypair,
743 },
744 state: SendSMState::Funding,
745 })]
746 }),
747 };
748
749 let client_output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
750 vec![client_output],
751 vec![client_output_sm],
752 ));
753
754 let transaction = TransactionBuilder::new().with_outputs(client_output);
755
756 self.client_ctx
757 .finalize_and_submit_transaction(
758 operation_id,
759 LightningCommonInit::KIND.as_str(),
760 move |change_outpoint_range| {
761 LightningOperationMeta::Send(SendOperationMeta {
762 change_outpoint_range,
763 gateway: gateway_api.clone(),
764 contract: contract.clone(),
765 invoice: LightningInvoice::Bolt11(invoice.clone()),
766 custom_meta: custom_meta.clone(),
767 })
768 },
769 transaction,
770 )
771 .await
772 .map_err(|e| SendPaymentError::FailedToFundPayment(e.fmt_compact().to_string()))?;
773
774 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
775
776 self.client_ctx
777 .log_event(
778 &mut dbtx,
779 SendPaymentEvent {
780 operation_id,
781 amount: Amount::from_msats(amount),
782 fee: send_fee.fee(amount),
783 },
784 )
785 .await;
786
787 dbtx.commit_tx().await;
788
789 Ok(operation_id)
790 }
791
792 pub async fn get_invoice_send_status(
798 &self,
799 invoice: &Bolt11Invoice,
800 ) -> Result<InvoiceSendStatus, OperationLookupError> {
801 let mut last_op = None;
806
807 for attempt in 0_u64.. {
808 let operation_id = OperationId::from_encodable(&(invoice.clone(), attempt));
809
810 if !self.client_ctx.operation_exists(operation_id).await {
811 break;
812 }
813
814 last_op = Some(operation_id);
815 }
816
817 let Some(operation_id) = last_op else {
818 return Ok(InvoiceSendStatus::NotAttempted);
819 };
820
821 if self.client_ctx.has_active_states(operation_id).await {
822 return Ok(InvoiceSendStatus::InFlight(operation_id));
823 }
824
825 let mut stream = self
828 .subscribe_send_operation_state_updates(operation_id)
829 .await?
830 .into_stream();
831
832 while let Some(state) = stream.next().await {
833 if let SendOperationState::Success(_) = state {
834 return Ok(InvoiceSendStatus::Succeeded(operation_id));
835 }
836 }
837
838 Ok(InvoiceSendStatus::Failed(operation_id))
839 }
840
841 pub async fn subscribe_send_operation_state_updates(
843 &self,
844 operation_id: OperationId,
845 ) -> Result<UpdateStreamOrOutcome<SendOperationState>, OperationLookupError> {
846 let operation = self.client_ctx.get_operation(operation_id).await?;
847 let mut stream = self.notifier.subscribe(operation_id).await;
848 let client_ctx = self.client_ctx.clone();
849 let module_api = self.module_api.clone();
850
851 Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
852 SendOperationState::Funding
853 | SendOperationState::Funded
854 | SendOperationState::Refunding => false,
855 SendOperationState::Success(_)
856 | SendOperationState::Refunded
857 | SendOperationState::Failure => true,
858 }, move || {
859 stream! {
860 loop {
861 if let Some(LightningClientStateMachines::Send(state)) = stream.next().await {
862 match state.state {
863 SendSMState::Funding => yield SendOperationState::Funding,
864 SendSMState::Funded => yield SendOperationState::Funded,
865 SendSMState::Success(preimage) => {
866 assert!(state.common.contract.verify_preimage(&preimage));
868
869 yield SendOperationState::Success(preimage);
870 return;
871 },
872 SendSMState::Refunding(out_points) => {
873 yield SendOperationState::Refunding;
874
875 if client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await.is_ok() {
876 yield SendOperationState::Refunded;
877 return;
878 }
879
880 if let Some(preimage) = module_api.await_preimage(
884 state.common.outpoint,
885 0
886 ).await
887 && state.common.contract.verify_preimage(&preimage) {
888 yield SendOperationState::Success(preimage);
889 return;
890 }
891
892 yield SendOperationState::Failure;
893 return;
894 },
895 SendSMState::Rejected(..) => {
896 yield SendOperationState::Failure;
897 return;
898 },
899 }
900 }
901 }
902 }
903 }))
904 }
905
906 pub async fn await_final_send_operation_state(
908 &self,
909 operation_id: OperationId,
910 ) -> Result<FinalSendOperationState, OperationLookupError> {
911 let mut stream = self
912 .subscribe_send_operation_state_updates(operation_id)
913 .await?
914 .into_stream();
915
916 let mut final_state = None;
917
918 while let Some(state) = stream.next().await {
919 match state {
920 SendOperationState::Success(preimage) => {
921 final_state = Some(FinalSendOperationState::Success(preimage));
922 }
923 SendOperationState::Refunded => {
924 final_state = Some(FinalSendOperationState::Refunded);
925 }
926 SendOperationState::Failure => final_state = Some(FinalSendOperationState::Failure),
927 _ => {}
928 }
929 }
930
931 Ok(final_state.expect("Stream contains one final state"))
932 }
933
934 pub async fn receive(
946 &self,
947 amount: Amount,
948 expiry_secs: u32,
949 description: Bolt11InvoiceDescription,
950 gateway: Option<SafeUrl>,
951 custom_meta: Value,
952 ) -> Result<(Bolt11Invoice, OperationId), ReceiveError> {
953 if expiry_secs > MAX_INVOICE_EXPIRY_SECS {
954 return Err(ReceiveError::InvoiceExpiryTooLong);
955 }
956
957 let (gateway, routing_info) = self.resolve_receive_gateway(gateway).await?;
958
959 self.receive_with_routing_info(
960 amount,
961 expiry_secs,
962 description,
963 gateway,
964 routing_info,
965 custom_meta,
966 )
967 .await
968 }
969
970 pub async fn receive_with_terms(
983 &self,
984 amount: Amount,
985 expiry_secs: u32,
986 description: Bolt11InvoiceDescription,
987 gateway: SafeUrl,
988 receive_fee: PaymentFee,
989 custom_meta: Value,
990 ) -> Result<(Bolt11Invoice, OperationId), ReceiveWithTermsError> {
991 if expiry_secs > MAX_INVOICE_EXPIRY_SECS {
992 return Err(ReceiveError::InvoiceExpiryTooLong.into());
993 }
994
995 let (gateway, routing_info) = self.resolve_receive_gateway(Some(gateway)).await?;
996
997 if routing_info.receive_fee != receive_fee {
998 return Err(ReceiveWithTermsError::TermsChanged {
999 receive_fee: routing_info.receive_fee,
1000 });
1001 }
1002
1003 let (invoice, operation_id) = self
1004 .receive_with_routing_info(
1005 amount,
1006 expiry_secs,
1007 description,
1008 gateway,
1009 routing_info,
1010 custom_meta,
1011 )
1012 .await?;
1013
1014 Ok((invoice, operation_id))
1015 }
1016
1017 async fn resolve_receive_gateway(
1021 &self,
1022 gateway: Option<SafeUrl>,
1023 ) -> Result<(SafeUrl, RoutingInfo), ReceiveError> {
1024 match gateway {
1025 Some(gateway) => {
1026 let routing_info = self
1027 .routing_info(&gateway)
1028 .await
1029 .map_err(|e| ReceiveError::FailedToConnectToGateway(e.to_string()))?
1030 .ok_or(ReceiveError::FederationNotSupported)?;
1031
1032 if !routing_info.receive_enabled {
1033 return Err(ReceiveError::ReceiveDisabled);
1034 }
1035
1036 Ok((gateway, routing_info))
1037 }
1038 None => self
1039 .select_receive_gateway()
1040 .await
1041 .map_err(ReceiveError::SelectGateway),
1042 }
1043 }
1044
1045 async fn select_receive_gateway(&self) -> Result<(SafeUrl, RoutingInfo), SelectGatewayError> {
1050 let gateways = self
1051 .module_api
1052 .gateways()
1053 .await
1054 .map_err(|e| SelectGatewayError::FailedToRequestGateways(e.to_string()))?;
1055
1056 if gateways.is_empty() {
1057 return Err(SelectGatewayError::NoGatewaysAvailable);
1058 }
1059
1060 let mut any_responded = false;
1061
1062 for gateway in gateways {
1063 if let Ok(Some(routing_info)) = self.routing_info(&gateway).await {
1064 any_responded = true;
1065
1066 if routing_info.receive_enabled {
1067 return Ok((gateway, routing_info));
1068 }
1069 }
1070 }
1071
1072 if any_responded {
1073 Err(SelectGatewayError::NoGatewayAcceptsReceives)
1074 } else {
1075 Err(SelectGatewayError::GatewaysUnresponsive)
1076 }
1077 }
1078
1079 async fn receive_with_routing_info(
1083 &self,
1084 amount: Amount,
1085 expiry_secs: u32,
1086 description: Bolt11InvoiceDescription,
1087 gateway: SafeUrl,
1088 routing_info: RoutingInfo,
1089 custom_meta: Value,
1090 ) -> Result<(Bolt11Invoice, OperationId), ReceiveError> {
1091 let recipient_static_pk = self.keypair.public_key();
1092
1093 let (ephemeral_tweak, ephemeral_pk) = tweak::generate(recipient_static_pk);
1094
1095 let encryption_seed = ephemeral_tweak
1096 .consensus_hash::<sha256::Hash>()
1097 .to_byte_array();
1098
1099 let preimage = encryption_seed
1100 .consensus_hash::<sha256::Hash>()
1101 .to_byte_array();
1102
1103 if !routing_info
1104 .receive_fee
1105 .is_within(&PaymentFee::RECEIVE_FEE_LIMIT)
1106 {
1107 return Err(ReceiveError::GatewayFeeExceedsLimit);
1108 }
1109
1110 let contract_amount = routing_info.receive_fee.subtract_from(amount.msats);
1111
1112 if !self.is_worth_claiming(contract_amount).await {
1116 return Err(ReceiveError::AmountTooSmall);
1117 }
1118
1119 let expiration = duration_since_epoch()
1120 .as_secs()
1121 .saturating_add(u64::from(expiry_secs));
1122
1123 let claim_pk = recipient_static_pk
1124 .mul_tweak(
1125 secp256k1::SECP256K1,
1126 &Scalar::from_be_bytes(ephemeral_tweak).expect("Within curve order"),
1127 )
1128 .expect("Tweak is valid");
1129
1130 let contract = IncomingContract::new(
1131 self.cfg.tpe_agg_pk,
1132 encryption_seed,
1133 preimage,
1134 PaymentImage::Hash(preimage.consensus_hash()),
1135 contract_amount,
1136 expiration,
1137 claim_pk,
1138 routing_info.module_public_key,
1139 ephemeral_pk,
1140 );
1141
1142 let invoice = self
1143 .gateway_conn
1144 .bolt11_invoice(
1145 gateway.clone(),
1146 self.federation_id,
1147 contract.clone(),
1148 amount,
1149 description,
1150 expiry_secs,
1151 )
1152 .await
1153 .map_err(|e| ReceiveError::FailedToConnectToGateway(e.to_string()))?;
1154
1155 if invoice.payment_hash() != &preimage.consensus_hash() {
1156 return Err(ReceiveError::InvalidInvoice);
1157 }
1158
1159 if invoice.amount_milli_satoshis() != Some(amount.msats) {
1160 return Err(ReceiveError::IncorrectInvoiceAmount);
1161 }
1162
1163 let operation_id = self
1164 .receive_incoming_contract(
1165 self.keypair.secret_key(),
1166 contract.clone(),
1167 LightningOperationMeta::Receive(ReceiveOperationMeta {
1168 gateway,
1169 contract,
1170 invoice: LightningInvoice::Bolt11(invoice.clone()),
1171 custom_meta,
1172 }),
1173 )
1174 .await
1175 .expect("The contract has been generated with our public key");
1176
1177 Ok((invoice, operation_id))
1178 }
1179
1180 pub async fn receive_fee_quote(
1195 &self,
1196 amount: Amount,
1197 ) -> Result<FeeQuote, TransactionSubmitError> {
1198 self.client_ctx
1199 .fee_quote(
1200 OperationId::new_random(),
1201 FeeQuoteRequest {
1202 input_amount: Amounts::new_bitcoin(amount),
1203 output_amount: Amounts::ZERO,
1204 input_fee: Amounts::new_bitcoin(self.cfg.fee_consensus.fee(amount)),
1205 output_fee: Amounts::ZERO,
1206 },
1207 )
1208 .await
1209 }
1210
1211 async fn is_worth_claiming(&self, amount: Amount) -> bool {
1222 match self.receive_fee_quote(amount).await {
1223 Ok(quote) => quote.total().get_bitcoin() < amount,
1224 Err(_) => false,
1225 }
1226 }
1227
1228 pub async fn send_fee_quote(&self, amount: Amount) -> Result<FeeQuote, TransactionSubmitError> {
1244 self.client_ctx
1245 .fee_quote(
1246 OperationId::new_random(),
1247 FeeQuoteRequest {
1248 input_amount: Amounts::ZERO,
1249 output_amount: Amounts::new_bitcoin(amount),
1250 input_fee: Amounts::ZERO,
1251 output_fee: Amounts::new_bitcoin(self.cfg.fee_consensus.fee(amount)),
1252 },
1253 )
1254 .await
1255 }
1256
1257 pub async fn spendable_amount(
1294 &self,
1295 balance: Amount,
1296 gateway: Option<SafeUrl>,
1297 ) -> Result<Amount, SpendableAmountError> {
1298 let routing_info = match gateway {
1299 Some(gateway) => self
1300 .routing_info(&gateway)
1301 .await?
1302 .ok_or(SpendableAmountError::FederationNotSupported)?,
1303 None => self.select_gateway(None).await?.1,
1304 };
1305
1306 let send_fee = routing_info.send_fee_default;
1311
1312 if !send_fee.is_within(&PaymentFee::SEND_FEE_LIMIT) {
1313 return Err(SpendableAmountError::SendFeeExceedsLimit {
1314 fee: send_fee,
1315 limit: PaymentFee::SEND_FEE_LIMIT,
1316 });
1317 }
1318
1319 max_affordable_send_amount(
1320 balance,
1321 Amount::from_msats(1),
1322 balance,
1323 |invoice_amount: Amount| send_fee.add_to(invoice_amount.msats),
1324 |contract_amount: Amount| self.send_fee_quote(contract_amount),
1325 )
1326 .await
1327 .map_err(SpendableAmountError::Quote)?
1328 .ok_or(SpendableAmountError::BalanceTooLow { balance })
1329 }
1330
1331 async fn receive_incoming_contract(
1334 &self,
1335 sk: SecretKey,
1336 contract: IncomingContract,
1337 operation_meta: LightningOperationMeta,
1338 ) -> Option<OperationId> {
1339 let operation_id = OperationId::from_encodable(&contract.clone());
1340
1341 let (claim_keypair, agg_decryption_key) = self.recover_contract_keys(sk, &contract)?;
1342
1343 let receive_sm = LightningClientStateMachines::Receive(ReceiveStateMachine {
1344 common: ReceiveSMCommon {
1345 operation_id,
1346 contract: contract.clone(),
1347 claim_keypair,
1348 agg_decryption_key,
1349 },
1350 state: ReceiveSMState::Pending,
1351 });
1352
1353 self.client_ctx
1356 .manual_operation_start(
1357 operation_id,
1358 LightningCommonInit::KIND.as_str(),
1359 operation_meta,
1360 vec![self.client_ctx.make_dyn_state(receive_sm)],
1361 )
1362 .await
1363 .ok();
1364
1365 Some(operation_id)
1366 }
1367
1368 fn recover_contract_keys(
1369 &self,
1370 sk: SecretKey,
1371 contract: &IncomingContract,
1372 ) -> Option<(Keypair, AggregateDecryptionKey)> {
1373 let tweak = ecdh::SharedSecret::new(&contract.commitment.ephemeral_pk, &sk);
1374
1375 let encryption_seed = tweak
1376 .secret_bytes()
1377 .consensus_hash::<sha256::Hash>()
1378 .to_byte_array();
1379
1380 let claim_keypair = sk
1381 .mul_tweak(&Scalar::from_be_bytes(tweak.secret_bytes()).expect("Within curve order"))
1382 .expect("Tweak is valid")
1383 .keypair(secp256k1::SECP256K1);
1384
1385 if claim_keypair.public_key() != contract.commitment.claim_pk {
1386 return None; }
1388
1389 let agg_decryption_key = derive_agg_dk(&self.cfg.tpe_agg_pk, &encryption_seed);
1390
1391 if !contract.verify_agg_decryption_key(&self.cfg.tpe_agg_pk, &agg_decryption_key) {
1392 return None; }
1394
1395 contract.decrypt_preimage(&agg_decryption_key)?;
1396
1397 Some((claim_keypair, agg_decryption_key))
1398 }
1399
1400 pub async fn subscribe_receive_operation_state_updates(
1402 &self,
1403 operation_id: OperationId,
1404 ) -> Result<UpdateStreamOrOutcome<ReceiveOperationState>, OperationLookupError> {
1405 let operation = self.client_ctx.get_operation(operation_id).await?;
1406 let mut stream = self.notifier.subscribe(operation_id).await;
1407 let client_ctx = self.client_ctx.clone();
1408
1409 Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
1410 ReceiveOperationState::Pending | ReceiveOperationState::Claiming => false,
1411 ReceiveOperationState::Expired
1412 | ReceiveOperationState::Claimed
1413 | ReceiveOperationState::Failure
1414 | ReceiveOperationState::Uneconomical => true,
1415 }, move || {
1416 stream! {
1417 loop {
1418 if let Some(LightningClientStateMachines::Receive(state)) = stream.next().await {
1419 match state.state {
1420 ReceiveSMState::Pending => yield ReceiveOperationState::Pending,
1421 ReceiveSMState::Claiming(out_points) => {
1422 yield ReceiveOperationState::Claiming;
1423
1424 if client_ctx.await_primary_module_outputs(operation_id, out_points).await.is_ok() {
1425 yield ReceiveOperationState::Claimed;
1426 } else {
1427 yield ReceiveOperationState::Failure;
1428 }
1429 return;
1430 },
1431 ReceiveSMState::Expired => {
1432 yield ReceiveOperationState::Expired;
1433 return;
1434 }
1435 ReceiveSMState::Uneconomical => {
1436 yield ReceiveOperationState::Uneconomical;
1437 return;
1438 }
1439 }
1440 }
1441 }
1442 }
1443 }))
1444 }
1445
1446 pub async fn await_final_receive_operation_state(
1448 &self,
1449 operation_id: OperationId,
1450 ) -> Result<FinalReceiveOperationState, OperationLookupError> {
1451 let mut stream = self
1452 .subscribe_receive_operation_state_updates(operation_id)
1453 .await?
1454 .into_stream();
1455
1456 let mut final_state = None;
1457
1458 while let Some(state) = stream.next().await {
1459 match state {
1460 ReceiveOperationState::Expired => {
1461 final_state = Some(FinalReceiveOperationState::Expired);
1462 }
1463 ReceiveOperationState::Claimed => {
1464 final_state = Some(FinalReceiveOperationState::Claimed);
1465 }
1466 ReceiveOperationState::Failure => {
1467 final_state = Some(FinalReceiveOperationState::Failure);
1468 }
1469 ReceiveOperationState::Uneconomical => {
1470 final_state = Some(FinalReceiveOperationState::Uneconomical);
1471 }
1472 _ => {}
1473 }
1474 }
1475
1476 Ok(final_state.expect("Stream contains one final state"))
1477 }
1478
1479 pub async fn generate_lnurl(
1482 &self,
1483 recurringd: SafeUrl,
1484 gateway: Option<SafeUrl>,
1485 ) -> Result<String, GenerateLnurlError> {
1486 let gateways = if let Some(gateway) = gateway {
1487 vec![gateway]
1488 } else {
1489 let gateways = self
1490 .module_api
1491 .gateways()
1492 .await
1493 .map_err(|e| GenerateLnurlError::FailedToRequestGateways(e.to_string()))?;
1494
1495 if gateways.is_empty() {
1496 return Err(GenerateLnurlError::NoGatewaysAvailable);
1497 }
1498
1499 gateways
1500 };
1501
1502 let payload = fedimint_core::base32::encode_prefixed(
1503 fedimint_core::base32::FEDIMINT_PREFIX,
1504 &lnurl::LnurlRequest {
1505 federation_id: self.federation_id,
1506 recipient_pk: self.lnurl_keypair.public_key(),
1507 aggregate_pk: self.cfg.tpe_agg_pk,
1508 gateways,
1509 },
1510 );
1511
1512 Ok(fedimint_lnurl::encode_lnurl(&format!(
1513 "{recurringd}pay/{payload}"
1514 )))
1515 }
1516
1517 fn spawn_receive_lnurl_task(
1518 &self,
1519 custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
1520 task_group: &TaskGroup,
1521 client_span: &tracing::Span,
1522 ) {
1523 let module = self.clone();
1524 let api = self.module_api.clone();
1525
1526 task_group.spawn_cancellable_with_span(
1527 client_span.clone(),
1528 "receive_lnurl_task",
1529 async move {
1530 api.wait_for_initialized_connections().await;
1531 loop {
1532 module.receive_lnurl(custom_meta_fn()).await;
1533 }
1534 },
1535 );
1536 }
1537
1538 async fn receive_lnurl(&self, custom_meta: Value) {
1539 let stream_index = self
1550 .client_ctx
1551 .module_db()
1552 .begin_transaction_nc()
1553 .await
1554 .get_value(&IncomingContractStreamIndexKey)
1555 .await
1556 .unwrap_or(0);
1557
1558 let (contracts, next_index) = self
1559 .module_api
1560 .await_incoming_contracts(stream_index, 128)
1561 .await;
1562
1563 for contract in &contracts {
1564 if self
1570 .recover_contract_keys(self.lnurl_keypair.secret_key(), contract)
1571 .is_none()
1572 {
1573 continue;
1574 }
1575
1576 if !self.is_worth_claiming(contract.commitment.amount).await {
1577 warn!(
1578 target: LOG_CLIENT_MODULE_LNV2,
1579 amount = %contract.commitment.amount,
1580 "Ignoring incoming contract, its amount does not cover the claim fee"
1581 );
1582
1583 continue;
1584 }
1585
1586 if let Some(operation_id) = self
1587 .receive_incoming_contract(
1588 self.lnurl_keypair.secret_key(),
1589 contract.clone(),
1590 LightningOperationMeta::LnurlReceive(LnurlReceiveOperationMeta {
1591 contract: contract.clone(),
1592 custom_meta: custom_meta.clone(),
1593 }),
1594 )
1595 .await
1596 {
1597 self.await_final_receive_operation_state(operation_id)
1598 .await
1599 .ok();
1600 }
1601 }
1602
1603 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1612
1613 dbtx.insert_entry(&IncomingContractStreamIndexKey, &next_index)
1614 .await;
1615
1616 dbtx.commit_tx().await;
1617 }
1618}
1619
1620#[derive(Error, Debug, Clone, Eq, PartialEq)]
1621pub enum SelectGatewayError {
1622 #[error("Failed to request gateways")]
1623 FailedToRequestGateways(String),
1624 #[error("No gateways are available")]
1625 NoGatewaysAvailable,
1626 #[error("All gateways failed to respond")]
1627 GatewaysUnresponsive,
1628 #[error("No online gateway currently accepts incoming payments for this federation")]
1629 NoGatewayAcceptsReceives,
1630}
1631
1632#[derive(Debug, Clone, Eq, PartialEq)]
1635pub enum InvoiceSendStatus {
1636 NotAttempted,
1638 InFlight(OperationId),
1640 Succeeded(OperationId),
1643 Failed(OperationId),
1645}
1646
1647#[derive(Error, Debug, Clone, Eq, PartialEq)]
1648pub enum SendPaymentError {
1649 #[error("Invoice is missing an amount")]
1650 InvoiceMissingAmount,
1651 #[error("Invoice has expired")]
1652 InvoiceExpired,
1653 #[error("Payment attempt is duplicate")]
1654 DuplicatePaymentAttempt(OperationId),
1655 #[error(transparent)]
1656 SelectGateway(SelectGatewayError),
1657 #[error("Failed to connect to gateway")]
1658 FailedToConnectToGateway(String),
1659 #[error("Gateway does not support this federation")]
1660 FederationNotSupported,
1661 #[error("Gateway fee exceeds the allowed limit")]
1662 GatewayFeeExceedsLimit,
1663 #[error("Gateway expiration time exceeds the allowed limit")]
1664 GatewayExpirationExceedsLimit,
1665 #[error("Failed to request block count")]
1666 FailedToRequestBlockCount(String),
1667 #[error("Failed to fund the payment")]
1668 FailedToFundPayment(String),
1669 #[error("Invoice is for a different currency")]
1670 WrongCurrency {
1671 invoice_currency: Currency,
1672 federation_currency: Currency,
1673 },
1674}
1675
1676#[derive(Error, Debug, Clone, Eq, PartialEq)]
1678#[non_exhaustive]
1679pub enum SendWithTermsError {
1680 #[error("Gateway's send terms changed since they were checked")]
1683 TermsChanged {
1684 send_fee: PaymentFee,
1685 expiration_delta: u64,
1686 },
1687 #[error(transparent)]
1689 Send(#[from] SendPaymentError),
1690}
1691
1692#[derive(Error, Debug, Clone, Eq, PartialEq)]
1693pub enum ReceiveError {
1694 #[error(transparent)]
1695 SelectGateway(SelectGatewayError),
1696 #[error("Failed to connect to gateway")]
1697 FailedToConnectToGateway(String),
1698 #[error("Gateway does not support this federation")]
1699 FederationNotSupported,
1700 #[error("Gateway does not currently accept incoming payments for this federation")]
1701 ReceiveDisabled,
1702 #[error("Gateway fee exceeds the allowed limit")]
1703 GatewayFeeExceedsLimit,
1704 #[error("Amount is too small to cover fees")]
1705 AmountTooSmall,
1706 #[error("Gateway returned an invalid invoice")]
1707 InvalidInvoice,
1708 #[error("Gateway returned an invoice with incorrect amount")]
1709 IncorrectInvoiceAmount,
1710 #[error("Requested invoice expiry exceeds the maximum of one day")]
1711 InvoiceExpiryTooLong,
1712}
1713
1714#[derive(Error, Debug, Clone, Eq, PartialEq)]
1716#[non_exhaustive]
1717pub enum ReceiveWithTermsError {
1718 #[error("Gateway's receive fee changed since it was checked")]
1721 TermsChanged { receive_fee: PaymentFee },
1722 #[error(transparent)]
1724 Receive(#[from] ReceiveError),
1725}
1726
1727#[derive(Error, Debug, Clone, Eq, PartialEq)]
1728pub enum GenerateLnurlError {
1729 #[error("No gateways are available")]
1730 NoGatewaysAvailable,
1731 #[error("Failed to request gateways")]
1732 FailedToRequestGateways(String),
1733}
1734
1735#[derive(Error, Debug, Clone, Eq, PartialEq)]
1736pub enum ListGatewaysError {
1737 #[error("Failed to request gateways")]
1738 FailedToListGateways,
1739}
1740
1741#[derive(Error, Debug, Clone, Eq, PartialEq)]
1742pub enum RoutingInfoError {
1743 #[error("Failed to request routing info")]
1744 FailedToRequestRoutingInfo,
1745}
1746
1747#[derive(Debug, Error)]
1755#[non_exhaustive]
1756pub enum SpendableAmountError {
1757 #[error(transparent)]
1760 RoutingInfo(#[from] RoutingInfoError),
1761
1762 #[error("The gateway does not support this federation")]
1765 FederationNotSupported,
1766
1767 #[error(transparent)]
1770 SelectGateway(#[from] SelectGatewayError),
1771
1772 #[error("The gateway's default send fee {fee} exceeds the limit {limit}")]
1775 SendFeeExceedsLimit {
1776 fee: PaymentFee,
1778 limit: PaymentFee,
1780 },
1781
1782 #[error("The fee quote for the payment failed")]
1784 Quote(#[source] TransactionSubmitError),
1785
1786 #[error("The balance {balance} is too low to send any amount after fees")]
1789 BalanceTooLow {
1790 balance: Amount,
1792 },
1793}
1794
1795#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1796pub enum LightningClientStateMachines {
1797 Send(SendStateMachine),
1798 Receive(ReceiveStateMachine),
1799}
1800
1801impl IntoDynInstance for LightningClientStateMachines {
1802 type DynType = DynState;
1803
1804 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1805 DynState::from_typed(instance_id, self)
1806 }
1807}
1808
1809impl State for LightningClientStateMachines {
1810 type ModuleContext = LightningClientContext;
1811
1812 fn transitions(
1813 &self,
1814 context: &Self::ModuleContext,
1815 global_context: &DynGlobalClientContext,
1816 ) -> Vec<StateTransition<Self>> {
1817 match self {
1818 LightningClientStateMachines::Send(state) => {
1819 sm_enum_variant_translation!(
1820 state.transitions(context, global_context),
1821 LightningClientStateMachines::Send
1822 )
1823 }
1824 LightningClientStateMachines::Receive(state) => {
1825 sm_enum_variant_translation!(
1826 state.transitions(context, global_context),
1827 LightningClientStateMachines::Receive
1828 )
1829 }
1830 }
1831 }
1832
1833 fn operation_id(&self) -> OperationId {
1834 match self {
1835 LightningClientStateMachines::Send(state) => state.operation_id(),
1836 LightningClientStateMachines::Receive(state) => state.operation_id(),
1837 }
1838 }
1839}