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