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