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, OutPoint, TransactionId, 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 funding_txid: TransactionId,
83 pub funding_change_outpoints: Vec<OutPoint>,
84 pub gateway: SafeUrl,
85 pub contract: OutgoingContract,
86 pub invoice: LightningInvoice,
87 pub custom_meta: Value,
88}
89
90impl SendOperationMeta {
91 pub fn gateway_fee(&self) -> Amount {
93 match &self.invoice {
94 LightningInvoice::Bolt11(invoice) => self.contract.amount.saturating_sub(
95 Amount::from_msats(invoice.amount_milli_satoshis().expect("Invoice has amount")),
96 ),
97 }
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ReceiveOperationMeta {
103 pub gateway: SafeUrl,
104 pub contract: IncomingContract,
105 pub invoice: LightningInvoice,
106 pub custom_meta: Value,
107}
108
109impl ReceiveOperationMeta {
110 pub fn gateway_fee(&self) -> Amount {
112 match &self.invoice {
113 LightningInvoice::Bolt11(invoice) => {
114 Amount::from_msats(invoice.amount_milli_satoshis().expect("Invoice has amount"))
115 .saturating_sub(self.contract.commitment.amount)
116 }
117 }
118 }
119}
120
121#[cfg_attr(doc, aquamarine::aquamarine)]
122#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
140pub enum SendOperationState {
141 Funding,
143 Funded,
145 Success,
147 Refunding,
149 Refunded,
151 Failure,
153}
154
155#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
157pub enum FinalSendOperationState {
158 Success,
160 Refunded,
162 Failure,
164}
165
166pub type SendResult = Result<OperationId, SendPaymentError>;
167
168#[cfg_attr(doc, aquamarine::aquamarine)]
169#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
181pub enum ReceiveOperationState {
182 Pending,
184 Expired,
186 Claiming,
188 Claimed,
190 Failure,
192}
193
194#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
196pub enum FinalReceiveOperationState {
197 Expired,
199 Claimed,
201 Failure,
203}
204
205pub type ReceiveResult = Result<(Bolt11Invoice, OperationId), ReceiveError>;
206
207#[derive(Debug, Clone)]
208pub struct LightningClientInit {
209 pub gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
210}
211
212impl Default for LightningClientInit {
213 fn default() -> Self {
214 LightningClientInit {
215 gateway_conn: Arc::new(RealGatewayConnection),
216 }
217 }
218}
219
220impl ModuleInit for LightningClientInit {
221 type Common = LightningCommonInit;
222
223 async fn dump_database(
224 &self,
225 _dbtx: &mut DatabaseTransaction<'_>,
226 _prefix_names: Vec<String>,
227 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
228 Box::new(BTreeMap::new().into_iter())
229 }
230}
231
232#[apply(async_trait_maybe_send!)]
233impl ClientModuleInit for LightningClientInit {
234 type Module = LightningClientModule;
235
236 fn supported_api_versions(&self) -> MultiApiVersion {
237 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
238 .expect("no version conflicts")
239 }
240
241 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
242 Ok(LightningClientModule::new(
243 *args.federation_id(),
244 args.cfg().clone(),
245 args.notifier().clone(),
246 args.context(),
247 args.module_api().clone(),
248 args.module_root_secret()
249 .clone()
250 .to_secp_key(fedimint_core::secp256k1::SECP256K1),
251 self.gateway_conn.clone(),
252 args.admin_auth().cloned(),
253 args.task_group(),
254 ))
255 }
256
257 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
258 Some(
259 DbKeyPrefix::iter()
260 .map(|p| p as u8)
261 .chain(
262 DbKeyPrefix::ExternalReservedStart as u8
263 ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
264 )
265 .collect(),
266 )
267 }
268}
269
270#[derive(Debug, Clone)]
271pub struct LightningClientContext {
272 federation_id: FederationId,
273 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
274}
275
276impl Context for LightningClientContext {
277 const KIND: Option<ModuleKind> = Some(KIND);
278}
279
280#[derive(Debug)]
281pub struct LightningClientModule {
282 federation_id: FederationId,
283 cfg: LightningClientConfig,
284 notifier: ModuleNotifier<LightningClientStateMachines>,
285 client_ctx: ClientContext<Self>,
286 module_api: DynModuleApi,
287 keypair: Keypair,
288 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
289 #[allow(unused)] admin_auth: Option<ApiAuth>,
291}
292
293#[apply(async_trait_maybe_send!)]
294impl ClientModule for LightningClientModule {
295 type Init = LightningClientInit;
296 type Common = LightningModuleTypes;
297 type Backup = NoModuleBackup;
298 type ModuleStateMachineContext = LightningClientContext;
299 type States = LightningClientStateMachines;
300
301 fn context(&self) -> Self::ModuleStateMachineContext {
302 LightningClientContext {
303 federation_id: self.federation_id,
304 gateway_conn: self.gateway_conn.clone(),
305 }
306 }
307
308 fn input_fee(
309 &self,
310 amount: Amount,
311 _input: &<Self::Common as ModuleCommon>::Input,
312 ) -> Option<Amount> {
313 Some(self.cfg.fee_consensus.fee(amount))
314 }
315
316 fn output_fee(
317 &self,
318 amount: Amount,
319 _output: &<Self::Common as ModuleCommon>::Output,
320 ) -> Option<Amount> {
321 Some(self.cfg.fee_consensus.fee(amount))
322 }
323
324 #[cfg(feature = "cli")]
325 async fn handle_cli_command(
326 &self,
327 args: &[std::ffi::OsString],
328 ) -> anyhow::Result<serde_json::Value> {
329 cli::handle_cli_command(self, args).await
330 }
331}
332
333fn generate_ephemeral_tweak(static_pk: PublicKey) -> ([u8; 32], PublicKey) {
334 let keypair = Keypair::new(secp256k1::SECP256K1, &mut rand::thread_rng());
335
336 let tweak = ecdh::SharedSecret::new(&static_pk, &keypair.secret_key());
337
338 (tweak.secret_bytes(), keypair.public_key())
339}
340
341impl LightningClientModule {
342 #[allow(clippy::too_many_arguments)]
343 fn new(
344 federation_id: FederationId,
345 cfg: LightningClientConfig,
346 notifier: ModuleNotifier<LightningClientStateMachines>,
347 client_ctx: ClientContext<Self>,
348 module_api: DynModuleApi,
349 keypair: Keypair,
350 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
351 admin_auth: Option<ApiAuth>,
352 task_group: &TaskGroup,
353 ) -> Self {
354 Self::spawn_gateway_map_update_task(
355 federation_id,
356 client_ctx.clone(),
357 module_api.clone(),
358 gateway_conn.clone(),
359 task_group,
360 );
361
362 Self {
363 federation_id,
364 cfg,
365 notifier,
366 client_ctx,
367 module_api,
368 keypair,
369 gateway_conn,
370 admin_auth,
371 }
372 }
373
374 fn spawn_gateway_map_update_task(
375 federation_id: FederationId,
376 client_ctx: ClientContext<Self>,
377 module_api: DynModuleApi,
378 gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
379 task_group: &TaskGroup,
380 ) {
381 task_group.spawn("gateway_map_update_task", move |handle| async move {
382 let mut interval = tokio::time::interval(Duration::from_secs(24 * 60 * 60));
383 let mut shutdown_rx = handle.make_shutdown_rx();
384
385 loop {
386 tokio::select! {
387 _ = &mut Box::pin(interval.tick()) => {
388 Self::update_gateway_map(
389 &federation_id,
390 &client_ctx,
391 &module_api,
392 &gateway_conn
393 ).await;
394 },
395 () = &mut shutdown_rx => { break },
396 };
397 }
398 });
399 }
400
401 async fn update_gateway_map(
402 federation_id: &FederationId,
403 client_ctx: &ClientContext<Self>,
404 module_api: &DynModuleApi,
405 gateway_conn: &Arc<dyn GatewayConnection + Send + Sync>,
406 ) {
407 if let Ok(gateways) = module_api.gateways().await {
414 let mut dbtx = client_ctx.module_db().begin_transaction().await;
415
416 for gateway in gateways {
417 if let Ok(Some(routing_info)) = gateway_conn
418 .routing_info(gateway.clone(), federation_id)
419 .await
420 {
421 dbtx.insert_entry(&GatewayKey(routing_info.lightning_public_key), &gateway)
422 .await;
423 }
424 }
425
426 if let Err(e) = dbtx.commit_tx_result().await {
427 warn!("Failed to commit the updated gateway mapping to the database: {e}");
428 }
429 }
430 }
431
432 async fn select_gateway(
433 &self,
434 invoice: Option<Bolt11Invoice>,
435 ) -> Result<(SafeUrl, RoutingInfo), SelectGatewayError> {
436 let gateways = self
437 .module_api
438 .gateways()
439 .await
440 .map_err(|e| SelectGatewayError::FederationError(e.to_string()))?;
441
442 if gateways.is_empty() {
443 return Err(SelectGatewayError::NoVettedGateways);
444 }
445
446 if let Some(invoice) = invoice {
447 if let Some(gateway) = self
448 .client_ctx
449 .module_db()
450 .begin_transaction_nc()
451 .await
452 .get_value(&GatewayKey(invoice.recover_payee_pub_key()))
453 .await
454 .filter(|gateway| gateways.contains(gateway))
455 {
456 if let Ok(Some(routing_info)) = self.routing_info(&gateway).await {
457 return Ok((gateway, routing_info));
458 }
459 }
460 }
461
462 for gateway in gateways {
463 if let Ok(Some(routing_info)) = self.routing_info(&gateway).await {
464 return Ok((gateway, routing_info));
465 }
466 }
467
468 Err(SelectGatewayError::FailedToFetchRoutingInfo)
469 }
470
471 async fn routing_info(
472 &self,
473 gateway: &SafeUrl,
474 ) -> Result<Option<RoutingInfo>, GatewayConnectionError> {
475 self.gateway_conn
476 .routing_info(gateway.clone(), &self.federation_id)
477 .await
478 }
479
480 pub async fn send(
497 &self,
498 invoice: Bolt11Invoice,
499 gateway: Option<SafeUrl>,
500 custom_meta: Value,
501 ) -> Result<OperationId, SendPaymentError> {
502 let amount = invoice
503 .amount_milli_satoshis()
504 .ok_or(SendPaymentError::InvoiceMissingAmount)?;
505
506 if invoice.is_expired() {
507 return Err(SendPaymentError::InvoiceExpired);
508 }
509
510 if self.cfg.network != invoice.currency().into() {
511 return Err(SendPaymentError::WrongCurrency {
512 invoice_currency: invoice.currency(),
513 federation_currency: self.cfg.network.into(),
514 });
515 }
516
517 let operation_id = self.get_next_operation_id(&invoice).await?;
518
519 let (ephemeral_tweak, ephemeral_pk) = generate_ephemeral_tweak(self.keypair.public_key());
520
521 let refund_keypair = SecretKey::from_slice(&ephemeral_tweak)
522 .expect("32 bytes, within curve order")
523 .keypair(secp256k1::SECP256K1);
524
525 let (gateway_api, routing_info) = match gateway {
526 Some(gateway_api) => (
527 gateway_api.clone(),
528 self.routing_info(&gateway_api)
529 .await
530 .map_err(SendPaymentError::GatewayConnectionError)?
531 .ok_or(SendPaymentError::UnknownFederation)?,
532 ),
533 None => self
534 .select_gateway(Some(invoice.clone()))
535 .await
536 .map_err(SendPaymentError::FailedToSelectGateway)?,
537 };
538
539 let (send_fee, expiration_delta) = routing_info.send_parameters(&invoice);
540
541 if !send_fee.le(&PaymentFee::SEND_FEE_LIMIT) {
542 return Err(SendPaymentError::PaymentFeeExceedsLimit);
543 }
544
545 if EXPIRATION_DELTA_LIMIT < expiration_delta {
546 return Err(SendPaymentError::ExpirationDeltaExceedsLimit);
547 }
548
549 let consensus_block_count = self
550 .module_api
551 .consensus_block_count()
552 .await
553 .map_err(|e| SendPaymentError::FederationError(e.to_string()))?;
554
555 let contract = OutgoingContract {
556 payment_image: PaymentImage::Hash(*invoice.payment_hash()),
557 amount: send_fee.add_to(amount),
558 expiration: consensus_block_count + expiration_delta + CONTRACT_CONFIRMATION_BUFFER,
559 claim_pk: routing_info.module_public_key,
560 refund_pk: refund_keypair.public_key(),
561 ephemeral_pk,
562 };
563
564 let contract_clone = contract.clone();
565 let gateway_api_clone = gateway_api.clone();
566 let invoice_clone = invoice.clone();
567
568 let client_output = ClientOutput::<LightningOutput> {
569 output: LightningOutput::V0(LightningOutputV0::Outgoing(contract.clone())),
570 amount: contract.amount,
571 };
572 let client_output_sm = ClientOutputSM::<LightningClientStateMachines> {
573 state_machines: Arc::new(move |out_point_range: OutPointRange| {
574 vec![LightningClientStateMachines::Send(SendStateMachine {
575 common: SendSMCommon {
576 operation_id,
577 funding_txid: out_point_range.txid(),
578 gateway_api: gateway_api_clone.clone(),
579 contract: contract_clone.clone(),
580 invoice: LightningInvoice::Bolt11(invoice_clone.clone()),
581 refund_keypair,
582 },
583 state: SendSMState::Funding,
584 })]
585 }),
586 };
587
588 let client_output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
589 vec![client_output],
590 vec![client_output_sm],
591 ));
592 let transaction = TransactionBuilder::new().with_outputs(client_output);
593
594 self.client_ctx
595 .finalize_and_submit_transaction(
596 operation_id,
597 LightningCommonInit::KIND.as_str(),
598 move |change_range| {
599 LightningOperationMeta::Send(SendOperationMeta {
600 funding_txid: change_range.txid(),
601 funding_change_outpoints: change_range.into_iter().collect(),
602 gateway: gateway_api.clone(),
603 contract: contract.clone(),
604 invoice: LightningInvoice::Bolt11(invoice.clone()),
605 custom_meta: custom_meta.clone(),
606 })
607 },
608 transaction,
609 )
610 .await
611 .map_err(|e| SendPaymentError::FinalizationError(e.to_string()))?;
612
613 Ok(operation_id)
614 }
615
616 async fn get_next_operation_id(
617 &self,
618 invoice: &Bolt11Invoice,
619 ) -> Result<OperationId, SendPaymentError> {
620 for payment_attempt in 0..u64::MAX {
621 let operation_id = OperationId::from_encodable(&(invoice.clone(), payment_attempt));
622
623 if !self.client_ctx.operation_exists(operation_id).await {
624 return Ok(operation_id);
625 }
626
627 if self.client_ctx.has_active_states(operation_id).await {
628 return Err(SendPaymentError::PendingPreviousPayment(operation_id));
629 }
630
631 let mut stream = self
632 .subscribe_send_operation_state_updates(operation_id)
633 .await
634 .expect("operation_id exists")
635 .into_stream();
636
637 while let Some(state) = stream.next().await {
640 if let SendOperationState::Success = state {
641 return Err(SendPaymentError::SuccessfulPreviousPayment(operation_id));
642 }
643 }
644 }
645
646 panic!("We could not find an unused operation id for sending a lightning payment");
647 }
648
649 pub async fn subscribe_send_operation_state_updates(
651 &self,
652 operation_id: OperationId,
653 ) -> anyhow::Result<UpdateStreamOrOutcome<SendOperationState>> {
654 let operation = self.client_ctx.get_operation(operation_id).await?;
655 let mut stream = self.notifier.subscribe(operation_id).await;
656 let client_ctx = self.client_ctx.clone();
657 let module_api = self.module_api.clone();
658
659 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
660 stream! {
661 loop {
662 if let Some(LightningClientStateMachines::Send(state)) = stream.next().await {
663 match state.state {
664 SendSMState::Funding => yield SendOperationState::Funding,
665 SendSMState::Funded => yield SendOperationState::Funded,
666 SendSMState::Success(preimage) => {
667 assert!(state.common.contract.verify_preimage(&preimage));
669
670 yield SendOperationState::Success;
671 return;
672 },
673 SendSMState::Refunding(out_points) => {
674 yield SendOperationState::Refunding;
675
676 if client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await.is_ok() {
677 yield SendOperationState::Refunded;
678 return;
679 }
680
681 if let Some(preimage) = module_api.await_preimage(
685 &state.common.contract.contract_id(),
686 0
687 ).await {
688 if state.common.contract.verify_preimage(&preimage) {
689 yield SendOperationState::Success;
690 return;
691 }
692 }
693
694 yield SendOperationState::Failure;
695 return;
696 },
697 SendSMState::Rejected(..) => {
698 yield SendOperationState::Failure;
699 return;
700 },
701 }
702 }
703 }
704 }
705 }))
706 }
707
708 pub async fn await_final_send_operation_state(
710 &self,
711 operation_id: OperationId,
712 ) -> anyhow::Result<FinalSendOperationState> {
713 let state = self
714 .subscribe_send_operation_state_updates(operation_id)
715 .await?
716 .into_stream()
717 .filter_map(|state| {
718 futures::future::ready(match state {
719 SendOperationState::Success => Some(FinalSendOperationState::Success),
720 SendOperationState::Refunded => Some(FinalSendOperationState::Refunded),
721 SendOperationState::Failure => Some(FinalSendOperationState::Failure),
722 _ => None,
723 })
724 })
725 .next()
726 .await
727 .expect("Stream contains one final state");
728
729 Ok(state)
730 }
731
732 pub async fn receive(
744 &self,
745 amount: Amount,
746 expiry_secs: u32,
747 description: Bolt11InvoiceDescription,
748 gateway: Option<SafeUrl>,
749 custom_meta: Value,
750 ) -> Result<(Bolt11Invoice, OperationId), ReceiveError> {
751 let (gateway, contract, invoice) = self
752 .create_contract_and_fetch_invoice(
753 self.keypair.public_key(),
754 amount,
755 expiry_secs,
756 description,
757 gateway,
758 )
759 .await?;
760
761 let operation_id = self
762 .receive_incoming_contract(gateway, contract, invoice.clone(), custom_meta)
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) = generate_ephemeral_tweak(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(50) {
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 gateway: SafeUrl,
868 contract: IncomingContract,
869 invoice: Bolt11Invoice,
870 custom_meta: Value,
871 ) -> Option<OperationId> {
872 let operation_id = OperationId::from_encodable(&contract.clone());
873
874 let (claim_keypair, agg_decryption_key) = self.recover_contract_keys(&contract)?;
875
876 let receive_sm = LightningClientStateMachines::Receive(ReceiveStateMachine {
877 common: ReceiveSMCommon {
878 operation_id,
879 contract: contract.clone(),
880 claim_keypair,
881 agg_decryption_key,
882 },
883 state: ReceiveSMState::Pending,
884 });
885
886 self.client_ctx
889 .manual_operation_start(
890 operation_id,
891 LightningCommonInit::KIND.as_str(),
892 LightningOperationMeta::Receive(ReceiveOperationMeta {
893 gateway,
894 contract,
895 invoice: LightningInvoice::Bolt11(invoice),
896 custom_meta,
897 }),
898 vec![self.client_ctx.make_dyn_state(receive_sm)],
899 )
900 .await
901 .ok();
902
903 Some(operation_id)
904 }
905
906 fn recover_contract_keys(
907 &self,
908 contract: &IncomingContract,
909 ) -> Option<(Keypair, AggregateDecryptionKey)> {
910 let ephemeral_tweak = ecdh::SharedSecret::new(
911 &contract.commitment.ephemeral_pk,
912 &self.keypair.secret_key(),
913 )
914 .secret_bytes();
915
916 let encryption_seed = ephemeral_tweak
917 .consensus_hash::<sha256::Hash>()
918 .to_byte_array();
919
920 let claim_keypair = self
921 .keypair
922 .secret_key()
923 .mul_tweak(&Scalar::from_be_bytes(ephemeral_tweak).expect("Within curve order"))
924 .expect("Tweak is valid")
925 .keypair(secp256k1::SECP256K1);
926
927 if claim_keypair.public_key() != contract.commitment.claim_pk {
928 return None; }
930
931 let agg_decryption_key = derive_agg_dk(&self.cfg.tpe_agg_pk, &encryption_seed);
932
933 if !contract.verify_agg_decryption_key(&self.cfg.tpe_agg_pk, &agg_decryption_key) {
934 return None; }
936
937 contract.decrypt_preimage(&agg_decryption_key)?;
938
939 Some((claim_keypair, agg_decryption_key))
940 }
941
942 pub async fn subscribe_receive_operation_state_updates(
944 &self,
945 operation_id: OperationId,
946 ) -> anyhow::Result<UpdateStreamOrOutcome<ReceiveOperationState>> {
947 let operation = self.client_ctx.get_operation(operation_id).await?;
948 let mut stream = self.notifier.subscribe(operation_id).await;
949 let client_ctx = self.client_ctx.clone();
950
951 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
952 stream! {
953 loop {
954 if let Some(LightningClientStateMachines::Receive(state)) = stream.next().await {
955 match state.state {
956 ReceiveSMState::Pending => yield ReceiveOperationState::Pending,
957 ReceiveSMState::Claiming(out_points) => {
958 yield ReceiveOperationState::Claiming;
959
960 if client_ctx.await_primary_module_outputs(operation_id, out_points).await.is_ok() {
961 yield ReceiveOperationState::Claimed;
962 } else {
963 yield ReceiveOperationState::Failure;
964 }
965 return;
966 },
967 ReceiveSMState::Expired => {
968 yield ReceiveOperationState::Expired;
969 return;
970 }
971 }
972 }
973 }
974 }
975 }))
976 }
977
978 pub async fn await_final_receive_operation_state(
980 &self,
981 operation_id: OperationId,
982 ) -> anyhow::Result<FinalReceiveOperationState> {
983 let state = self
984 .subscribe_receive_operation_state_updates(operation_id)
985 .await?
986 .into_stream()
987 .filter_map(|state| {
988 futures::future::ready(match state {
989 ReceiveOperationState::Expired => Some(FinalReceiveOperationState::Expired),
990 ReceiveOperationState::Claimed => Some(FinalReceiveOperationState::Claimed),
991 ReceiveOperationState::Failure => Some(FinalReceiveOperationState::Failure),
992 _ => None,
993 })
994 })
995 .next()
996 .await
997 .expect("Stream contains one final state");
998
999 Ok(state)
1000 }
1001}
1002
1003#[derive(Error, Debug, Clone, Eq, PartialEq)]
1004pub enum SelectGatewayError {
1005 #[error("Federation returned an error: {0}")]
1006 FederationError(String),
1007 #[error("The federation has no vetted gateways")]
1008 NoVettedGateways,
1009 #[error("All vetted gateways failed to respond on request of the routing info")]
1010 FailedToFetchRoutingInfo,
1011}
1012
1013#[derive(Error, Debug, Clone, Eq, PartialEq)]
1014pub enum SendPaymentError {
1015 #[error("The invoice has not amount")]
1016 InvoiceMissingAmount,
1017 #[error("The invoice has expired")]
1018 InvoiceExpired,
1019 #[error("A previous payment for the same invoice is still pending: {}", .0.fmt_full())]
1020 PendingPreviousPayment(OperationId),
1021 #[error("A previous payment for the same invoice was successful: {}", .0.fmt_full())]
1022 SuccessfulPreviousPayment(OperationId),
1023 #[error("Failed to select gateway: {0}")]
1024 FailedToSelectGateway(SelectGatewayError),
1025 #[error("Gateway connection error: {0}")]
1026 GatewayConnectionError(GatewayConnectionError),
1027 #[error("The gateway does not support our federation")]
1028 UnknownFederation,
1029 #[error("The gateways fee of exceeds the limit")]
1030 PaymentFeeExceedsLimit,
1031 #[error("The gateways expiration delta of exceeds the limit")]
1032 ExpirationDeltaExceedsLimit,
1033 #[error("Federation returned an error: {0}")]
1034 FederationError(String),
1035 #[error("We failed to finalize the funding transaction")]
1036 FinalizationError(String),
1037 #[error(
1038 "The invoice was for the wrong currency. Invoice currency={invoice_currency} Federation Currency={federation_currency}"
1039 )]
1040 WrongCurrency {
1041 invoice_currency: Currency,
1042 federation_currency: Currency,
1043 },
1044}
1045
1046#[derive(Error, Debug, Clone, Eq, PartialEq)]
1047pub enum ReceiveError {
1048 #[error("Failed to select gateway: {0}")]
1049 FailedToSelectGateway(SelectGatewayError),
1050 #[error("Gateway connection error: {0}")]
1051 GatewayConnectionError(GatewayConnectionError),
1052 #[error("The gateway does not support our federation")]
1053 UnknownFederation,
1054 #[error("The gateways fee exceeds the limit")]
1055 PaymentFeeExceedsLimit,
1056 #[error("The total fees required to complete this payment exceed its amount")]
1057 DustAmount,
1058 #[error("The invoice's payment hash is incorrect")]
1059 InvalidInvoicePaymentHash,
1060 #[error("The invoice's amount is incorrect")]
1061 InvalidInvoiceAmount,
1062}
1063
1064#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1065pub enum LightningClientStateMachines {
1066 Send(SendStateMachine),
1067 Receive(ReceiveStateMachine),
1068}
1069
1070impl IntoDynInstance for LightningClientStateMachines {
1071 type DynType = DynState;
1072
1073 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1074 DynState::from_typed(instance_id, self)
1075 }
1076}
1077
1078impl State for LightningClientStateMachines {
1079 type ModuleContext = LightningClientContext;
1080
1081 fn transitions(
1082 &self,
1083 context: &Self::ModuleContext,
1084 global_context: &DynGlobalClientContext,
1085 ) -> Vec<StateTransition<Self>> {
1086 match self {
1087 LightningClientStateMachines::Send(state) => {
1088 sm_enum_variant_translation!(
1089 state.transitions(context, global_context),
1090 LightningClientStateMachines::Send
1091 )
1092 }
1093 LightningClientStateMachines::Receive(state) => {
1094 sm_enum_variant_translation!(
1095 state.transitions(context, global_context),
1096 LightningClientStateMachines::Receive
1097 )
1098 }
1099 }
1100 }
1101
1102 fn operation_id(&self) -> OperationId {
1103 match self {
1104 LightningClientStateMachines::Send(state) => state.operation_id(),
1105 LightningClientStateMachines::Receive(state) => state.operation_id(),
1106 }
1107 }
1108}