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, FeeQuote, FeeQuoteRequest, 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(#[serde(with = "fedimint_core::hex::serde")] [u8; 32]),
173 Refunded,
175 Failure,
177}
178
179pub type SendResult = Result<OperationId, SendPaymentError>;
180
181#[cfg_attr(doc, aquamarine::aquamarine)]
182#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
194pub enum ReceiveOperationState {
195 Pending,
197 Expired,
199 Claiming,
201 Claimed,
203 Failure,
205}
206
207#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
209pub enum FinalReceiveOperationState {
210 Expired,
212 Claimed,
214 Failure,
216}
217
218pub type ReceiveResult = Result<(Bolt11Invoice, OperationId), ReceiveError>;
219
220#[derive(Clone)]
221pub struct LightningClientInit {
222 pub gateway_conn: Option<Arc<dyn GatewayConnection + Send + Sync>>,
223 pub custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
224}
225
226impl std::fmt::Debug for LightningClientInit {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 f.debug_struct("LightningClientInit")
229 .field("gateway_conn", &self.gateway_conn)
230 .field("custom_meta_fn", &"<function>")
231 .finish()
232 }
233}
234
235impl Default for LightningClientInit {
236 fn default() -> Self {
237 LightningClientInit {
238 gateway_conn: None,
239 custom_meta_fn: Arc::new(|| Value::Null),
240 }
241 }
242}
243
244impl ModuleInit for LightningClientInit {
245 type Common = LightningCommonInit;
246
247 async fn dump_database(
248 &self,
249 _dbtx: &mut DatabaseTransaction<'_>,
250 _prefix_names: Vec<String>,
251 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
252 Box::new(BTreeMap::new().into_iter())
253 }
254}
255
256#[apply(async_trait_maybe_send!)]
257impl ClientModuleInit for LightningClientInit {
258 type Module = LightningClientModule;
259
260 fn supported_api_versions(&self) -> MultiApiVersion {
261 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
262 .expect("no version conflicts")
263 }
264
265 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
266 let gateway_conn = if let Some(gateway_conn) = self.gateway_conn.clone() {
267 gateway_conn
268 } else {
269 let api = GatewayApi::new(None, args.connector_registry.clone());
270 Arc::new(RealGatewayConnection { api })
271 };
272 Ok(LightningClientModule::new(
273 *args.federation_id(),
274 args.cfg().clone(),
275 args.notifier().clone(),
276 args.context(),
277 args.module_api().clone(),
278 args.module_root_secret(),
279 gateway_conn,
280 self.custom_meta_fn.clone(),
281 args.admin_auth().cloned(),
282 args.task_group(),
283 args.client_span(),
284 ))
285 }
286
287 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
288 Some(
289 DbKeyPrefix::iter()
290 .map(|p| p as u8)
291 .chain(
292 DbKeyPrefix::ExternalReservedStart as u8
293 ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
294 )
295 .collect(),
296 )
297 }
298}
299
300#[derive(Debug, Clone)]
301pub struct LightningClientContext {
302 federation_id: FederationId,
303 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
304 pub(crate) client_ctx: ClientContext<LightningClientModule>,
305}
306
307impl Context for LightningClientContext {
308 const KIND: Option<ModuleKind> = Some(KIND);
309}
310
311#[derive(Debug, Clone)]
312pub struct LightningClientModule {
313 federation_id: FederationId,
314 cfg: LightningClientConfig,
315 notifier: ModuleNotifier<LightningClientStateMachines>,
316 client_ctx: ClientContext<Self>,
317 module_api: DynModuleApi,
318 keypair: Keypair,
319 lnurl_keypair: Keypair,
320 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
321 #[allow(unused)] admin_auth: Option<ApiAuth>,
323}
324
325#[apply(async_trait_maybe_send!)]
326impl ClientModule for LightningClientModule {
327 type Init = LightningClientInit;
328 type Common = LightningModuleTypes;
329 type Backup = NoModuleBackup;
330 type ModuleStateMachineContext = LightningClientContext;
331 type States = LightningClientStateMachines;
332
333 fn context(&self) -> Self::ModuleStateMachineContext {
334 LightningClientContext {
335 federation_id: self.federation_id,
336 gateway_conn: self.gateway_conn.clone(),
337 client_ctx: self.client_ctx.clone(),
338 }
339 }
340
341 fn input_fee(
342 &self,
343 amounts: &Amounts,
344 _input: &<Self::Common as ModuleCommon>::Input,
345 ) -> Option<Amounts> {
346 Some(Amounts::new_bitcoin(
347 self.cfg.fee_consensus.fee(amounts.expect_only_bitcoin()),
348 ))
349 }
350
351 fn output_fee(
352 &self,
353 amounts: &Amounts,
354 _output: &<Self::Common as ModuleCommon>::Output,
355 ) -> Option<Amounts> {
356 Some(Amounts::new_bitcoin(
357 self.cfg.fee_consensus.fee(amounts.expect_only_bitcoin()),
358 ))
359 }
360
361 #[cfg(feature = "cli")]
362 async fn handle_cli_command(
363 &self,
364 args: &[std::ffi::OsString],
365 ) -> anyhow::Result<serde_json::Value> {
366 cli::handle_cli_command(self, args).await
367 }
368}
369
370impl LightningClientModule {
371 #[allow(clippy::too_many_arguments)]
372 fn new(
373 federation_id: FederationId,
374 cfg: LightningClientConfig,
375 notifier: ModuleNotifier<LightningClientStateMachines>,
376 client_ctx: ClientContext<Self>,
377 module_api: DynModuleApi,
378 module_root_secret: &DerivableSecret,
379 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
380 custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
381 admin_auth: Option<ApiAuth>,
382 task_group: &TaskGroup,
383 client_span: &tracing::Span,
384 ) -> Self {
385 let module = Self {
386 federation_id,
387 cfg,
388 notifier,
389 client_ctx,
390 module_api,
391 keypair: module_root_secret
392 .child_key(ChildId(0))
393 .to_secp_key(SECP256K1),
394 lnurl_keypair: module_root_secret
395 .child_key(ChildId(1))
396 .to_secp_key(SECP256K1),
397 gateway_conn,
398 admin_auth,
399 };
400
401 module.spawn_receive_lnurl_task(custom_meta_fn, task_group, client_span);
402
403 module.spawn_gateway_map_update_task(task_group, client_span);
404
405 module
406 }
407
408 fn spawn_gateway_map_update_task(&self, task_group: &TaskGroup, client_span: &tracing::Span) {
409 let module = self.clone();
410 let api = self.module_api.clone();
411
412 task_group.spawn_cancellable_with_span(
413 client_span.clone(),
414 "gateway_map_update_task",
415 async move {
416 api.wait_for_initialized_connections().await;
417 module.update_gateway_map().await;
418 },
419 );
420 }
421
422 async fn update_gateway_map(&self) {
423 if let Ok(gateways) = self.module_api.gateways().await {
430 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
431
432 for gateway in gateways {
433 if let Ok(Some(routing_info)) = self
434 .gateway_conn
435 .routing_info(gateway.clone(), &self.federation_id)
436 .await
437 {
438 dbtx.insert_entry(&GatewayKey(routing_info.lightning_public_key), &gateway)
439 .await;
440 }
441 }
442
443 if let Err(e) = dbtx.commit_tx_result().await {
444 warn!("Failed to commit the updated gateway mapping to the database: {e}");
445 }
446 }
447 }
448
449 pub async fn select_gateway(
454 &self,
455 invoice: Option<Bolt11Invoice>,
456 ) -> Result<(SafeUrl, RoutingInfo), SelectGatewayError> {
457 let gateways = self
458 .module_api
459 .gateways()
460 .await
461 .map_err(|e| SelectGatewayError::FailedToRequestGateways(e.to_string()))?;
462
463 if gateways.is_empty() {
464 return Err(SelectGatewayError::NoGatewaysAvailable);
465 }
466
467 if let Some(invoice) = invoice
468 && let Some(gateway) = self
469 .client_ctx
470 .module_db()
471 .begin_transaction_nc()
472 .await
473 .get_value(&GatewayKey(invoice.recover_payee_pub_key()))
474 .await
475 .filter(|gateway| gateways.contains(gateway))
476 && let Ok(Some(routing_info)) = self.routing_info(&gateway).await
477 {
478 return Ok((gateway, routing_info));
479 }
480
481 for gateway in gateways {
482 if let Ok(Some(routing_info)) = self.routing_info(&gateway).await {
483 return Ok((gateway, routing_info));
484 }
485 }
486
487 Err(SelectGatewayError::GatewaysUnresponsive)
488 }
489
490 pub async fn list_gateways(
493 &self,
494 peer: Option<PeerId>,
495 ) -> Result<Vec<SafeUrl>, ListGatewaysError> {
496 if let Some(peer) = peer {
497 self.module_api
498 .gateways_from_peer(peer)
499 .await
500 .map_err(|_| ListGatewaysError::FailedToListGateways)
501 } else {
502 self.module_api
503 .gateways()
504 .await
505 .map_err(|_| ListGatewaysError::FailedToListGateways)
506 }
507 }
508
509 pub async fn routing_info(
512 &self,
513 gateway: &SafeUrl,
514 ) -> Result<Option<RoutingInfo>, RoutingInfoError> {
515 self.gateway_conn
516 .routing_info(gateway.clone(), &self.federation_id)
517 .await
518 .map_err(|_| RoutingInfoError::FailedToRequestRoutingInfo)
519 }
520
521 #[allow(clippy::too_many_lines)]
538 pub async fn send(
539 &self,
540 invoice: Bolt11Invoice,
541 gateway: Option<SafeUrl>,
542 custom_meta: Value,
543 ) -> Result<OperationId, SendPaymentError> {
544 let amount = invoice
545 .amount_milli_satoshis()
546 .ok_or(SendPaymentError::InvoiceMissingAmount)?;
547
548 if invoice.is_expired() {
549 return Err(SendPaymentError::InvoiceExpired);
550 }
551
552 if self.cfg.network != invoice.currency().into() {
553 return Err(SendPaymentError::WrongCurrency {
554 invoice_currency: invoice.currency(),
555 federation_currency: self.cfg.network.into(),
556 });
557 }
558
559 let operation_id = self.get_next_operation_id(&invoice).await?;
560
561 let (ephemeral_tweak, ephemeral_pk) = tweak::generate(self.keypair.public_key());
562
563 let refund_keypair = SecretKey::from_slice(&ephemeral_tweak)
564 .expect("32 bytes, within curve order")
565 .keypair(secp256k1::SECP256K1);
566
567 let (gateway_api, routing_info) = match gateway {
568 Some(gateway_api) => (
569 gateway_api.clone(),
570 self.routing_info(&gateway_api)
571 .await
572 .map_err(|e| SendPaymentError::FailedToConnectToGateway(e.to_string()))?
573 .ok_or(SendPaymentError::FederationNotSupported)?,
574 ),
575 None => self
576 .select_gateway(Some(invoice.clone()))
577 .await
578 .map_err(SendPaymentError::SelectGateway)?,
579 };
580
581 let (send_fee, expiration_delta) = routing_info.send_parameters(&invoice);
582
583 if !send_fee.le(&PaymentFee::SEND_FEE_LIMIT) {
584 return Err(SendPaymentError::GatewayFeeExceedsLimit);
585 }
586
587 if EXPIRATION_DELTA_LIMIT < expiration_delta {
588 return Err(SendPaymentError::GatewayExpirationExceedsLimit);
589 }
590
591 let consensus_block_count = self
592 .module_api
593 .consensus_block_count()
594 .await
595 .map_err(|e| SendPaymentError::FailedToRequestBlockCount(e.to_string()))?;
596
597 let contract = OutgoingContract {
598 payment_image: PaymentImage::Hash(*invoice.payment_hash()),
599 amount: send_fee.add_to(amount),
600 expiration: consensus_block_count + expiration_delta + CONTRACT_CONFIRMATION_BUFFER,
601 claim_pk: routing_info.module_public_key,
602 refund_pk: refund_keypair.public_key(),
603 ephemeral_pk,
604 };
605
606 let contract_clone = contract.clone();
607 let gateway_api_clone = gateway_api.clone();
608 let invoice_clone = invoice.clone();
609
610 let client_output = ClientOutput::<LightningOutput> {
611 output: LightningOutput::V0(LightningOutputV0::Outgoing(contract.clone())),
612 amounts: Amounts::new_bitcoin(contract.amount),
613 };
614
615 let client_output_sm = ClientOutputSM::<LightningClientStateMachines> {
616 state_machines: Arc::new(move |range: OutPointRange| {
617 vec![LightningClientStateMachines::Send(SendStateMachine {
618 common: SendSMCommon {
619 operation_id,
620 outpoint: range.into_iter().next().unwrap(),
621 contract: contract_clone.clone(),
622 gateway_api: Some(gateway_api_clone.clone()),
623 invoice: Some(LightningInvoice::Bolt11(invoice_clone.clone())),
624 refund_keypair,
625 },
626 state: SendSMState::Funding,
627 })]
628 }),
629 };
630
631 let client_output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
632 vec![client_output],
633 vec![client_output_sm],
634 ));
635
636 let transaction = TransactionBuilder::new().with_outputs(client_output);
637
638 self.client_ctx
639 .finalize_and_submit_transaction(
640 operation_id,
641 LightningCommonInit::KIND.as_str(),
642 move |change_outpoint_range| {
643 LightningOperationMeta::Send(SendOperationMeta {
644 change_outpoint_range,
645 gateway: gateway_api.clone(),
646 contract: contract.clone(),
647 invoice: LightningInvoice::Bolt11(invoice.clone()),
648 custom_meta: custom_meta.clone(),
649 })
650 },
651 transaction,
652 )
653 .await
654 .map_err(|e| SendPaymentError::FailedToFundPayment(e.to_string()))?;
655
656 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
657
658 self.client_ctx
659 .log_event(
660 &mut dbtx,
661 SendPaymentEvent {
662 operation_id,
663 amount: Amount::from_msats(amount),
664 fee: send_fee.fee(amount),
665 },
666 )
667 .await;
668
669 dbtx.commit_tx().await;
670
671 Ok(operation_id)
672 }
673
674 async fn get_next_operation_id(
675 &self,
676 invoice: &Bolt11Invoice,
677 ) -> Result<OperationId, SendPaymentError> {
678 for payment_attempt in 0..u64::MAX {
679 let operation_id = OperationId::from_encodable(&(invoice.clone(), payment_attempt));
680
681 if !self.client_ctx.operation_exists(operation_id).await {
682 return Ok(operation_id);
683 }
684
685 if self.client_ctx.has_active_states(operation_id).await {
686 return Err(SendPaymentError::PaymentInProgress(operation_id));
687 }
688
689 let mut stream = self
690 .subscribe_send_operation_state_updates(operation_id)
691 .await
692 .expect("operation_id exists")
693 .into_stream();
694
695 while let Some(state) = stream.next().await {
698 if let SendOperationState::Success(_) = state {
699 return Err(SendPaymentError::InvoiceAlreadyPaid(operation_id));
700 }
701 }
702 }
703
704 panic!("We could not find an unused operation id for sending a lightning payment");
705 }
706
707 pub async fn subscribe_send_operation_state_updates(
709 &self,
710 operation_id: OperationId,
711 ) -> anyhow::Result<UpdateStreamOrOutcome<SendOperationState>> {
712 let operation = self.client_ctx.get_operation(operation_id).await?;
713 let mut stream = self.notifier.subscribe(operation_id).await;
714 let client_ctx = self.client_ctx.clone();
715 let module_api = self.module_api.clone();
716
717 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
718 stream! {
719 loop {
720 if let Some(LightningClientStateMachines::Send(state)) = stream.next().await {
721 match state.state {
722 SendSMState::Funding => yield SendOperationState::Funding,
723 SendSMState::Funded => yield SendOperationState::Funded,
724 SendSMState::Success(preimage) => {
725 assert!(state.common.contract.verify_preimage(&preimage));
727
728 yield SendOperationState::Success(preimage);
729 return;
730 },
731 SendSMState::Refunding(out_points) => {
732 yield SendOperationState::Refunding;
733
734 if client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await.is_ok() {
735 yield SendOperationState::Refunded;
736 return;
737 }
738
739 if let Some(preimage) = module_api.await_preimage(
743 state.common.outpoint,
744 0
745 ).await
746 && state.common.contract.verify_preimage(&preimage) {
747 yield SendOperationState::Success(preimage);
748 return;
749 }
750
751 yield SendOperationState::Failure;
752 return;
753 },
754 SendSMState::Rejected(..) => {
755 yield SendOperationState::Failure;
756 return;
757 },
758 }
759 }
760 }
761 }
762 }))
763 }
764
765 pub async fn await_final_send_operation_state(
767 &self,
768 operation_id: OperationId,
769 ) -> anyhow::Result<FinalSendOperationState> {
770 let mut stream = self
771 .subscribe_send_operation_state_updates(operation_id)
772 .await?
773 .into_stream();
774
775 let mut final_state = None;
776
777 while let Some(state) = stream.next().await {
778 match state {
779 SendOperationState::Success(preimage) => {
780 final_state = Some(FinalSendOperationState::Success(preimage));
781 }
782 SendOperationState::Refunded => {
783 final_state = Some(FinalSendOperationState::Refunded);
784 }
785 SendOperationState::Failure => final_state = Some(FinalSendOperationState::Failure),
786 _ => {}
787 }
788 }
789
790 Ok(final_state.expect("Stream contains one final state"))
791 }
792
793 pub async fn receive(
805 &self,
806 amount: Amount,
807 expiry_secs: u32,
808 description: Bolt11InvoiceDescription,
809 gateway: Option<SafeUrl>,
810 custom_meta: Value,
811 ) -> Result<(Bolt11Invoice, OperationId), ReceiveError> {
812 let (gateway, contract, invoice) = self
813 .create_contract_and_fetch_invoice(
814 self.keypair.public_key(),
815 amount,
816 expiry_secs,
817 description,
818 gateway,
819 )
820 .await?;
821
822 let operation_id = self
823 .receive_incoming_contract(
824 self.keypair.secret_key(),
825 contract.clone(),
826 LightningOperationMeta::Receive(ReceiveOperationMeta {
827 gateway,
828 contract,
829 invoice: LightningInvoice::Bolt11(invoice.clone()),
830 custom_meta,
831 }),
832 )
833 .await
834 .expect("The contract has been generated with our public key");
835
836 Ok((invoice, operation_id))
837 }
838
839 pub async fn receive_fee_quote(&self, amount: Amount) -> anyhow::Result<FeeQuote> {
854 self.client_ctx
855 .fee_quote(
856 OperationId::new_random(),
857 FeeQuoteRequest {
858 input_amount: Amounts::new_bitcoin(amount),
859 output_amount: Amounts::ZERO,
860 input_fee: Amounts::new_bitcoin(self.cfg.fee_consensus.fee(amount)),
861 output_fee: Amounts::ZERO,
862 },
863 )
864 .await
865 }
866
867 pub async fn send_fee_quote(&self, amount: Amount) -> anyhow::Result<FeeQuote> {
883 self.client_ctx
884 .fee_quote(
885 OperationId::new_random(),
886 FeeQuoteRequest {
887 input_amount: Amounts::ZERO,
888 output_amount: Amounts::new_bitcoin(amount),
889 input_fee: Amounts::ZERO,
890 output_fee: Amounts::new_bitcoin(self.cfg.fee_consensus.fee(amount)),
891 },
892 )
893 .await
894 }
895
896 async fn create_contract_and_fetch_invoice(
900 &self,
901 recipient_static_pk: PublicKey,
902 amount: Amount,
903 expiry_secs: u32,
904 description: Bolt11InvoiceDescription,
905 gateway: Option<SafeUrl>,
906 ) -> Result<(SafeUrl, IncomingContract, Bolt11Invoice), ReceiveError> {
907 let (ephemeral_tweak, ephemeral_pk) = tweak::generate(recipient_static_pk);
908
909 let encryption_seed = ephemeral_tweak
910 .consensus_hash::<sha256::Hash>()
911 .to_byte_array();
912
913 let preimage = encryption_seed
914 .consensus_hash::<sha256::Hash>()
915 .to_byte_array();
916
917 let (gateway, routing_info) = match gateway {
918 Some(gateway) => (
919 gateway.clone(),
920 self.routing_info(&gateway)
921 .await
922 .map_err(|e| ReceiveError::FailedToConnectToGateway(e.to_string()))?
923 .ok_or(ReceiveError::FederationNotSupported)?,
924 ),
925 None => self
926 .select_gateway(None)
927 .await
928 .map_err(ReceiveError::SelectGateway)?,
929 };
930
931 if !routing_info.receive_fee.le(&PaymentFee::RECEIVE_FEE_LIMIT) {
932 return Err(ReceiveError::GatewayFeeExceedsLimit);
933 }
934
935 let contract_amount = routing_info.receive_fee.subtract_from(amount.msats);
936
937 if contract_amount < MINIMUM_INCOMING_CONTRACT_AMOUNT {
938 return Err(ReceiveError::AmountTooSmall);
939 }
940
941 let expiration = duration_since_epoch()
942 .as_secs()
943 .saturating_add(u64::from(expiry_secs));
944
945 let claim_pk = recipient_static_pk
946 .mul_tweak(
947 secp256k1::SECP256K1,
948 &Scalar::from_be_bytes(ephemeral_tweak).expect("Within curve order"),
949 )
950 .expect("Tweak is valid");
951
952 let contract = IncomingContract::new(
953 self.cfg.tpe_agg_pk,
954 encryption_seed,
955 preimage,
956 PaymentImage::Hash(preimage.consensus_hash()),
957 contract_amount,
958 expiration,
959 claim_pk,
960 routing_info.module_public_key,
961 ephemeral_pk,
962 );
963
964 let invoice = self
965 .gateway_conn
966 .bolt11_invoice(
967 gateway.clone(),
968 self.federation_id,
969 contract.clone(),
970 amount,
971 description,
972 expiry_secs,
973 )
974 .await
975 .map_err(|e| ReceiveError::FailedToConnectToGateway(e.to_string()))?;
976
977 if invoice.payment_hash() != &preimage.consensus_hash() {
978 return Err(ReceiveError::InvalidInvoice);
979 }
980
981 if invoice.amount_milli_satoshis() != Some(amount.msats) {
982 return Err(ReceiveError::IncorrectInvoiceAmount);
983 }
984
985 Ok((gateway, contract, invoice))
986 }
987
988 async fn receive_incoming_contract(
991 &self,
992 sk: SecretKey,
993 contract: IncomingContract,
994 operation_meta: LightningOperationMeta,
995 ) -> Option<OperationId> {
996 let operation_id = OperationId::from_encodable(&contract.clone());
997
998 let (claim_keypair, agg_decryption_key) = self.recover_contract_keys(sk, &contract)?;
999
1000 let receive_sm = LightningClientStateMachines::Receive(ReceiveStateMachine {
1001 common: ReceiveSMCommon {
1002 operation_id,
1003 contract: contract.clone(),
1004 claim_keypair,
1005 agg_decryption_key,
1006 },
1007 state: ReceiveSMState::Pending,
1008 });
1009
1010 self.client_ctx
1013 .manual_operation_start(
1014 operation_id,
1015 LightningCommonInit::KIND.as_str(),
1016 operation_meta,
1017 vec![self.client_ctx.make_dyn_state(receive_sm)],
1018 )
1019 .await
1020 .ok();
1021
1022 Some(operation_id)
1023 }
1024
1025 fn recover_contract_keys(
1026 &self,
1027 sk: SecretKey,
1028 contract: &IncomingContract,
1029 ) -> Option<(Keypair, AggregateDecryptionKey)> {
1030 let tweak = ecdh::SharedSecret::new(&contract.commitment.ephemeral_pk, &sk);
1031
1032 let encryption_seed = tweak
1033 .secret_bytes()
1034 .consensus_hash::<sha256::Hash>()
1035 .to_byte_array();
1036
1037 let claim_keypair = sk
1038 .mul_tweak(&Scalar::from_be_bytes(tweak.secret_bytes()).expect("Within curve order"))
1039 .expect("Tweak is valid")
1040 .keypair(secp256k1::SECP256K1);
1041
1042 if claim_keypair.public_key() != contract.commitment.claim_pk {
1043 return None; }
1045
1046 let agg_decryption_key = derive_agg_dk(&self.cfg.tpe_agg_pk, &encryption_seed);
1047
1048 if !contract.verify_agg_decryption_key(&self.cfg.tpe_agg_pk, &agg_decryption_key) {
1049 return None; }
1051
1052 contract.decrypt_preimage(&agg_decryption_key)?;
1053
1054 Some((claim_keypair, agg_decryption_key))
1055 }
1056
1057 pub async fn subscribe_receive_operation_state_updates(
1059 &self,
1060 operation_id: OperationId,
1061 ) -> anyhow::Result<UpdateStreamOrOutcome<ReceiveOperationState>> {
1062 let operation = self.client_ctx.get_operation(operation_id).await?;
1063 let mut stream = self.notifier.subscribe(operation_id).await;
1064 let client_ctx = self.client_ctx.clone();
1065
1066 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
1067 stream! {
1068 loop {
1069 if let Some(LightningClientStateMachines::Receive(state)) = stream.next().await {
1070 match state.state {
1071 ReceiveSMState::Pending => yield ReceiveOperationState::Pending,
1072 ReceiveSMState::Claiming(out_points) => {
1073 yield ReceiveOperationState::Claiming;
1074
1075 if client_ctx.await_primary_module_outputs(operation_id, out_points).await.is_ok() {
1076 yield ReceiveOperationState::Claimed;
1077 } else {
1078 yield ReceiveOperationState::Failure;
1079 }
1080 return;
1081 },
1082 ReceiveSMState::Expired => {
1083 yield ReceiveOperationState::Expired;
1084 return;
1085 }
1086 }
1087 }
1088 }
1089 }
1090 }))
1091 }
1092
1093 pub async fn await_final_receive_operation_state(
1095 &self,
1096 operation_id: OperationId,
1097 ) -> anyhow::Result<FinalReceiveOperationState> {
1098 let mut stream = self
1099 .subscribe_receive_operation_state_updates(operation_id)
1100 .await?
1101 .into_stream();
1102
1103 let mut final_state = None;
1104
1105 while let Some(state) = stream.next().await {
1106 match state {
1107 ReceiveOperationState::Expired => {
1108 final_state = Some(FinalReceiveOperationState::Expired);
1109 }
1110 ReceiveOperationState::Claimed => {
1111 final_state = Some(FinalReceiveOperationState::Claimed);
1112 }
1113 ReceiveOperationState::Failure => {
1114 final_state = Some(FinalReceiveOperationState::Failure);
1115 }
1116 _ => {}
1117 }
1118 }
1119
1120 Ok(final_state.expect("Stream contains one final state"))
1121 }
1122
1123 pub async fn generate_lnurl(
1126 &self,
1127 recurringd: SafeUrl,
1128 gateway: Option<SafeUrl>,
1129 ) -> Result<String, GenerateLnurlError> {
1130 let gateways = if let Some(gateway) = gateway {
1131 vec![gateway]
1132 } else {
1133 let gateways = self
1134 .module_api
1135 .gateways()
1136 .await
1137 .map_err(|e| GenerateLnurlError::FailedToRequestGateways(e.to_string()))?;
1138
1139 if gateways.is_empty() {
1140 return Err(GenerateLnurlError::NoGatewaysAvailable);
1141 }
1142
1143 gateways
1144 };
1145
1146 let payload = fedimint_core::base32::encode_prefixed(
1147 fedimint_core::base32::FEDIMINT_PREFIX,
1148 &lnurl::LnurlRequest {
1149 federation_id: self.federation_id,
1150 recipient_pk: self.lnurl_keypair.public_key(),
1151 aggregate_pk: self.cfg.tpe_agg_pk,
1152 gateways,
1153 },
1154 );
1155
1156 Ok(fedimint_lnurl::encode_lnurl(&format!(
1157 "{recurringd}pay/{payload}"
1158 )))
1159 }
1160
1161 fn spawn_receive_lnurl_task(
1162 &self,
1163 custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
1164 task_group: &TaskGroup,
1165 client_span: &tracing::Span,
1166 ) {
1167 let module = self.clone();
1168 let api = self.module_api.clone();
1169
1170 task_group.spawn_cancellable_with_span(
1171 client_span.clone(),
1172 "receive_lnurl_task",
1173 async move {
1174 api.wait_for_initialized_connections().await;
1175 loop {
1176 module.receive_lnurl(custom_meta_fn()).await;
1177 }
1178 },
1179 );
1180 }
1181
1182 async fn receive_lnurl(&self, custom_meta: Value) {
1183 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1184
1185 let stream_index = dbtx
1186 .get_value(&IncomingContractStreamIndexKey)
1187 .await
1188 .unwrap_or(0);
1189
1190 let (contracts, next_index) = self
1191 .module_api
1192 .await_incoming_contracts(stream_index, 128)
1193 .await;
1194
1195 for contract in &contracts {
1196 if let Some(operation_id) = self
1197 .receive_incoming_contract(
1198 self.lnurl_keypair.secret_key(),
1199 contract.clone(),
1200 LightningOperationMeta::LnurlReceive(LnurlReceiveOperationMeta {
1201 contract: contract.clone(),
1202 custom_meta: custom_meta.clone(),
1203 }),
1204 )
1205 .await
1206 {
1207 self.await_final_receive_operation_state(operation_id)
1208 .await
1209 .ok();
1210 }
1211 }
1212
1213 dbtx.insert_entry(&IncomingContractStreamIndexKey, &next_index)
1214 .await;
1215
1216 dbtx.commit_tx().await;
1217 }
1218}
1219
1220#[derive(Error, Debug, Clone, Eq, PartialEq)]
1221pub enum SelectGatewayError {
1222 #[error("Failed to request gateways")]
1223 FailedToRequestGateways(String),
1224 #[error("No gateways are available")]
1225 NoGatewaysAvailable,
1226 #[error("All gateways failed to respond")]
1227 GatewaysUnresponsive,
1228}
1229
1230#[derive(Error, Debug, Clone, Eq, PartialEq)]
1231pub enum SendPaymentError {
1232 #[error("Invoice is missing an amount")]
1233 InvoiceMissingAmount,
1234 #[error("Invoice has expired")]
1235 InvoiceExpired,
1236 #[error("A payment for this invoice is already in progress")]
1237 PaymentInProgress(OperationId),
1238 #[error("This invoice has already been paid")]
1239 InvoiceAlreadyPaid(OperationId),
1240 #[error(transparent)]
1241 SelectGateway(SelectGatewayError),
1242 #[error("Failed to connect to gateway")]
1243 FailedToConnectToGateway(String),
1244 #[error("Gateway does not support this federation")]
1245 FederationNotSupported,
1246 #[error("Gateway fee exceeds the allowed limit")]
1247 GatewayFeeExceedsLimit,
1248 #[error("Gateway expiration time exceeds the allowed limit")]
1249 GatewayExpirationExceedsLimit,
1250 #[error("Failed to request block count")]
1251 FailedToRequestBlockCount(String),
1252 #[error("Failed to fund the payment")]
1253 FailedToFundPayment(String),
1254 #[error("Invoice is for a different currency")]
1255 WrongCurrency {
1256 invoice_currency: Currency,
1257 federation_currency: Currency,
1258 },
1259}
1260
1261#[derive(Error, Debug, Clone, Eq, PartialEq)]
1262pub enum ReceiveError {
1263 #[error(transparent)]
1264 SelectGateway(SelectGatewayError),
1265 #[error("Failed to connect to gateway")]
1266 FailedToConnectToGateway(String),
1267 #[error("Gateway does not support this federation")]
1268 FederationNotSupported,
1269 #[error("Gateway fee exceeds the allowed limit")]
1270 GatewayFeeExceedsLimit,
1271 #[error("Amount is too small to cover fees")]
1272 AmountTooSmall,
1273 #[error("Gateway returned an invalid invoice")]
1274 InvalidInvoice,
1275 #[error("Gateway returned an invoice with incorrect amount")]
1276 IncorrectInvoiceAmount,
1277}
1278
1279#[derive(Error, Debug, Clone, Eq, PartialEq)]
1280pub enum GenerateLnurlError {
1281 #[error("No gateways are available")]
1282 NoGatewaysAvailable,
1283 #[error("Failed to request gateways")]
1284 FailedToRequestGateways(String),
1285}
1286
1287#[derive(Error, Debug, Clone, Eq, PartialEq)]
1288pub enum ListGatewaysError {
1289 #[error("Failed to request gateways")]
1290 FailedToListGateways,
1291}
1292
1293#[derive(Error, Debug, Clone, Eq, PartialEq)]
1294pub enum RoutingInfoError {
1295 #[error("Failed to request routing info")]
1296 FailedToRequestRoutingInfo,
1297}
1298
1299#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1300pub enum LightningClientStateMachines {
1301 Send(SendStateMachine),
1302 Receive(ReceiveStateMachine),
1303}
1304
1305impl IntoDynInstance for LightningClientStateMachines {
1306 type DynType = DynState;
1307
1308 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1309 DynState::from_typed(instance_id, self)
1310 }
1311}
1312
1313impl State for LightningClientStateMachines {
1314 type ModuleContext = LightningClientContext;
1315
1316 fn transitions(
1317 &self,
1318 context: &Self::ModuleContext,
1319 global_context: &DynGlobalClientContext,
1320 ) -> Vec<StateTransition<Self>> {
1321 match self {
1322 LightningClientStateMachines::Send(state) => {
1323 sm_enum_variant_translation!(
1324 state.transitions(context, global_context),
1325 LightningClientStateMachines::Send
1326 )
1327 }
1328 LightningClientStateMachines::Receive(state) => {
1329 sm_enum_variant_translation!(
1330 state.transitions(context, global_context),
1331 LightningClientStateMachines::Receive
1332 )
1333 }
1334 }
1335 }
1336
1337 fn operation_id(&self) -> OperationId {
1338 match self {
1339 LightningClientStateMachines::Send(state) => state.operation_id(),
1340 LightningClientStateMachines::Receive(state) => state.operation_id(),
1341 }
1342 }
1343}