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;
16use std::time::Duration;
17
18use async_stream::stream;
19use bitcoin::hashes::{Hash, sha256};
20use bitcoin::secp256k1;
21use db::{DbKeyPrefix, GatewayKey, IncomingContractStreamIndexKey};
22use fedimint_api_client::api::DynModuleApi;
23use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
24use fedimint_client_module::module::recovery::NoModuleBackup;
25use fedimint_client_module::module::{ClientContext, ClientModule, OutPointRange};
26use fedimint_client_module::oplog::UpdateStreamOrOutcome;
27use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
28use fedimint_client_module::transaction::{
29 ClientOutput, ClientOutputBundle, ClientOutputSM, TransactionBuilder,
30};
31use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
32use fedimint_core::config::FederationId;
33use fedimint_core::core::{IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
34use fedimint_core::db::{DatabaseTransaction, IDatabaseTransactionOpsCoreTyped};
35use fedimint_core::encoding::{Decodable, Encodable};
36use fedimint_core::module::{
37 ApiAuth, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
38};
39use fedimint_core::secp256k1::SECP256K1;
40use fedimint_core::task::TaskGroup;
41use fedimint_core::time::duration_since_epoch;
42use fedimint_core::util::SafeUrl;
43use fedimint_core::{Amount, apply, async_trait_maybe_send};
44use fedimint_derive_secret::{ChildId, DerivableSecret};
45use fedimint_lnv2_common::config::LightningClientConfig;
46use fedimint_lnv2_common::contracts::{IncomingContract, OutgoingContract, PaymentImage};
47use fedimint_lnv2_common::gateway_api::{
48 GatewayConnection, GatewayConnectionError, PaymentFee, RealGatewayConnection, RoutingInfo,
49};
50use fedimint_lnv2_common::{
51 Bolt11InvoiceDescription, KIND, LightningCommonInit, LightningInvoice, LightningModuleTypes,
52 LightningOutput, LightningOutputV0, lnurl, tweak,
53};
54use futures::StreamExt;
55use lightning_invoice::{Bolt11Invoice, Currency};
56use secp256k1::{Keypair, PublicKey, Scalar, SecretKey, ecdh};
57use serde::{Deserialize, Serialize};
58use serde_json::Value;
59use strum::IntoEnumIterator as _;
60use thiserror::Error;
61use tpe::{AggregateDecryptionKey, derive_agg_dk};
62use tracing::warn;
63
64use crate::api::LightningFederationApi;
65use crate::receive_sm::{ReceiveSMCommon, ReceiveSMState, ReceiveStateMachine};
66use crate::send_sm::{SendSMCommon, SendSMState, SendStateMachine};
67
68const EXPIRATION_DELTA_LIMIT: u64 = 1440;
71
72const CONTRACT_CONFIRMATION_BUFFER: u64 = 12;
74
75#[allow(clippy::large_enum_variant)]
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub enum LightningOperationMeta {
78 Send(SendOperationMeta),
79 Receive(ReceiveOperationMeta),
80 LnurlReceive(LnurlReceiveOperationMeta),
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct SendOperationMeta {
85 pub change_outpoint_range: OutPointRange,
86 pub gateway: SafeUrl,
87 pub contract: OutgoingContract,
88 pub invoice: LightningInvoice,
89 pub custom_meta: Value,
90}
91
92impl SendOperationMeta {
93 pub fn gateway_fee(&self) -> Amount {
95 match &self.invoice {
96 LightningInvoice::Bolt11(invoice) => self.contract.amount.saturating_sub(
97 Amount::from_msats(invoice.amount_milli_satoshis().expect("Invoice has amount")),
98 ),
99 }
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct ReceiveOperationMeta {
105 pub gateway: SafeUrl,
106 pub contract: IncomingContract,
107 pub invoice: LightningInvoice,
108 pub custom_meta: Value,
109}
110
111impl ReceiveOperationMeta {
112 pub fn gateway_fee(&self) -> Amount {
114 match &self.invoice {
115 LightningInvoice::Bolt11(invoice) => {
116 Amount::from_msats(invoice.amount_milli_satoshis().expect("Invoice has amount"))
117 .saturating_sub(self.contract.commitment.amount)
118 }
119 }
120 }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct LnurlReceiveOperationMeta {
125 pub contract: IncomingContract,
126 pub custom_meta: Value,
127}
128
129#[cfg_attr(doc, aquamarine::aquamarine)]
130#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
148pub enum SendOperationState {
149 Funding,
151 Funded,
153 Success([u8; 32]),
155 Refunding,
157 Refunded,
159 Failure,
161}
162
163#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
165pub enum FinalSendOperationState {
166 Success,
168 Refunded,
170 Failure,
172}
173
174pub type SendResult = Result<OperationId, SendPaymentError>;
175
176#[cfg_attr(doc, aquamarine::aquamarine)]
177#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
189pub enum ReceiveOperationState {
190 Pending,
192 Expired,
194 Claiming,
196 Claimed,
198 Failure,
200}
201
202#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
204pub enum FinalReceiveOperationState {
205 Expired,
207 Claimed,
209 Failure,
211}
212
213pub type ReceiveResult = Result<(Bolt11Invoice, OperationId), ReceiveError>;
214
215#[derive(Debug, Clone)]
216pub struct LightningClientInit {
217 pub gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
218}
219
220impl Default for LightningClientInit {
221 fn default() -> Self {
222 LightningClientInit {
223 gateway_conn: Arc::new(RealGatewayConnection),
224 }
225 }
226}
227
228impl ModuleInit for LightningClientInit {
229 type Common = LightningCommonInit;
230
231 async fn dump_database(
232 &self,
233 _dbtx: &mut DatabaseTransaction<'_>,
234 _prefix_names: Vec<String>,
235 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
236 Box::new(BTreeMap::new().into_iter())
237 }
238}
239
240#[apply(async_trait_maybe_send!)]
241impl ClientModuleInit for LightningClientInit {
242 type Module = LightningClientModule;
243
244 fn supported_api_versions(&self) -> MultiApiVersion {
245 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
246 .expect("no version conflicts")
247 }
248
249 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
250 Ok(LightningClientModule::new(
251 *args.federation_id(),
252 args.cfg().clone(),
253 args.notifier().clone(),
254 args.context(),
255 args.module_api().clone(),
256 args.module_root_secret(),
257 self.gateway_conn.clone(),
258 args.admin_auth().cloned(),
259 args.task_group(),
260 ))
261 }
262
263 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
264 Some(
265 DbKeyPrefix::iter()
266 .map(|p| p as u8)
267 .chain(
268 DbKeyPrefix::ExternalReservedStart as u8
269 ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
270 )
271 .collect(),
272 )
273 }
274}
275
276#[derive(Debug, Clone)]
277pub struct LightningClientContext {
278 federation_id: FederationId,
279 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
280}
281
282impl Context for LightningClientContext {
283 const KIND: Option<ModuleKind> = Some(KIND);
284}
285
286#[derive(Debug)]
287pub struct LightningClientModule {
288 federation_id: FederationId,
289 cfg: LightningClientConfig,
290 notifier: ModuleNotifier<LightningClientStateMachines>,
291 client_ctx: ClientContext<Self>,
292 module_api: DynModuleApi,
293 keypair: Keypair,
294 lnurl_keypair: Keypair,
295 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
296 #[allow(unused)] admin_auth: Option<ApiAuth>,
298}
299
300#[apply(async_trait_maybe_send!)]
301impl ClientModule for LightningClientModule {
302 type Init = LightningClientInit;
303 type Common = LightningModuleTypes;
304 type Backup = NoModuleBackup;
305 type ModuleStateMachineContext = LightningClientContext;
306 type States = LightningClientStateMachines;
307
308 fn context(&self) -> Self::ModuleStateMachineContext {
309 LightningClientContext {
310 federation_id: self.federation_id,
311 gateway_conn: self.gateway_conn.clone(),
312 }
313 }
314
315 fn input_fee(
316 &self,
317 amount: Amount,
318 _input: &<Self::Common as ModuleCommon>::Input,
319 ) -> Option<Amount> {
320 Some(self.cfg.fee_consensus.fee(amount))
321 }
322
323 fn output_fee(
324 &self,
325 amount: Amount,
326 _output: &<Self::Common as ModuleCommon>::Output,
327 ) -> Option<Amount> {
328 Some(self.cfg.fee_consensus.fee(amount))
329 }
330
331 #[cfg(feature = "cli")]
332 async fn handle_cli_command(
333 &self,
334 args: &[std::ffi::OsString],
335 ) -> anyhow::Result<serde_json::Value> {
336 cli::handle_cli_command(self, args).await
337 }
338}
339
340impl LightningClientModule {
341 #[allow(clippy::too_many_arguments)]
342 fn new(
343 federation_id: FederationId,
344 cfg: LightningClientConfig,
345 notifier: ModuleNotifier<LightningClientStateMachines>,
346 client_ctx: ClientContext<Self>,
347 module_api: DynModuleApi,
348 module_root_secret: &DerivableSecret,
349 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
350 admin_auth: Option<ApiAuth>,
351 task_group: &TaskGroup,
352 ) -> Self {
353 Self::spawn_gateway_map_update_task(
354 federation_id,
355 client_ctx.clone(),
356 module_api.clone(),
357 gateway_conn.clone(),
358 task_group,
359 );
360
361 Self {
362 federation_id,
363 cfg,
364 notifier,
365 client_ctx,
366 module_api,
367 keypair: module_root_secret
368 .child_key(ChildId(0))
369 .to_secp_key(SECP256K1),
370 lnurl_keypair: module_root_secret
371 .child_key(ChildId(1))
372 .to_secp_key(SECP256K1),
373 gateway_conn,
374 admin_auth,
375 }
376 }
377
378 fn spawn_gateway_map_update_task(
379 federation_id: FederationId,
380 client_ctx: ClientContext<Self>,
381 module_api: DynModuleApi,
382 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
383 task_group: &TaskGroup,
384 ) {
385 task_group.spawn_cancellable("gateway_map_update_task", async move {
386 let mut interval = tokio::time::interval(Duration::from_secs(24 * 60 * 60));
387
388 loop {
389 Self::update_gateway_map(&federation_id, &client_ctx, &module_api, &gateway_conn)
390 .await;
391 interval.tick().await;
392 }
393 });
394 }
395
396 async fn update_gateway_map(
397 federation_id: &FederationId,
398 client_ctx: &ClientContext<Self>,
399 module_api: &DynModuleApi,
400 gateway_conn: &Arc<dyn GatewayConnection + Send + Sync>,
401 ) {
402 if let Ok(gateways) = module_api.gateways().await {
409 let mut dbtx = client_ctx.module_db().begin_transaction().await;
410
411 for gateway in gateways {
412 if let Ok(Some(routing_info)) = gateway_conn
413 .routing_info(gateway.clone(), 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 amount: 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 pub async fn receive_lnurl(&self, batch_size: Option<usize>, custom_meta: Value) -> usize {
1032 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1033
1034 let stream_index = dbtx
1035 .get_value(&IncomingContractStreamIndexKey)
1036 .await
1037 .unwrap_or(0);
1038
1039 let (contracts, next_index) = self
1040 .module_api
1041 .await_incoming_contracts(stream_index, batch_size.unwrap_or(128))
1042 .await;
1043
1044 let mut received_contracts = 0;
1045
1046 for contract in &contracts {
1047 if let Some(operation_id) = self
1048 .receive_incoming_contract(
1049 self.lnurl_keypair.secret_key(),
1050 contract.clone(),
1051 LightningOperationMeta::LnurlReceive(LnurlReceiveOperationMeta {
1052 contract: contract.clone(),
1053 custom_meta: custom_meta.clone(),
1054 }),
1055 )
1056 .await
1057 {
1058 self.await_final_receive_operation_state(operation_id)
1059 .await
1060 .ok();
1061
1062 received_contracts += 1;
1063 }
1064 }
1065
1066 dbtx.insert_entry(&IncomingContractStreamIndexKey, &next_index)
1067 .await;
1068
1069 dbtx.commit_tx().await;
1070
1071 received_contracts
1072 }
1073}
1074
1075#[derive(Error, Debug, Clone, Eq, PartialEq)]
1076pub enum SelectGatewayError {
1077 #[error("Federation returned an error: {0}")]
1078 FederationError(String),
1079 #[error("The federation has no vetted gateways")]
1080 NoVettedGateways,
1081 #[error("All vetted gateways failed to respond on request of the routing info")]
1082 FailedToFetchRoutingInfo,
1083}
1084
1085#[derive(Error, Debug, Clone, Eq, PartialEq)]
1086pub enum SendPaymentError {
1087 #[error("The invoice has not amount")]
1088 InvoiceMissingAmount,
1089 #[error("The invoice has expired")]
1090 InvoiceExpired,
1091 #[error("A previous payment for the same invoice is still pending: {}", .0.fmt_full())]
1092 PendingPreviousPayment(OperationId),
1093 #[error("A previous payment for the same invoice was successful: {}", .0.fmt_full())]
1094 SuccessfulPreviousPayment(OperationId),
1095 #[error("Failed to select gateway: {0}")]
1096 FailedToSelectGateway(SelectGatewayError),
1097 #[error("Gateway connection error: {0}")]
1098 GatewayConnectionError(GatewayConnectionError),
1099 #[error("The gateway does not support our federation")]
1100 UnknownFederation,
1101 #[error("The gateways fee of exceeds the limit")]
1102 PaymentFeeExceedsLimit,
1103 #[error("The gateways expiration delta of exceeds the limit")]
1104 ExpirationDeltaExceedsLimit,
1105 #[error("Federation returned an error: {0}")]
1106 FederationError(String),
1107 #[error("We failed to finalize the funding transaction")]
1108 FinalizationError(String),
1109 #[error(
1110 "The invoice was for the wrong currency. Invoice currency={invoice_currency} Federation Currency={federation_currency}"
1111 )]
1112 WrongCurrency {
1113 invoice_currency: Currency,
1114 federation_currency: Currency,
1115 },
1116}
1117
1118#[derive(Error, Debug, Clone, Eq, PartialEq)]
1119pub enum ReceiveError {
1120 #[error("Failed to select gateway: {0}")]
1121 FailedToSelectGateway(SelectGatewayError),
1122 #[error("Gateway connection error: {0}")]
1123 GatewayConnectionError(GatewayConnectionError),
1124 #[error("The gateway does not support our federation")]
1125 UnknownFederation,
1126 #[error("The gateways fee exceeds the limit")]
1127 PaymentFeeExceedsLimit,
1128 #[error("The total fees required to complete this payment exceed its amount")]
1129 DustAmount,
1130 #[error("The invoice's payment hash is incorrect")]
1131 InvalidInvoicePaymentHash,
1132 #[error("The invoice's amount is incorrect")]
1133 InvalidInvoiceAmount,
1134}
1135
1136#[derive(Error, Debug, Clone, Eq, PartialEq)]
1137pub enum RegisterLnurlError {
1138 #[error("The federation has no vetted gateways")]
1139 NoVettedGateways,
1140 #[error("Federation returned an error: {0}")]
1141 FederationError(String),
1142 #[error("Failed to register lnurl: {0}")]
1143 RegistrationError(String),
1144}
1145
1146#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1147pub enum LightningClientStateMachines {
1148 Send(SendStateMachine),
1149 Receive(ReceiveStateMachine),
1150}
1151
1152impl IntoDynInstance for LightningClientStateMachines {
1153 type DynType = DynState;
1154
1155 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1156 DynState::from_typed(instance_id, self)
1157 }
1158}
1159
1160impl State for LightningClientStateMachines {
1161 type ModuleContext = LightningClientContext;
1162
1163 fn transitions(
1164 &self,
1165 context: &Self::ModuleContext,
1166 global_context: &DynGlobalClientContext,
1167 ) -> Vec<StateTransition<Self>> {
1168 match self {
1169 LightningClientStateMachines::Send(state) => {
1170 sm_enum_variant_translation!(
1171 state.transitions(context, global_context),
1172 LightningClientStateMachines::Send
1173 )
1174 }
1175 LightningClientStateMachines::Receive(state) => {
1176 sm_enum_variant_translation!(
1177 state.transitions(context, global_context),
1178 LightningClientStateMachines::Receive
1179 )
1180 }
1181 }
1182 }
1183
1184 fn operation_id(&self) -> OperationId {
1185 match self {
1186 LightningClientStateMachines::Send(state) => state.operation_id(),
1187 LightningClientStateMachines::Receive(state) => state.operation_id(),
1188 }
1189 }
1190}