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;
12mod 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::module::init::{ClientModuleInit, ClientModuleInitArgs};
26use fedimint_client_module::module::recovery::NoModuleBackup;
27use fedimint_client_module::module::{ClientContext, ClientModule, OutPointRange};
28use fedimint_client_module::oplog::UpdateStreamOrOutcome;
29use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
30use fedimint_client_module::transaction::{
31 ClientOutput, ClientOutputBundle, ClientOutputSM, TransactionBuilder,
32};
33use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
34use fedimint_core::config::FederationId;
35use fedimint_core::core::{IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
36use fedimint_core::db::{DatabaseTransaction, IDatabaseTransactionOpsCoreTyped};
37use fedimint_core::encoding::{Decodable, Encodable};
38use fedimint_core::module::{
39 Amounts, ApiAuth, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
40};
41use fedimint_core::secp256k1::SECP256K1;
42use fedimint_core::task::TaskGroup;
43use fedimint_core::time::duration_since_epoch;
44use fedimint_core::util::SafeUrl;
45use fedimint_core::{Amount, PeerId, apply, async_trait_maybe_send};
46use fedimint_derive_secret::{ChildId, DerivableSecret};
47use fedimint_lnv2_common::config::LightningClientConfig;
48use fedimint_lnv2_common::contracts::{IncomingContract, OutgoingContract, PaymentImage};
49use fedimint_lnv2_common::gateway_api::{
50 GatewayConnection, PaymentFee, RealGatewayConnection, RoutingInfo,
51};
52use fedimint_lnv2_common::{
53 Bolt11InvoiceDescription, GatewayApi, KIND, LightningCommonInit, LightningInvoice,
54 LightningModuleTypes, LightningOutput, LightningOutputV0, MINIMUM_INCOMING_CONTRACT_AMOUNT,
55 lnurl, tweak,
56};
57use futures::StreamExt;
58use lightning_invoice::{Bolt11Invoice, Currency};
59use secp256k1::{Keypair, PublicKey, Scalar, SecretKey, ecdh};
60use serde::{Deserialize, Serialize};
61use serde_json::Value;
62use strum::IntoEnumIterator as _;
63use thiserror::Error;
64use tpe::{AggregateDecryptionKey, derive_agg_dk};
65use tracing::warn;
66
67use crate::api::LightningFederationApi;
68use crate::events::SendPaymentEvent;
69use crate::receive_sm::{ReceiveSMCommon, ReceiveSMState, ReceiveStateMachine};
70use crate::send_sm::{SendSMCommon, SendSMState, SendStateMachine};
71
72const EXPIRATION_DELTA_LIMIT: u64 = 1440;
75
76const CONTRACT_CONFIRMATION_BUFFER: u64 = 12;
78
79#[allow(clippy::large_enum_variant)]
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub enum LightningOperationMeta {
82 Send(SendOperationMeta),
83 Receive(ReceiveOperationMeta),
84 LnurlReceive(LnurlReceiveOperationMeta),
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct SendOperationMeta {
89 pub change_outpoint_range: OutPointRange,
90 pub gateway: SafeUrl,
91 pub contract: OutgoingContract,
92 pub invoice: LightningInvoice,
93 pub custom_meta: Value,
94}
95
96impl SendOperationMeta {
97 pub fn gateway_fee(&self) -> Amount {
99 match &self.invoice {
100 LightningInvoice::Bolt11(invoice) => self.contract.amount.saturating_sub(
101 Amount::from_msats(invoice.amount_milli_satoshis().expect("Invoice has amount")),
102 ),
103 }
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ReceiveOperationMeta {
109 pub gateway: SafeUrl,
110 pub contract: IncomingContract,
111 pub invoice: LightningInvoice,
112 pub custom_meta: Value,
113}
114
115impl ReceiveOperationMeta {
116 pub fn gateway_fee(&self) -> Amount {
118 match &self.invoice {
119 LightningInvoice::Bolt11(invoice) => {
120 Amount::from_msats(invoice.amount_milli_satoshis().expect("Invoice has amount"))
121 .saturating_sub(self.contract.commitment.amount)
122 }
123 }
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct LnurlReceiveOperationMeta {
129 pub contract: IncomingContract,
130 pub custom_meta: Value,
131}
132
133#[cfg_attr(doc, aquamarine::aquamarine)]
134#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
152pub enum SendOperationState {
153 Funding,
155 Funded,
157 Success([u8; 32]),
159 Refunding,
161 Refunded,
163 Failure,
165}
166
167#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
169pub enum FinalSendOperationState {
170 Success,
172 Refunded,
174 Failure,
176}
177
178pub type SendResult = Result<OperationId, SendPaymentError>;
179
180#[cfg_attr(doc, aquamarine::aquamarine)]
181#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
193pub enum ReceiveOperationState {
194 Pending,
196 Expired,
198 Claiming,
200 Claimed,
202 Failure,
204}
205
206#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
208pub enum FinalReceiveOperationState {
209 Expired,
211 Claimed,
213 Failure,
215}
216
217pub type ReceiveResult = Result<(Bolt11Invoice, OperationId), ReceiveError>;
218
219#[derive(Clone)]
220pub struct LightningClientInit {
221 pub gateway_conn: Option<Arc<dyn GatewayConnection + Send + Sync>>,
222 pub custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
223}
224
225impl std::fmt::Debug for LightningClientInit {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 f.debug_struct("LightningClientInit")
228 .field("gateway_conn", &self.gateway_conn)
229 .field("custom_meta_fn", &"<function>")
230 .finish()
231 }
232}
233
234impl Default for LightningClientInit {
235 fn default() -> Self {
236 LightningClientInit {
237 gateway_conn: None,
238 custom_meta_fn: Arc::new(|| Value::Null),
239 }
240 }
241}
242
243impl ModuleInit for LightningClientInit {
244 type Common = LightningCommonInit;
245
246 async fn dump_database(
247 &self,
248 _dbtx: &mut DatabaseTransaction<'_>,
249 _prefix_names: Vec<String>,
250 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
251 Box::new(BTreeMap::new().into_iter())
252 }
253}
254
255#[apply(async_trait_maybe_send!)]
256impl ClientModuleInit for LightningClientInit {
257 type Module = LightningClientModule;
258
259 fn supported_api_versions(&self) -> MultiApiVersion {
260 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
261 .expect("no version conflicts")
262 }
263
264 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
265 let gateway_conn = if let Some(gateway_conn) = self.gateway_conn.clone() {
266 gateway_conn
267 } else {
268 let api = GatewayApi::new(None, args.connector_registry.clone());
269 Arc::new(RealGatewayConnection { api })
270 };
271 Ok(LightningClientModule::new(
272 *args.federation_id(),
273 args.cfg().clone(),
274 args.notifier().clone(),
275 args.context(),
276 args.module_api().clone(),
277 args.module_root_secret(),
278 gateway_conn,
279 self.custom_meta_fn.clone(),
280 args.admin_auth().cloned(),
281 args.task_group(),
282 ))
283 }
284
285 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
286 Some(
287 DbKeyPrefix::iter()
288 .map(|p| p as u8)
289 .chain(
290 DbKeyPrefix::ExternalReservedStart as u8
291 ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
292 )
293 .collect(),
294 )
295 }
296}
297
298#[derive(Debug, Clone)]
299pub struct LightningClientContext {
300 federation_id: FederationId,
301 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
302 pub(crate) client_ctx: ClientContext<LightningClientModule>,
303}
304
305impl Context for LightningClientContext {
306 const KIND: Option<ModuleKind> = Some(KIND);
307}
308
309#[derive(Debug, Clone)]
310pub struct LightningClientModule {
311 federation_id: FederationId,
312 cfg: LightningClientConfig,
313 notifier: ModuleNotifier<LightningClientStateMachines>,
314 client_ctx: ClientContext<Self>,
315 module_api: DynModuleApi,
316 keypair: Keypair,
317 lnurl_keypair: Keypair,
318 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
319 #[allow(unused)] admin_auth: Option<ApiAuth>,
321}
322
323#[apply(async_trait_maybe_send!)]
324impl ClientModule for LightningClientModule {
325 type Init = LightningClientInit;
326 type Common = LightningModuleTypes;
327 type Backup = NoModuleBackup;
328 type ModuleStateMachineContext = LightningClientContext;
329 type States = LightningClientStateMachines;
330
331 fn context(&self) -> Self::ModuleStateMachineContext {
332 LightningClientContext {
333 federation_id: self.federation_id,
334 gateway_conn: self.gateway_conn.clone(),
335 client_ctx: self.client_ctx.clone(),
336 }
337 }
338
339 fn input_fee(
340 &self,
341 amounts: &Amounts,
342 _input: &<Self::Common as ModuleCommon>::Input,
343 ) -> Option<Amounts> {
344 Some(Amounts::new_bitcoin(
345 self.cfg.fee_consensus.fee(amounts.expect_only_bitcoin()),
346 ))
347 }
348
349 fn output_fee(
350 &self,
351 amounts: &Amounts,
352 _output: &<Self::Common as ModuleCommon>::Output,
353 ) -> Option<Amounts> {
354 Some(Amounts::new_bitcoin(
355 self.cfg.fee_consensus.fee(amounts.expect_only_bitcoin()),
356 ))
357 }
358
359 #[cfg(feature = "cli")]
360 async fn handle_cli_command(
361 &self,
362 args: &[std::ffi::OsString],
363 ) -> anyhow::Result<serde_json::Value> {
364 cli::handle_cli_command(self, args).await
365 }
366}
367
368impl LightningClientModule {
369 #[allow(clippy::too_many_arguments)]
370 fn new(
371 federation_id: FederationId,
372 cfg: LightningClientConfig,
373 notifier: ModuleNotifier<LightningClientStateMachines>,
374 client_ctx: ClientContext<Self>,
375 module_api: DynModuleApi,
376 module_root_secret: &DerivableSecret,
377 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
378 custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
379 admin_auth: Option<ApiAuth>,
380 task_group: &TaskGroup,
381 ) -> Self {
382 let module = Self {
383 federation_id,
384 cfg,
385 notifier,
386 client_ctx,
387 module_api,
388 keypair: module_root_secret
389 .child_key(ChildId(0))
390 .to_secp_key(SECP256K1),
391 lnurl_keypair: module_root_secret
392 .child_key(ChildId(1))
393 .to_secp_key(SECP256K1),
394 gateway_conn,
395 admin_auth,
396 };
397
398 module.spawn_receive_lnurl_task(custom_meta_fn, task_group);
399
400 module.spawn_gateway_map_update_task(task_group);
401
402 module
403 }
404
405 fn spawn_gateway_map_update_task(&self, task_group: &TaskGroup) {
406 let module = self.clone();
407 let api = self.module_api.clone();
408
409 task_group.spawn_cancellable("gateway_map_update_task", async move {
410 api.wait_for_initialized_connections().await;
411 module.update_gateway_map().await;
412 });
413 }
414
415 async fn update_gateway_map(&self) {
416 if let Ok(gateways) = self.module_api.gateways().await {
423 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
424
425 for gateway in gateways {
426 if let Ok(Some(routing_info)) = self
427 .gateway_conn
428 .routing_info(gateway.clone(), &self.federation_id)
429 .await
430 {
431 dbtx.insert_entry(&GatewayKey(routing_info.lightning_public_key), &gateway)
432 .await;
433 }
434 }
435
436 if let Err(e) = dbtx.commit_tx_result().await {
437 warn!("Failed to commit the updated gateway mapping to the database: {e}");
438 }
439 }
440 }
441
442 pub async fn select_gateway(
447 &self,
448 invoice: Option<Bolt11Invoice>,
449 ) -> Result<(SafeUrl, RoutingInfo), SelectGatewayError> {
450 let gateways = self
451 .module_api
452 .gateways()
453 .await
454 .map_err(|e| SelectGatewayError::FailedToRequestGateways(e.to_string()))?;
455
456 if gateways.is_empty() {
457 return Err(SelectGatewayError::NoGatewaysAvailable);
458 }
459
460 if let Some(invoice) = invoice
461 && let Some(gateway) = self
462 .client_ctx
463 .module_db()
464 .begin_transaction_nc()
465 .await
466 .get_value(&GatewayKey(invoice.recover_payee_pub_key()))
467 .await
468 .filter(|gateway| gateways.contains(gateway))
469 && let Ok(Some(routing_info)) = self.routing_info(&gateway).await
470 {
471 return Ok((gateway, routing_info));
472 }
473
474 for gateway in gateways {
475 if let Ok(Some(routing_info)) = self.routing_info(&gateway).await {
476 return Ok((gateway, routing_info));
477 }
478 }
479
480 Err(SelectGatewayError::GatewaysUnresponsive)
481 }
482
483 pub async fn list_gateways(
486 &self,
487 peer: Option<PeerId>,
488 ) -> Result<Vec<SafeUrl>, ListGatewaysError> {
489 if let Some(peer) = peer {
490 self.module_api
491 .gateways_from_peer(peer)
492 .await
493 .map_err(|_| ListGatewaysError::FailedToListGateways)
494 } else {
495 self.module_api
496 .gateways()
497 .await
498 .map_err(|_| ListGatewaysError::FailedToListGateways)
499 }
500 }
501
502 pub async fn routing_info(
505 &self,
506 gateway: &SafeUrl,
507 ) -> Result<Option<RoutingInfo>, RoutingInfoError> {
508 self.gateway_conn
509 .routing_info(gateway.clone(), &self.federation_id)
510 .await
511 .map_err(|_| RoutingInfoError::FailedToRequestRoutingInfo)
512 }
513
514 #[allow(clippy::too_many_lines)]
531 pub async fn send(
532 &self,
533 invoice: Bolt11Invoice,
534 gateway: Option<SafeUrl>,
535 custom_meta: Value,
536 ) -> Result<OperationId, SendPaymentError> {
537 let amount = invoice
538 .amount_milli_satoshis()
539 .ok_or(SendPaymentError::InvoiceMissingAmount)?;
540
541 if invoice.is_expired() {
542 return Err(SendPaymentError::InvoiceExpired);
543 }
544
545 if self.cfg.network != invoice.currency().into() {
546 return Err(SendPaymentError::WrongCurrency {
547 invoice_currency: invoice.currency(),
548 federation_currency: self.cfg.network.into(),
549 });
550 }
551
552 let operation_id = self.get_next_operation_id(&invoice).await?;
553
554 let (ephemeral_tweak, ephemeral_pk) = tweak::generate(self.keypair.public_key());
555
556 let refund_keypair = SecretKey::from_slice(&ephemeral_tweak)
557 .expect("32 bytes, within curve order")
558 .keypair(secp256k1::SECP256K1);
559
560 let (gateway_api, routing_info) = match gateway {
561 Some(gateway_api) => (
562 gateway_api.clone(),
563 self.routing_info(&gateway_api)
564 .await
565 .map_err(|e| SendPaymentError::FailedToConnectToGateway(e.to_string()))?
566 .ok_or(SendPaymentError::FederationNotSupported)?,
567 ),
568 None => self
569 .select_gateway(Some(invoice.clone()))
570 .await
571 .map_err(SendPaymentError::SelectGateway)?,
572 };
573
574 let (send_fee, expiration_delta) = routing_info.send_parameters(&invoice);
575
576 if !send_fee.le(&PaymentFee::SEND_FEE_LIMIT) {
577 return Err(SendPaymentError::GatewayFeeExceedsLimit);
578 }
579
580 if EXPIRATION_DELTA_LIMIT < expiration_delta {
581 return Err(SendPaymentError::GatewayExpirationExceedsLimit);
582 }
583
584 let consensus_block_count = self
585 .module_api
586 .consensus_block_count()
587 .await
588 .map_err(|e| SendPaymentError::FailedToRequestBlockCount(e.to_string()))?;
589
590 let contract = OutgoingContract {
591 payment_image: PaymentImage::Hash(*invoice.payment_hash()),
592 amount: send_fee.add_to(amount),
593 expiration: consensus_block_count + expiration_delta + CONTRACT_CONFIRMATION_BUFFER,
594 claim_pk: routing_info.module_public_key,
595 refund_pk: refund_keypair.public_key(),
596 ephemeral_pk,
597 };
598
599 let contract_clone = contract.clone();
600 let gateway_api_clone = gateway_api.clone();
601 let invoice_clone = invoice.clone();
602
603 let client_output = ClientOutput::<LightningOutput> {
604 output: LightningOutput::V0(LightningOutputV0::Outgoing(contract.clone())),
605 amounts: Amounts::new_bitcoin(contract.amount),
606 };
607
608 let client_output_sm = ClientOutputSM::<LightningClientStateMachines> {
609 state_machines: Arc::new(move |range: OutPointRange| {
610 vec![LightningClientStateMachines::Send(SendStateMachine {
611 common: SendSMCommon {
612 operation_id,
613 outpoint: range.into_iter().next().unwrap(),
614 contract: contract_clone.clone(),
615 gateway_api: Some(gateway_api_clone.clone()),
616 invoice: Some(LightningInvoice::Bolt11(invoice_clone.clone())),
617 refund_keypair,
618 },
619 state: SendSMState::Funding,
620 })]
621 }),
622 };
623
624 let client_output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
625 vec![client_output],
626 vec![client_output_sm],
627 ));
628
629 let transaction = TransactionBuilder::new().with_outputs(client_output);
630
631 self.client_ctx
632 .finalize_and_submit_transaction(
633 operation_id,
634 LightningCommonInit::KIND.as_str(),
635 move |change_outpoint_range| {
636 LightningOperationMeta::Send(SendOperationMeta {
637 change_outpoint_range,
638 gateway: gateway_api.clone(),
639 contract: contract.clone(),
640 invoice: LightningInvoice::Bolt11(invoice.clone()),
641 custom_meta: custom_meta.clone(),
642 })
643 },
644 transaction,
645 )
646 .await
647 .map_err(|e| SendPaymentError::FailedToFundPayment(e.to_string()))?;
648
649 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
650
651 self.client_ctx
652 .log_event(
653 &mut dbtx,
654 SendPaymentEvent {
655 operation_id,
656 amount: send_fee.add_to(amount),
657 fee: send_fee.fee(amount),
658 },
659 )
660 .await;
661
662 dbtx.commit_tx().await;
663
664 Ok(operation_id)
665 }
666
667 async fn get_next_operation_id(
668 &self,
669 invoice: &Bolt11Invoice,
670 ) -> Result<OperationId, SendPaymentError> {
671 for payment_attempt in 0..u64::MAX {
672 let operation_id = OperationId::from_encodable(&(invoice.clone(), payment_attempt));
673
674 if !self.client_ctx.operation_exists(operation_id).await {
675 return Ok(operation_id);
676 }
677
678 if self.client_ctx.has_active_states(operation_id).await {
679 return Err(SendPaymentError::PaymentInProgress(operation_id));
680 }
681
682 let mut stream = self
683 .subscribe_send_operation_state_updates(operation_id)
684 .await
685 .expect("operation_id exists")
686 .into_stream();
687
688 while let Some(state) = stream.next().await {
691 if let SendOperationState::Success(_) = state {
692 return Err(SendPaymentError::InvoiceAlreadyPaid(operation_id));
693 }
694 }
695 }
696
697 panic!("We could not find an unused operation id for sending a lightning payment");
698 }
699
700 pub async fn subscribe_send_operation_state_updates(
702 &self,
703 operation_id: OperationId,
704 ) -> anyhow::Result<UpdateStreamOrOutcome<SendOperationState>> {
705 let operation = self.client_ctx.get_operation(operation_id).await?;
706 let mut stream = self.notifier.subscribe(operation_id).await;
707 let client_ctx = self.client_ctx.clone();
708 let module_api = self.module_api.clone();
709
710 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
711 stream! {
712 loop {
713 if let Some(LightningClientStateMachines::Send(state)) = stream.next().await {
714 match state.state {
715 SendSMState::Funding => yield SendOperationState::Funding,
716 SendSMState::Funded => yield SendOperationState::Funded,
717 SendSMState::Success(preimage) => {
718 assert!(state.common.contract.verify_preimage(&preimage));
720
721 yield SendOperationState::Success(preimage);
722 return;
723 },
724 SendSMState::Refunding(out_points) => {
725 yield SendOperationState::Refunding;
726
727 if client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await.is_ok() {
728 yield SendOperationState::Refunded;
729 return;
730 }
731
732 if let Some(preimage) = module_api.await_preimage(
736 state.common.outpoint,
737 0
738 ).await
739 && state.common.contract.verify_preimage(&preimage) {
740 yield SendOperationState::Success(preimage);
741 return;
742 }
743
744 yield SendOperationState::Failure;
745 return;
746 },
747 SendSMState::Rejected(..) => {
748 yield SendOperationState::Failure;
749 return;
750 },
751 }
752 }
753 }
754 }
755 }))
756 }
757
758 pub async fn await_final_send_operation_state(
760 &self,
761 operation_id: OperationId,
762 ) -> anyhow::Result<FinalSendOperationState> {
763 let mut stream = self
764 .subscribe_send_operation_state_updates(operation_id)
765 .await?
766 .into_stream();
767
768 let mut final_state = None;
769
770 while let Some(state) = stream.next().await {
771 match state {
772 SendOperationState::Success(_) => {
773 final_state = Some(FinalSendOperationState::Success);
774 }
775 SendOperationState::Refunded => {
776 final_state = Some(FinalSendOperationState::Refunded);
777 }
778 SendOperationState::Failure => final_state = Some(FinalSendOperationState::Failure),
779 _ => {}
780 }
781 }
782
783 Ok(final_state.expect("Stream contains one final state"))
784 }
785
786 pub async fn receive(
798 &self,
799 amount: Amount,
800 expiry_secs: u32,
801 description: Bolt11InvoiceDescription,
802 gateway: Option<SafeUrl>,
803 custom_meta: Value,
804 ) -> Result<(Bolt11Invoice, OperationId), ReceiveError> {
805 let (gateway, contract, invoice) = self
806 .create_contract_and_fetch_invoice(
807 self.keypair.public_key(),
808 amount,
809 expiry_secs,
810 description,
811 gateway,
812 )
813 .await?;
814
815 let operation_id = self
816 .receive_incoming_contract(
817 self.keypair.secret_key(),
818 contract.clone(),
819 LightningOperationMeta::Receive(ReceiveOperationMeta {
820 gateway,
821 contract,
822 invoice: LightningInvoice::Bolt11(invoice.clone()),
823 custom_meta,
824 }),
825 )
826 .await
827 .expect("The contract has been generated with our public key");
828
829 Ok((invoice, operation_id))
830 }
831
832 async fn create_contract_and_fetch_invoice(
836 &self,
837 recipient_static_pk: PublicKey,
838 amount: Amount,
839 expiry_secs: u32,
840 description: Bolt11InvoiceDescription,
841 gateway: Option<SafeUrl>,
842 ) -> Result<(SafeUrl, IncomingContract, Bolt11Invoice), ReceiveError> {
843 let (ephemeral_tweak, ephemeral_pk) = tweak::generate(recipient_static_pk);
844
845 let encryption_seed = ephemeral_tweak
846 .consensus_hash::<sha256::Hash>()
847 .to_byte_array();
848
849 let preimage = encryption_seed
850 .consensus_hash::<sha256::Hash>()
851 .to_byte_array();
852
853 let (gateway, routing_info) = match gateway {
854 Some(gateway) => (
855 gateway.clone(),
856 self.routing_info(&gateway)
857 .await
858 .map_err(|e| ReceiveError::FailedToConnectToGateway(e.to_string()))?
859 .ok_or(ReceiveError::FederationNotSupported)?,
860 ),
861 None => self
862 .select_gateway(None)
863 .await
864 .map_err(ReceiveError::SelectGateway)?,
865 };
866
867 if !routing_info.receive_fee.le(&PaymentFee::RECEIVE_FEE_LIMIT) {
868 return Err(ReceiveError::GatewayFeeExceedsLimit);
869 }
870
871 let contract_amount = routing_info.receive_fee.subtract_from(amount.msats);
872
873 if contract_amount < MINIMUM_INCOMING_CONTRACT_AMOUNT {
874 return Err(ReceiveError::AmountTooSmall);
875 }
876
877 let expiration = duration_since_epoch()
878 .as_secs()
879 .saturating_add(u64::from(expiry_secs));
880
881 let claim_pk = recipient_static_pk
882 .mul_tweak(
883 secp256k1::SECP256K1,
884 &Scalar::from_be_bytes(ephemeral_tweak).expect("Within curve order"),
885 )
886 .expect("Tweak is valid");
887
888 let contract = IncomingContract::new(
889 self.cfg.tpe_agg_pk,
890 encryption_seed,
891 preimage,
892 PaymentImage::Hash(preimage.consensus_hash()),
893 contract_amount,
894 expiration,
895 claim_pk,
896 routing_info.module_public_key,
897 ephemeral_pk,
898 );
899
900 let invoice = self
901 .gateway_conn
902 .bolt11_invoice(
903 gateway.clone(),
904 self.federation_id,
905 contract.clone(),
906 amount,
907 description,
908 expiry_secs,
909 )
910 .await
911 .map_err(|e| ReceiveError::FailedToConnectToGateway(e.to_string()))?;
912
913 if invoice.payment_hash() != &preimage.consensus_hash() {
914 return Err(ReceiveError::InvalidInvoice);
915 }
916
917 if invoice.amount_milli_satoshis() != Some(amount.msats) {
918 return Err(ReceiveError::IncorrectInvoiceAmount);
919 }
920
921 Ok((gateway, contract, invoice))
922 }
923
924 async fn receive_incoming_contract(
927 &self,
928 sk: SecretKey,
929 contract: IncomingContract,
930 operation_meta: LightningOperationMeta,
931 ) -> Option<OperationId> {
932 let operation_id = OperationId::from_encodable(&contract.clone());
933
934 let (claim_keypair, agg_decryption_key) = self.recover_contract_keys(sk, &contract)?;
935
936 let receive_sm = LightningClientStateMachines::Receive(ReceiveStateMachine {
937 common: ReceiveSMCommon {
938 operation_id,
939 contract: contract.clone(),
940 claim_keypair,
941 agg_decryption_key,
942 },
943 state: ReceiveSMState::Pending,
944 });
945
946 self.client_ctx
949 .manual_operation_start(
950 operation_id,
951 LightningCommonInit::KIND.as_str(),
952 operation_meta,
953 vec![self.client_ctx.make_dyn_state(receive_sm)],
954 )
955 .await
956 .ok();
957
958 Some(operation_id)
959 }
960
961 fn recover_contract_keys(
962 &self,
963 sk: SecretKey,
964 contract: &IncomingContract,
965 ) -> Option<(Keypair, AggregateDecryptionKey)> {
966 let tweak = ecdh::SharedSecret::new(&contract.commitment.ephemeral_pk, &sk);
967
968 let encryption_seed = tweak
969 .secret_bytes()
970 .consensus_hash::<sha256::Hash>()
971 .to_byte_array();
972
973 let claim_keypair = sk
974 .mul_tweak(&Scalar::from_be_bytes(tweak.secret_bytes()).expect("Within curve order"))
975 .expect("Tweak is valid")
976 .keypair(secp256k1::SECP256K1);
977
978 if claim_keypair.public_key() != contract.commitment.claim_pk {
979 return None; }
981
982 let agg_decryption_key = derive_agg_dk(&self.cfg.tpe_agg_pk, &encryption_seed);
983
984 if !contract.verify_agg_decryption_key(&self.cfg.tpe_agg_pk, &agg_decryption_key) {
985 return None; }
987
988 contract.decrypt_preimage(&agg_decryption_key)?;
989
990 Some((claim_keypair, agg_decryption_key))
991 }
992
993 pub async fn subscribe_receive_operation_state_updates(
995 &self,
996 operation_id: OperationId,
997 ) -> anyhow::Result<UpdateStreamOrOutcome<ReceiveOperationState>> {
998 let operation = self.client_ctx.get_operation(operation_id).await?;
999 let mut stream = self.notifier.subscribe(operation_id).await;
1000 let client_ctx = self.client_ctx.clone();
1001
1002 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
1003 stream! {
1004 loop {
1005 if let Some(LightningClientStateMachines::Receive(state)) = stream.next().await {
1006 match state.state {
1007 ReceiveSMState::Pending => yield ReceiveOperationState::Pending,
1008 ReceiveSMState::Claiming(out_points) => {
1009 yield ReceiveOperationState::Claiming;
1010
1011 if client_ctx.await_primary_module_outputs(operation_id, out_points).await.is_ok() {
1012 yield ReceiveOperationState::Claimed;
1013 } else {
1014 yield ReceiveOperationState::Failure;
1015 }
1016 return;
1017 },
1018 ReceiveSMState::Expired => {
1019 yield ReceiveOperationState::Expired;
1020 return;
1021 }
1022 }
1023 }
1024 }
1025 }
1026 }))
1027 }
1028
1029 pub async fn await_final_receive_operation_state(
1031 &self,
1032 operation_id: OperationId,
1033 ) -> anyhow::Result<FinalReceiveOperationState> {
1034 let mut stream = self
1035 .subscribe_receive_operation_state_updates(operation_id)
1036 .await?
1037 .into_stream();
1038
1039 let mut final_state = None;
1040
1041 while let Some(state) = stream.next().await {
1042 match state {
1043 ReceiveOperationState::Expired => {
1044 final_state = Some(FinalReceiveOperationState::Expired);
1045 }
1046 ReceiveOperationState::Claimed => {
1047 final_state = Some(FinalReceiveOperationState::Claimed);
1048 }
1049 ReceiveOperationState::Failure => {
1050 final_state = Some(FinalReceiveOperationState::Failure);
1051 }
1052 _ => {}
1053 }
1054 }
1055
1056 Ok(final_state.expect("Stream contains one final state"))
1057 }
1058
1059 pub async fn generate_lnurl(
1062 &self,
1063 recurringd: SafeUrl,
1064 gateway: Option<SafeUrl>,
1065 ) -> Result<String, GenerateLnurlError> {
1066 let gateways = if let Some(gateway) = gateway {
1067 vec![gateway]
1068 } else {
1069 let gateways = self
1070 .module_api
1071 .gateways()
1072 .await
1073 .map_err(|e| GenerateLnurlError::FailedToRequestGateways(e.to_string()))?;
1074
1075 if gateways.is_empty() {
1076 return Err(GenerateLnurlError::NoGatewaysAvailable);
1077 }
1078
1079 gateways
1080 };
1081
1082 let payload = fedimint_core::base32::encode_prefixed(
1083 fedimint_core::base32::FEDIMINT_PREFIX,
1084 &lnurl::LnurlRequest {
1085 federation_id: self.federation_id,
1086 recipient_pk: self.lnurl_keypair.public_key(),
1087 aggregate_pk: self.cfg.tpe_agg_pk,
1088 gateways,
1089 },
1090 );
1091
1092 Ok(fedimint_lnurl::encode_lnurl(&format!(
1093 "{recurringd}pay/{payload}"
1094 )))
1095 }
1096
1097 fn spawn_receive_lnurl_task(
1098 &self,
1099 custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
1100 task_group: &TaskGroup,
1101 ) {
1102 let module = self.clone();
1103 let api = self.module_api.clone();
1104
1105 task_group.spawn_cancellable("receive_lnurl_task", async move {
1106 api.wait_for_initialized_connections().await;
1107 loop {
1108 module.receive_lnurl(custom_meta_fn()).await;
1109 }
1110 });
1111 }
1112
1113 async fn receive_lnurl(&self, custom_meta: Value) {
1114 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1115
1116 let stream_index = dbtx
1117 .get_value(&IncomingContractStreamIndexKey)
1118 .await
1119 .unwrap_or(0);
1120
1121 let (contracts, next_index) = self
1122 .module_api
1123 .await_incoming_contracts(stream_index, 128)
1124 .await;
1125
1126 for contract in &contracts {
1127 if let Some(operation_id) = self
1128 .receive_incoming_contract(
1129 self.lnurl_keypair.secret_key(),
1130 contract.clone(),
1131 LightningOperationMeta::LnurlReceive(LnurlReceiveOperationMeta {
1132 contract: contract.clone(),
1133 custom_meta: custom_meta.clone(),
1134 }),
1135 )
1136 .await
1137 {
1138 self.await_final_receive_operation_state(operation_id)
1139 .await
1140 .ok();
1141 }
1142 }
1143
1144 dbtx.insert_entry(&IncomingContractStreamIndexKey, &next_index)
1145 .await;
1146
1147 dbtx.commit_tx().await;
1148 }
1149}
1150
1151#[derive(Error, Debug, Clone, Eq, PartialEq)]
1152pub enum SelectGatewayError {
1153 #[error("Failed to request gateways")]
1154 FailedToRequestGateways(String),
1155 #[error("No gateways are available")]
1156 NoGatewaysAvailable,
1157 #[error("All gateways failed to respond")]
1158 GatewaysUnresponsive,
1159}
1160
1161#[derive(Error, Debug, Clone, Eq, PartialEq)]
1162pub enum SendPaymentError {
1163 #[error("Invoice is missing an amount")]
1164 InvoiceMissingAmount,
1165 #[error("Invoice has expired")]
1166 InvoiceExpired,
1167 #[error("A payment for this invoice is already in progress")]
1168 PaymentInProgress(OperationId),
1169 #[error("This invoice has already been paid")]
1170 InvoiceAlreadyPaid(OperationId),
1171 #[error(transparent)]
1172 SelectGateway(SelectGatewayError),
1173 #[error("Failed to connect to gateway")]
1174 FailedToConnectToGateway(String),
1175 #[error("Gateway does not support this federation")]
1176 FederationNotSupported,
1177 #[error("Gateway fee exceeds the allowed limit")]
1178 GatewayFeeExceedsLimit,
1179 #[error("Gateway expiration time exceeds the allowed limit")]
1180 GatewayExpirationExceedsLimit,
1181 #[error("Failed to request block count")]
1182 FailedToRequestBlockCount(String),
1183 #[error("Failed to fund the payment")]
1184 FailedToFundPayment(String),
1185 #[error("Invoice is for a different currency")]
1186 WrongCurrency {
1187 invoice_currency: Currency,
1188 federation_currency: Currency,
1189 },
1190}
1191
1192#[derive(Error, Debug, Clone, Eq, PartialEq)]
1193pub enum ReceiveError {
1194 #[error(transparent)]
1195 SelectGateway(SelectGatewayError),
1196 #[error("Failed to connect to gateway")]
1197 FailedToConnectToGateway(String),
1198 #[error("Gateway does not support this federation")]
1199 FederationNotSupported,
1200 #[error("Gateway fee exceeds the allowed limit")]
1201 GatewayFeeExceedsLimit,
1202 #[error("Amount is too small to cover fees")]
1203 AmountTooSmall,
1204 #[error("Gateway returned an invalid invoice")]
1205 InvalidInvoice,
1206 #[error("Gateway returned an invoice with incorrect amount")]
1207 IncorrectInvoiceAmount,
1208}
1209
1210#[derive(Error, Debug, Clone, Eq, PartialEq)]
1211pub enum GenerateLnurlError {
1212 #[error("No gateways are available")]
1213 NoGatewaysAvailable,
1214 #[error("Failed to request gateways")]
1215 FailedToRequestGateways(String),
1216}
1217
1218#[derive(Error, Debug, Clone, Eq, PartialEq)]
1219pub enum ListGatewaysError {
1220 #[error("Failed to request gateways")]
1221 FailedToListGateways,
1222}
1223
1224#[derive(Error, Debug, Clone, Eq, PartialEq)]
1225pub enum RoutingInfoError {
1226 #[error("Failed to request routing info")]
1227 FailedToRequestRoutingInfo,
1228}
1229
1230#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1231pub enum LightningClientStateMachines {
1232 Send(SendStateMachine),
1233 Receive(ReceiveStateMachine),
1234}
1235
1236impl IntoDynInstance for LightningClientStateMachines {
1237 type DynType = DynState;
1238
1239 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1240 DynState::from_typed(instance_id, self)
1241 }
1242}
1243
1244impl State for LightningClientStateMachines {
1245 type ModuleContext = LightningClientContext;
1246
1247 fn transitions(
1248 &self,
1249 context: &Self::ModuleContext,
1250 global_context: &DynGlobalClientContext,
1251 ) -> Vec<StateTransition<Self>> {
1252 match self {
1253 LightningClientStateMachines::Send(state) => {
1254 sm_enum_variant_translation!(
1255 state.transitions(context, global_context),
1256 LightningClientStateMachines::Send
1257 )
1258 }
1259 LightningClientStateMachines::Receive(state) => {
1260 sm_enum_variant_translation!(
1261 state.transitions(context, global_context),
1262 LightningClientStateMachines::Receive
1263 )
1264 }
1265 }
1266 }
1267
1268 fn operation_id(&self) -> OperationId {
1269 match self {
1270 LightningClientStateMachines::Send(state) => state.operation_id(),
1271 LightningClientStateMachines::Receive(state) => state.operation_id(),
1272 }
1273 }
1274}