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};
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::util::MapStateTransitions;
28use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
29use fedimint_client_module::transaction::{
30 ClientOutput, ClientOutputBundle, ClientOutputSM, TransactionBuilder,
31};
32use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
33use fedimint_core::config::FederationId;
34use fedimint_core::core::{IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
35use fedimint_core::db::{DatabaseTransaction, IDatabaseTransactionOpsCoreTyped};
36use fedimint_core::encoding::{Decodable, Encodable};
37use fedimint_core::module::{
38 ApiAuth, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
39};
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_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,
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#[derive(Debug, Clone, Serialize, Deserialize)]
75pub enum LightningOperationMeta {
76 Send(SendOperationMeta),
77 Receive(ReceiveOperationMeta),
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct SendOperationMeta {
82 pub change_outpoint_range: OutPointRange,
83 pub gateway: SafeUrl,
84 pub contract: OutgoingContract,
85 pub invoice: LightningInvoice,
86 pub custom_meta: Value,
87}
88
89impl SendOperationMeta {
90 pub fn gateway_fee(&self) -> Amount {
92 match &self.invoice {
93 LightningInvoice::Bolt11(invoice) => self.contract.amount.saturating_sub(
94 Amount::from_msats(invoice.amount_milli_satoshis().expect("Invoice has amount")),
95 ),
96 }
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct ReceiveOperationMeta {
102 pub gateway: SafeUrl,
103 pub contract: IncomingContract,
104 pub invoice: LightningInvoice,
105 pub custom_meta: Value,
106}
107
108impl ReceiveOperationMeta {
109 pub fn gateway_fee(&self) -> Amount {
111 match &self.invoice {
112 LightningInvoice::Bolt11(invoice) => {
113 Amount::from_msats(invoice.amount_milli_satoshis().expect("Invoice has amount"))
114 .saturating_sub(self.contract.commitment.amount)
115 }
116 }
117 }
118}
119
120#[cfg_attr(doc, aquamarine::aquamarine)]
121#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
139pub enum SendOperationState {
140 Funding,
142 Funded,
144 Success([u8; 32]),
146 Refunding,
148 Refunded,
150 Failure,
152}
153
154#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
156pub enum FinalSendOperationState {
157 Success,
159 Refunded,
161 Failure,
163}
164
165pub type SendResult = Result<OperationId, SendPaymentError>;
166
167#[cfg_attr(doc, aquamarine::aquamarine)]
168#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
180pub enum ReceiveOperationState {
181 Pending,
183 Expired,
185 Claiming,
187 Claimed,
189 Failure,
191}
192
193#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
195pub enum FinalReceiveOperationState {
196 Expired,
198 Claimed,
200 Failure,
202}
203
204pub type ReceiveResult = Result<(Bolt11Invoice, OperationId), ReceiveError>;
205
206#[derive(Debug, Clone)]
207pub struct LightningClientInit {
208 pub gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
209}
210
211impl Default for LightningClientInit {
212 fn default() -> Self {
213 LightningClientInit {
214 gateway_conn: Arc::new(RealGatewayConnection),
215 }
216 }
217}
218
219impl ModuleInit for LightningClientInit {
220 type Common = LightningCommonInit;
221
222 async fn dump_database(
223 &self,
224 _dbtx: &mut DatabaseTransaction<'_>,
225 _prefix_names: Vec<String>,
226 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
227 Box::new(BTreeMap::new().into_iter())
228 }
229}
230
231#[apply(async_trait_maybe_send!)]
232impl ClientModuleInit for LightningClientInit {
233 type Module = LightningClientModule;
234
235 fn supported_api_versions(&self) -> MultiApiVersion {
236 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
237 .expect("no version conflicts")
238 }
239
240 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
241 Ok(LightningClientModule::new(
242 *args.federation_id(),
243 args.cfg().clone(),
244 args.notifier().clone(),
245 args.context(),
246 args.module_api().clone(),
247 args.module_root_secret()
248 .clone()
249 .to_secp_key(fedimint_core::secp256k1::SECP256K1),
250 self.gateway_conn.clone(),
251 args.admin_auth().cloned(),
252 args.task_group(),
253 ))
254 }
255
256 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
257 Some(
258 DbKeyPrefix::iter()
259 .map(|p| p as u8)
260 .chain(
261 DbKeyPrefix::ExternalReservedStart as u8
262 ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
263 )
264 .collect(),
265 )
266 }
267}
268
269#[derive(Debug, Clone)]
270pub struct LightningClientContext {
271 federation_id: FederationId,
272 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
273}
274
275impl Context for LightningClientContext {
276 const KIND: Option<ModuleKind> = Some(KIND);
277}
278
279#[derive(Debug)]
280pub struct LightningClientModule {
281 federation_id: FederationId,
282 cfg: LightningClientConfig,
283 notifier: ModuleNotifier<LightningClientStateMachines>,
284 client_ctx: ClientContext<Self>,
285 module_api: DynModuleApi,
286 keypair: Keypair,
287 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
288 #[allow(unused)] admin_auth: Option<ApiAuth>,
290}
291
292#[apply(async_trait_maybe_send!)]
293impl ClientModule for LightningClientModule {
294 type Init = LightningClientInit;
295 type Common = LightningModuleTypes;
296 type Backup = NoModuleBackup;
297 type ModuleStateMachineContext = LightningClientContext;
298 type States = LightningClientStateMachines;
299
300 fn context(&self) -> Self::ModuleStateMachineContext {
301 LightningClientContext {
302 federation_id: self.federation_id,
303 gateway_conn: self.gateway_conn.clone(),
304 }
305 }
306
307 fn input_fee(
308 &self,
309 amount: Amount,
310 _input: &<Self::Common as ModuleCommon>::Input,
311 ) -> Option<Amount> {
312 Some(self.cfg.fee_consensus.fee(amount))
313 }
314
315 fn output_fee(
316 &self,
317 amount: Amount,
318 _output: &<Self::Common as ModuleCommon>::Output,
319 ) -> Option<Amount> {
320 Some(self.cfg.fee_consensus.fee(amount))
321 }
322
323 #[cfg(feature = "cli")]
324 async fn handle_cli_command(
325 &self,
326 args: &[std::ffi::OsString],
327 ) -> anyhow::Result<serde_json::Value> {
328 cli::handle_cli_command(self, args).await
329 }
330}
331
332fn generate_ephemeral_tweak(static_pk: PublicKey) -> ([u8; 32], PublicKey) {
333 let keypair = Keypair::new(secp256k1::SECP256K1, &mut rand::thread_rng());
334
335 let tweak = ecdh::SharedSecret::new(&static_pk, &keypair.secret_key());
336
337 (tweak.secret_bytes(), keypair.public_key())
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 keypair: Keypair,
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,
368 gateway_conn,
369 admin_auth,
370 }
371 }
372
373 fn spawn_gateway_map_update_task(
374 federation_id: FederationId,
375 client_ctx: ClientContext<Self>,
376 module_api: DynModuleApi,
377 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
378 task_group: &TaskGroup,
379 ) {
380 task_group.spawn_cancellable("gateway_map_update_task", async move {
381 let mut interval = tokio::time::interval(Duration::from_secs(24 * 60 * 60));
382
383 loop {
384 Self::update_gateway_map(&federation_id, &client_ctx, &module_api, &gateway_conn)
385 .await;
386 interval.tick().await;
387 }
388 });
389 }
390
391 async fn update_gateway_map(
392 federation_id: &FederationId,
393 client_ctx: &ClientContext<Self>,
394 module_api: &DynModuleApi,
395 gateway_conn: &Arc<dyn GatewayConnection + Send + Sync>,
396 ) {
397 if let Ok(gateways) = module_api.gateways().await {
404 let mut dbtx = client_ctx.module_db().begin_transaction().await;
405
406 for gateway in gateways {
407 if let Ok(Some(routing_info)) = gateway_conn
408 .routing_info(gateway.clone(), federation_id)
409 .await
410 {
411 dbtx.insert_entry(&GatewayKey(routing_info.lightning_public_key), &gateway)
412 .await;
413 }
414 }
415
416 if let Err(e) = dbtx.commit_tx_result().await {
417 warn!("Failed to commit the updated gateway mapping to the database: {e}");
418 }
419 }
420 }
421
422 async fn select_gateway(
423 &self,
424 invoice: Option<Bolt11Invoice>,
425 ) -> Result<(SafeUrl, RoutingInfo), SelectGatewayError> {
426 let gateways = self
427 .module_api
428 .gateways()
429 .await
430 .map_err(|e| SelectGatewayError::FederationError(e.to_string()))?;
431
432 if gateways.is_empty() {
433 return Err(SelectGatewayError::NoVettedGateways);
434 }
435
436 if let Some(invoice) = invoice {
437 if let Some(gateway) = self
438 .client_ctx
439 .module_db()
440 .begin_transaction_nc()
441 .await
442 .get_value(&GatewayKey(invoice.recover_payee_pub_key()))
443 .await
444 .filter(|gateway| gateways.contains(gateway))
445 {
446 if let Ok(Some(routing_info)) = self.routing_info(&gateway).await {
447 return Ok((gateway, routing_info));
448 }
449 }
450 }
451
452 for gateway in gateways {
453 if let Ok(Some(routing_info)) = self.routing_info(&gateway).await {
454 return Ok((gateway, routing_info));
455 }
456 }
457
458 Err(SelectGatewayError::FailedToFetchRoutingInfo)
459 }
460
461 async fn routing_info(
462 &self,
463 gateway: &SafeUrl,
464 ) -> Result<Option<RoutingInfo>, GatewayConnectionError> {
465 self.gateway_conn
466 .routing_info(gateway.clone(), &self.federation_id)
467 .await
468 }
469
470 pub async fn send(
487 &self,
488 invoice: Bolt11Invoice,
489 gateway: Option<SafeUrl>,
490 custom_meta: Value,
491 ) -> Result<OperationId, SendPaymentError> {
492 let amount = invoice
493 .amount_milli_satoshis()
494 .ok_or(SendPaymentError::InvoiceMissingAmount)?;
495
496 if invoice.is_expired() {
497 return Err(SendPaymentError::InvoiceExpired);
498 }
499
500 if self.cfg.network != invoice.currency().into() {
501 return Err(SendPaymentError::WrongCurrency {
502 invoice_currency: invoice.currency(),
503 federation_currency: self.cfg.network.into(),
504 });
505 }
506
507 let operation_id = self.get_next_operation_id(&invoice).await?;
508
509 let (ephemeral_tweak, ephemeral_pk) = generate_ephemeral_tweak(self.keypair.public_key());
510
511 let refund_keypair = SecretKey::from_slice(&ephemeral_tweak)
512 .expect("32 bytes, within curve order")
513 .keypair(secp256k1::SECP256K1);
514
515 let (gateway_api, routing_info) = match gateway {
516 Some(gateway_api) => (
517 gateway_api.clone(),
518 self.routing_info(&gateway_api)
519 .await
520 .map_err(SendPaymentError::GatewayConnectionError)?
521 .ok_or(SendPaymentError::UnknownFederation)?,
522 ),
523 None => self
524 .select_gateway(Some(invoice.clone()))
525 .await
526 .map_err(SendPaymentError::FailedToSelectGateway)?,
527 };
528
529 let (send_fee, expiration_delta) = routing_info.send_parameters(&invoice);
530
531 if !send_fee.le(&PaymentFee::SEND_FEE_LIMIT) {
532 return Err(SendPaymentError::PaymentFeeExceedsLimit);
533 }
534
535 if EXPIRATION_DELTA_LIMIT < expiration_delta {
536 return Err(SendPaymentError::ExpirationDeltaExceedsLimit);
537 }
538
539 let consensus_block_count = self
540 .module_api
541 .consensus_block_count()
542 .await
543 .map_err(|e| SendPaymentError::FederationError(e.to_string()))?;
544
545 let contract = OutgoingContract {
546 payment_image: PaymentImage::Hash(*invoice.payment_hash()),
547 amount: send_fee.add_to(amount),
548 expiration: consensus_block_count + expiration_delta + CONTRACT_CONFIRMATION_BUFFER,
549 claim_pk: routing_info.module_public_key,
550 refund_pk: refund_keypair.public_key(),
551 ephemeral_pk,
552 };
553
554 let contract_clone = contract.clone();
555 let gateway_api_clone = gateway_api.clone();
556 let invoice_clone = invoice.clone();
557
558 let client_output = ClientOutput::<LightningOutput> {
559 output: LightningOutput::V0(LightningOutputV0::Outgoing(contract.clone())),
560 amount: contract.amount,
561 };
562 let client_output_sm = ClientOutputSM::<LightningClientStateMachines> {
563 state_machines: Arc::new(move |range: OutPointRange| {
564 vec![LightningClientStateMachines::Send(SendStateMachine {
565 common: SendSMCommon {
566 operation_id,
567 outpoint: range.into_iter().next().unwrap(),
568 contract: contract_clone.clone(),
569 gateway_api: Some(gateway_api_clone.clone()),
570 invoice: Some(LightningInvoice::Bolt11(invoice_clone.clone())),
571 refund_keypair,
572 },
573 state: SendSMState::Funding,
574 })]
575 }),
576 };
577
578 let client_output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
579 vec![client_output],
580 vec![client_output_sm],
581 ));
582 let transaction = TransactionBuilder::new().with_outputs(client_output);
583
584 self.client_ctx
585 .finalize_and_submit_transaction(
586 operation_id,
587 LightningCommonInit::KIND.as_str(),
588 move |change_outpoint_range| {
589 LightningOperationMeta::Send(SendOperationMeta {
590 change_outpoint_range,
591 gateway: gateway_api.clone(),
592 contract: contract.clone(),
593 invoice: LightningInvoice::Bolt11(invoice.clone()),
594 custom_meta: custom_meta.clone(),
595 })
596 },
597 transaction,
598 )
599 .await
600 .map_err(|e| SendPaymentError::FinalizationError(e.to_string()))?;
601
602 Ok(operation_id)
603 }
604
605 async fn get_next_operation_id(
606 &self,
607 invoice: &Bolt11Invoice,
608 ) -> Result<OperationId, SendPaymentError> {
609 for payment_attempt in 0..u64::MAX {
610 let operation_id = OperationId::from_encodable(&(invoice.clone(), payment_attempt));
611
612 if !self.client_ctx.operation_exists(operation_id).await {
613 return Ok(operation_id);
614 }
615
616 if self.client_ctx.has_active_states(operation_id).await {
617 return Err(SendPaymentError::PendingPreviousPayment(operation_id));
618 }
619
620 let mut stream = self
621 .subscribe_send_operation_state_updates(operation_id)
622 .await
623 .expect("operation_id exists")
624 .into_stream();
625
626 while let Some(state) = stream.next().await {
629 if let SendOperationState::Success(_) = state {
630 return Err(SendPaymentError::SuccessfulPreviousPayment(operation_id));
631 }
632 }
633 }
634
635 panic!("We could not find an unused operation id for sending a lightning payment");
636 }
637
638 pub async fn subscribe_send_operation_state_updates(
640 &self,
641 operation_id: OperationId,
642 ) -> anyhow::Result<UpdateStreamOrOutcome<SendOperationState>> {
643 let operation = self.client_ctx.get_operation(operation_id).await?;
644 let mut stream = self.notifier.subscribe(operation_id).await;
645 let client_ctx = self.client_ctx.clone();
646 let module_api = self.module_api.clone();
647
648 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
649 stream! {
650 loop {
651 if let Some(LightningClientStateMachines::Send(state)) = stream.next().await {
652 match state.state {
653 SendSMState::Funding => yield SendOperationState::Funding,
654 SendSMState::Funded => yield SendOperationState::Funded,
655 SendSMState::Success(preimage) => {
656 assert!(state.common.contract.verify_preimage(&preimage));
658
659 yield SendOperationState::Success(preimage);
660 return;
661 },
662 SendSMState::Refunding(out_points) => {
663 yield SendOperationState::Refunding;
664
665 if client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await.is_ok() {
666 yield SendOperationState::Refunded;
667 return;
668 }
669
670 if let Some(preimage) = module_api.await_preimage(
674 state.common.outpoint,
675 0
676 ).await {
677 if state.common.contract.verify_preimage(&preimage) {
678 yield SendOperationState::Success(preimage);
679 return;
680 }
681 }
682
683 yield SendOperationState::Failure;
684 return;
685 },
686 SendSMState::Rejected(..) => {
687 yield SendOperationState::Failure;
688 return;
689 },
690 }
691 }
692 }
693 }
694 }))
695 }
696
697 pub async fn await_final_send_operation_state(
699 &self,
700 operation_id: OperationId,
701 ) -> anyhow::Result<FinalSendOperationState> {
702 let state = self
703 .subscribe_send_operation_state_updates(operation_id)
704 .await?
705 .into_stream()
706 .filter_map(|state| {
707 futures::future::ready(match state {
708 SendOperationState::Success(_) => Some(FinalSendOperationState::Success),
709 SendOperationState::Refunded => Some(FinalSendOperationState::Refunded),
710 SendOperationState::Failure => Some(FinalSendOperationState::Failure),
711 _ => None,
712 })
713 })
714 .next()
715 .await
716 .expect("Stream contains one final state");
717
718 Ok(state)
719 }
720
721 pub async fn receive(
733 &self,
734 amount: Amount,
735 expiry_secs: u32,
736 description: Bolt11InvoiceDescription,
737 gateway: Option<SafeUrl>,
738 custom_meta: Value,
739 ) -> Result<(Bolt11Invoice, OperationId), ReceiveError> {
740 let (gateway, contract, invoice) = self
741 .create_contract_and_fetch_invoice(
742 self.keypair.public_key(),
743 amount,
744 expiry_secs,
745 description,
746 gateway,
747 )
748 .await?;
749
750 let operation_id = self
751 .receive_incoming_contract(gateway, contract, invoice.clone(), custom_meta)
752 .await
753 .expect("The contract has been generated with our public key");
754
755 Ok((invoice, operation_id))
756 }
757
758 async fn create_contract_and_fetch_invoice(
762 &self,
763 recipient_static_pk: PublicKey,
764 amount: Amount,
765 expiry_secs: u32,
766 description: Bolt11InvoiceDescription,
767 gateway: Option<SafeUrl>,
768 ) -> Result<(SafeUrl, IncomingContract, Bolt11Invoice), ReceiveError> {
769 let (ephemeral_tweak, ephemeral_pk) = generate_ephemeral_tweak(recipient_static_pk);
770
771 let encryption_seed = ephemeral_tweak
772 .consensus_hash::<sha256::Hash>()
773 .to_byte_array();
774
775 let preimage = encryption_seed
776 .consensus_hash::<sha256::Hash>()
777 .to_byte_array();
778
779 let (gateway, routing_info) = match gateway {
780 Some(gateway) => (
781 gateway.clone(),
782 self.routing_info(&gateway)
783 .await
784 .map_err(ReceiveError::GatewayConnectionError)?
785 .ok_or(ReceiveError::UnknownFederation)?,
786 ),
787 None => self
788 .select_gateway(None)
789 .await
790 .map_err(ReceiveError::FailedToSelectGateway)?,
791 };
792
793 if !routing_info.receive_fee.le(&PaymentFee::RECEIVE_FEE_LIMIT) {
794 return Err(ReceiveError::PaymentFeeExceedsLimit);
795 }
796
797 let contract_amount = routing_info.receive_fee.subtract_from(amount.msats);
798
799 if contract_amount < Amount::from_sats(50) {
802 return Err(ReceiveError::DustAmount);
803 }
804
805 let expiration = duration_since_epoch()
806 .as_secs()
807 .saturating_add(u64::from(expiry_secs));
808
809 let claim_pk = recipient_static_pk
810 .mul_tweak(
811 secp256k1::SECP256K1,
812 &Scalar::from_be_bytes(ephemeral_tweak).expect("Within curve order"),
813 )
814 .expect("Tweak is valid");
815
816 let contract = IncomingContract::new(
817 self.cfg.tpe_agg_pk,
818 encryption_seed,
819 preimage,
820 PaymentImage::Hash(preimage.consensus_hash()),
821 contract_amount,
822 expiration,
823 claim_pk,
824 routing_info.module_public_key,
825 ephemeral_pk,
826 );
827
828 let invoice = self
829 .gateway_conn
830 .bolt11_invoice(
831 gateway.clone(),
832 self.federation_id,
833 contract.clone(),
834 amount,
835 description,
836 expiry_secs,
837 )
838 .await
839 .map_err(ReceiveError::GatewayConnectionError)?;
840
841 if invoice.payment_hash() != &preimage.consensus_hash() {
842 return Err(ReceiveError::InvalidInvoicePaymentHash);
843 }
844
845 if invoice.amount_milli_satoshis() != Some(amount.msats) {
846 return Err(ReceiveError::InvalidInvoiceAmount);
847 }
848
849 Ok((gateway, contract, invoice))
850 }
851
852 async fn receive_incoming_contract(
855 &self,
856 gateway: SafeUrl,
857 contract: IncomingContract,
858 invoice: Bolt11Invoice,
859 custom_meta: Value,
860 ) -> Option<OperationId> {
861 let operation_id = OperationId::from_encodable(&contract.clone());
862
863 let (claim_keypair, agg_decryption_key) = self.recover_contract_keys(&contract)?;
864
865 let receive_sm = LightningClientStateMachines::Receive(ReceiveStateMachine {
866 common: ReceiveSMCommon {
867 operation_id,
868 contract: contract.clone(),
869 claim_keypair,
870 agg_decryption_key,
871 },
872 state: ReceiveSMState::Pending,
873 });
874
875 self.client_ctx
878 .manual_operation_start(
879 operation_id,
880 LightningCommonInit::KIND.as_str(),
881 LightningOperationMeta::Receive(ReceiveOperationMeta {
882 gateway,
883 contract,
884 invoice: LightningInvoice::Bolt11(invoice),
885 custom_meta,
886 }),
887 vec![self.client_ctx.make_dyn_state(receive_sm)],
888 )
889 .await
890 .ok();
891
892 Some(operation_id)
893 }
894
895 fn recover_contract_keys(
896 &self,
897 contract: &IncomingContract,
898 ) -> Option<(Keypair, AggregateDecryptionKey)> {
899 let ephemeral_tweak = ecdh::SharedSecret::new(
900 &contract.commitment.ephemeral_pk,
901 &self.keypair.secret_key(),
902 )
903 .secret_bytes();
904
905 let encryption_seed = ephemeral_tweak
906 .consensus_hash::<sha256::Hash>()
907 .to_byte_array();
908
909 let claim_keypair = self
910 .keypair
911 .secret_key()
912 .mul_tweak(&Scalar::from_be_bytes(ephemeral_tweak).expect("Within curve order"))
913 .expect("Tweak is valid")
914 .keypair(secp256k1::SECP256K1);
915
916 if claim_keypair.public_key() != contract.commitment.claim_pk {
917 return None; }
919
920 let agg_decryption_key = derive_agg_dk(&self.cfg.tpe_agg_pk, &encryption_seed);
921
922 if !contract.verify_agg_decryption_key(&self.cfg.tpe_agg_pk, &agg_decryption_key) {
923 return None; }
925
926 contract.decrypt_preimage(&agg_decryption_key)?;
927
928 Some((claim_keypair, agg_decryption_key))
929 }
930
931 pub async fn subscribe_receive_operation_state_updates(
933 &self,
934 operation_id: OperationId,
935 ) -> anyhow::Result<UpdateStreamOrOutcome<ReceiveOperationState>> {
936 let operation = self.client_ctx.get_operation(operation_id).await?;
937 let mut stream = self.notifier.subscribe(operation_id).await;
938 let client_ctx = self.client_ctx.clone();
939
940 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
941 stream! {
942 loop {
943 if let Some(LightningClientStateMachines::Receive(state)) = stream.next().await {
944 match state.state {
945 ReceiveSMState::Pending => yield ReceiveOperationState::Pending,
946 ReceiveSMState::Claiming(out_points) => {
947 yield ReceiveOperationState::Claiming;
948
949 if client_ctx.await_primary_module_outputs(operation_id, out_points).await.is_ok() {
950 yield ReceiveOperationState::Claimed;
951 } else {
952 yield ReceiveOperationState::Failure;
953 }
954 return;
955 },
956 ReceiveSMState::Expired => {
957 yield ReceiveOperationState::Expired;
958 return;
959 }
960 }
961 }
962 }
963 }
964 }))
965 }
966
967 pub async fn await_final_receive_operation_state(
969 &self,
970 operation_id: OperationId,
971 ) -> anyhow::Result<FinalReceiveOperationState> {
972 let state = self
973 .subscribe_receive_operation_state_updates(operation_id)
974 .await?
975 .into_stream()
976 .filter_map(|state| {
977 futures::future::ready(match state {
978 ReceiveOperationState::Expired => Some(FinalReceiveOperationState::Expired),
979 ReceiveOperationState::Claimed => Some(FinalReceiveOperationState::Claimed),
980 ReceiveOperationState::Failure => Some(FinalReceiveOperationState::Failure),
981 _ => None,
982 })
983 })
984 .next()
985 .await
986 .expect("Stream contains one final state");
987
988 Ok(state)
989 }
990}
991
992#[derive(Error, Debug, Clone, Eq, PartialEq)]
993pub enum SelectGatewayError {
994 #[error("Federation returned an error: {0}")]
995 FederationError(String),
996 #[error("The federation has no vetted gateways")]
997 NoVettedGateways,
998 #[error("All vetted gateways failed to respond on request of the routing info")]
999 FailedToFetchRoutingInfo,
1000}
1001
1002#[derive(Error, Debug, Clone, Eq, PartialEq)]
1003pub enum SendPaymentError {
1004 #[error("The invoice has not amount")]
1005 InvoiceMissingAmount,
1006 #[error("The invoice has expired")]
1007 InvoiceExpired,
1008 #[error("A previous payment for the same invoice is still pending: {}", .0.fmt_full())]
1009 PendingPreviousPayment(OperationId),
1010 #[error("A previous payment for the same invoice was successful: {}", .0.fmt_full())]
1011 SuccessfulPreviousPayment(OperationId),
1012 #[error("Failed to select gateway: {0}")]
1013 FailedToSelectGateway(SelectGatewayError),
1014 #[error("Gateway connection error: {0}")]
1015 GatewayConnectionError(GatewayConnectionError),
1016 #[error("The gateway does not support our federation")]
1017 UnknownFederation,
1018 #[error("The gateways fee of exceeds the limit")]
1019 PaymentFeeExceedsLimit,
1020 #[error("The gateways expiration delta of exceeds the limit")]
1021 ExpirationDeltaExceedsLimit,
1022 #[error("Federation returned an error: {0}")]
1023 FederationError(String),
1024 #[error("We failed to finalize the funding transaction")]
1025 FinalizationError(String),
1026 #[error(
1027 "The invoice was for the wrong currency. Invoice currency={invoice_currency} Federation Currency={federation_currency}"
1028 )]
1029 WrongCurrency {
1030 invoice_currency: Currency,
1031 federation_currency: Currency,
1032 },
1033}
1034
1035#[derive(Error, Debug, Clone, Eq, PartialEq)]
1036pub enum ReceiveError {
1037 #[error("Failed to select gateway: {0}")]
1038 FailedToSelectGateway(SelectGatewayError),
1039 #[error("Gateway connection error: {0}")]
1040 GatewayConnectionError(GatewayConnectionError),
1041 #[error("The gateway does not support our federation")]
1042 UnknownFederation,
1043 #[error("The gateways fee exceeds the limit")]
1044 PaymentFeeExceedsLimit,
1045 #[error("The total fees required to complete this payment exceed its amount")]
1046 DustAmount,
1047 #[error("The invoice's payment hash is incorrect")]
1048 InvalidInvoicePaymentHash,
1049 #[error("The invoice's amount is incorrect")]
1050 InvalidInvoiceAmount,
1051}
1052
1053#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1054pub enum LightningClientStateMachines {
1055 Send(SendStateMachine),
1056 Receive(ReceiveStateMachine),
1057}
1058
1059impl IntoDynInstance for LightningClientStateMachines {
1060 type DynType = DynState;
1061
1062 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1063 DynState::from_typed(instance_id, self)
1064 }
1065}
1066
1067impl State for LightningClientStateMachines {
1068 type ModuleContext = LightningClientContext;
1069
1070 fn transitions(
1071 &self,
1072 context: &Self::ModuleContext,
1073 global_context: &DynGlobalClientContext,
1074 ) -> Vec<StateTransition<Self>> {
1075 match self {
1076 LightningClientStateMachines::Send(state) => {
1077 sm_enum_variant_translation!(
1078 state.transitions(context, global_context),
1079 LightningClientStateMachines::Send
1080 )
1081 }
1082 LightningClientStateMachines::Receive(state) => {
1083 sm_enum_variant_translation!(
1084 state.transitions(context, global_context),
1085 LightningClientStateMachines::Receive
1086 )
1087 }
1088 }
1089 }
1090
1091 fn operation_id(&self) -> OperationId {
1092 match self {
1093 LightningClientStateMachines::Send(state) => state.operation_id(),
1094 LightningClientStateMachines::Receive(state) => state.operation_id(),
1095 }
1096 }
1097}