1mod complete;
2pub mod error;
3pub mod events;
4pub mod pay;
5#[cfg(test)]
6mod tests;
7
8use std::collections::BTreeMap;
9use std::fmt;
10use std::fmt::Debug;
11use std::future::Future;
12use std::sync::Arc;
13use std::time::Duration;
14
15use async_stream::stream;
16use async_trait::async_trait;
17use bitcoin::hashes::{Hash, sha256};
18use bitcoin::key::Secp256k1;
19use bitcoin::secp256k1::All;
20use complete::{GatewayCompleteCommon, GatewayCompleteStates, WaitForPreimageState};
21use events::{IncomingPaymentStarted, OutgoingPaymentStarted};
22use fedimint_api_client::api::DynModuleApi;
23use fedimint_client::ClientHandleArc;
24use fedimint_client_module::error::ClientModuleError;
25use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
26use fedimint_client_module::module::recovery::NoModuleBackup;
27use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
28use fedimint_client_module::oplog::UpdateStreamOrOutcome;
29use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
30use fedimint_client_module::transaction::{
31 ClientOutput, ClientOutputBundle, ClientOutputSM, TransactionBuilder,
32};
33use fedimint_client_module::{
34 AddStateMachinesError, DynGlobalClientContext, OperationLookupError,
35 sm_enum_variant_translation,
36};
37use fedimint_connectors::ConnectorRegistry;
38use fedimint_core::config::FederationId;
39use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
40use fedimint_core::db::DatabaseTransaction;
41use fedimint_core::encoding::{Decodable, Encodable};
42use fedimint_core::module::{Amounts, ApiVersion, ModuleInit, MultiApiVersion};
43use fedimint_core::time::duration_since_epoch;
44use fedimint_core::util::{FmtCompact, SafeUrl, Spanned};
45use fedimint_core::{Amount, OutPoint, apply, async_trait_maybe_send, secp256k1};
46use fedimint_derive_secret::ChildId;
47use fedimint_lightning::{
48 InterceptPaymentRequest, InterceptPaymentResponse, LightningContext, LightningRpcError,
49 PayInvoiceResponse,
50};
51use fedimint_ln_client::api::LnFederationApi;
52use fedimint_ln_client::incoming::{
53 FundingOfferState, IncomingSmCommon, IncomingSmError, IncomingSmStates, IncomingStateMachine,
54};
55use fedimint_ln_client::pay::{PayInvoicePayload, PaymentData};
56use fedimint_ln_client::{
57 LightningClientContext, LightningClientInit, RealGatewayConnection,
58 create_incoming_contract_output,
59};
60use fedimint_ln_common::config::LightningClientConfig;
61use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
62use fedimint_ln_common::contracts::{ContractId, FundedContract, Preimage};
63use fedimint_ln_common::route_hints::RouteHint;
64use fedimint_ln_common::{
65 GatewayRegistrationAuth, KIND, LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA,
66 LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN, LightningCommonInit, LightningGateway,
67 LightningGatewayAnnouncement, LightningModuleTypes, LightningOutput, LightningOutputV0,
68 MissingInvoiceAmountError, PreimageAuth, RemoveGatewayRequest,
69 create_gateway_registration_message, create_gateway_remove_message,
70};
71use fedimint_lnv2_common::GatewayApi;
72use futures::StreamExt;
73use lightning_invoice::RoutingFees;
74use secp256k1::Keypair;
75use serde::{Deserialize, Serialize};
76use thiserror::Error;
77use tracing::{debug, error, info, warn};
78
79use self::complete::GatewayCompleteStateMachine;
80use self::pay::{
81 GatewayPayCancelContract, GatewayPayCommon, GatewayPayInvoice, GatewayPayStateMachine,
82 GatewayPayStates, OutgoingPaymentError,
83};
84pub use crate::error::{
85 GatewayClientV1Error, GatewayPayInvoiceError, HandleDirectSwapError, HandleInterceptedHtlcError,
86};
87
88pub const LNV1_HTLC_EXPIRY_SAFETY_MARGIN: u32 = LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN as u32;
97
98#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
101pub enum GatewayExtPayStates {
102 Created,
103 Preimage {
104 preimage: Preimage,
105 },
106 Success {
107 preimage: Preimage,
108 out_points: Vec<OutPoint>,
109 },
110 Canceled {
111 error: OutgoingPaymentError,
112 },
113 Fail {
114 error: OutgoingPaymentError,
115 error_message: String,
116 },
117 OfferDoesNotExist {
118 contract_id: ContractId,
119 },
120}
121
122#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
125pub enum GatewayExtReceiveStates {
126 Funding,
127 Preimage(Preimage),
128 RefundSuccess {
129 out_points: Vec<OutPoint>,
130 error: IncomingSmError,
131 },
132 RefundError {
133 error_message: String,
134 error: IncomingSmError,
135 },
136 FundingFailed {
137 error: IncomingSmError,
138 },
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub enum GatewayMeta {
143 Pay {
151 preimage_auth: sha256::Hash,
152 },
153 Receive,
154}
155
156#[derive(Debug, Clone)]
157pub struct GatewayClientInit {
158 pub federation_index: u64,
159 pub lightning_manager: Arc<dyn IGatewayClientV1>,
160}
161
162impl ModuleInit for GatewayClientInit {
163 type Common = LightningCommonInit;
164
165 async fn dump_database(
166 &self,
167 _dbtx: &mut DatabaseTransaction<'_>,
168 _prefix_names: Vec<String>,
169 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
170 Box::new(vec![].into_iter())
171 }
172}
173
174#[apply(async_trait_maybe_send!)]
175impl ClientModuleInit for GatewayClientInit {
176 type Module = GatewayClientModule;
177
178 fn supported_api_versions(&self) -> MultiApiVersion {
179 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
180 .expect("no version conflicts")
181 }
182
183 async fn init(
184 &self,
185 args: &ClientModuleInitArgs<Self>,
186 ) -> Result<Self::Module, ClientModuleError> {
187 Ok(GatewayClientModule {
188 cfg: args.cfg().clone(),
189 notifier: args.notifier().clone(),
190 redeem_key: args
191 .module_root_secret()
192 .child_key(ChildId(0))
193 .to_secp_key(&fedimint_core::secp256k1::Secp256k1::new()),
194 module_api: args.module_api().clone(),
195 federation_index: self.federation_index,
196 client_ctx: args.context(),
197 lightning_manager: self.lightning_manager.clone(),
198 connector_registry: args.connector_registry.clone(),
199 intercepted_htlc_lock_pool: lockable::LockPool::new(),
200 })
201 }
202}
203
204#[derive(Debug, Clone)]
205pub struct GatewayClientContext {
206 redeem_key: Keypair,
207 secp: Secp256k1<All>,
208 pub ln_decoder: Decoder,
209 notifier: ModuleNotifier<GatewayClientStateMachines>,
210 pub client_ctx: ClientContext<GatewayClientModule>,
211 pub lightning_manager: Arc<dyn IGatewayClientV1>,
212 pub connector_registry: ConnectorRegistry,
213}
214
215impl Context for GatewayClientContext {
216 const KIND: Option<ModuleKind> = Some(fedimint_ln_common::KIND);
217}
218
219impl From<&GatewayClientContext> for LightningClientContext {
220 fn from(ctx: &GatewayClientContext) -> Self {
221 let gateway_conn = RealGatewayConnection {
222 api: GatewayApi::new(None, ctx.connector_registry.clone()),
223 };
224 LightningClientContext {
225 ln_decoder: ctx.ln_decoder.clone(),
226 redeem_key: ctx.redeem_key,
227 gateway_conn: Arc::new(gateway_conn),
228 client_ctx: None,
229 }
230 }
231}
232
233pub struct GatewayClientModule {
238 cfg: LightningClientConfig,
239 pub notifier: ModuleNotifier<GatewayClientStateMachines>,
240 pub redeem_key: Keypair,
241 federation_index: u64,
242 module_api: DynModuleApi,
243 client_ctx: ClientContext<Self>,
244 pub lightning_manager: Arc<dyn IGatewayClientV1>,
245 connector_registry: ConnectorRegistry,
246 intercepted_htlc_lock_pool: lockable::LockPool<HtlcCircuitKey>,
247}
248
249impl fmt::Debug for GatewayClientModule {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 f.debug_struct("GatewayClientModule")
252 .finish_non_exhaustive()
253 }
254}
255
256impl ClientModule for GatewayClientModule {
257 type Init = LightningClientInit;
258 type Common = LightningModuleTypes;
259 type Backup = NoModuleBackup;
260 type ModuleStateMachineContext = GatewayClientContext;
261 type States = GatewayClientStateMachines;
262
263 fn context(&self) -> Self::ModuleStateMachineContext {
264 Self::ModuleStateMachineContext {
265 redeem_key: self.redeem_key,
266 secp: Secp256k1::new(),
267 ln_decoder: self.decoder(),
268 notifier: self.notifier.clone(),
269 client_ctx: self.client_ctx.clone(),
270 lightning_manager: self.lightning_manager.clone(),
271 connector_registry: self.connector_registry.clone(),
272 }
273 }
274
275 fn input_fee(
276 &self,
277 _amount: &Amounts,
278 _input: &<Self::Common as fedimint_core::module::ModuleCommon>::Input,
279 ) -> Option<Amounts> {
280 Some(Amounts::new_bitcoin(self.cfg.fee_consensus.contract_input))
281 }
282
283 fn output_fee(
284 &self,
285 _amount: &Amounts,
286 output: &<Self::Common as fedimint_core::module::ModuleCommon>::Output,
287 ) -> Option<Amounts> {
288 match output.maybe_v0_ref()? {
289 LightningOutputV0::Contract(_) => {
290 Some(Amounts::new_bitcoin(self.cfg.fee_consensus.contract_output))
291 }
292 LightningOutputV0::Offer(_) | LightningOutputV0::CancelOutgoing { .. } => {
293 Some(Amounts::ZERO)
294 }
295 }
296 }
297}
298
299impl GatewayClientModule {
300 fn to_gateway_registration_info(
301 &self,
302 route_hints: Vec<RouteHint>,
303 ttl: Duration,
304 fees: RoutingFees,
305 lightning_context: LightningContext,
306 api: SafeUrl,
307 gateway_keypair: Keypair,
308 ) -> LightningGatewayAnnouncement {
309 let info = LightningGateway {
310 federation_index: self.federation_index,
311 gateway_redeem_key: self.redeem_key.public_key(),
312 node_pub_key: lightning_context.lightning_public_key,
313 lightning_alias: lightning_context.lightning_alias,
314 api,
315 route_hints,
316 fees,
317 gateway_id: gateway_keypair.public_key(),
318 supports_private_payments: lightning_context.lnrpc.supports_private_payments(),
319 };
320
321 let nonce = u64::try_from(duration_since_epoch().as_millis())
327 .expect("milliseconds since the epoch do not exceed u64 for another 500m years");
328 let signature = gateway_keypair.sign_schnorr(create_gateway_registration_message(
329 self.cfg.threshold_pub_key,
330 nonce,
331 &info,
332 ));
333
334 LightningGatewayAnnouncement {
335 info,
336 ttl,
337 vetted: false,
338 auth: Some(GatewayRegistrationAuth { nonce, signature }),
339 }
340 }
341
342 async fn create_funding_incoming_contract_output_from_htlc(
343 &self,
344 htlc: Htlc,
345 ) -> Result<
346 (
347 OperationId,
348 Amount,
349 ClientOutput<LightningOutputV0>,
350 ClientOutputSM<GatewayClientStateMachines>,
351 ContractId,
352 ),
353 IncomingSmError,
354 > {
355 let operation_id = OperationId(htlc.payment_hash.to_byte_array());
356 let (incoming_output, amount, contract_id) = create_incoming_contract_output(
365 &self.module_api,
366 htlc.payment_hash,
367 htlc.incoming_amount_msat,
368 &self.redeem_key,
369 )
370 .await?;
371
372 let client_output = ClientOutput::<LightningOutputV0> {
373 output: incoming_output,
374 amounts: Amounts::new_bitcoin(amount),
375 };
376 let client_output_sm = ClientOutputSM::<GatewayClientStateMachines> {
377 state_machines: Arc::new(move |out_point_range: OutPointRange| {
378 assert_eq!(out_point_range.count(), 1);
379 vec![
380 GatewayClientStateMachines::Receive(IncomingStateMachine {
381 common: IncomingSmCommon {
382 operation_id,
383 contract_id,
384 payment_hash: htlc.payment_hash,
385 },
386 state: IncomingSmStates::FundingOffer(FundingOfferState {
387 txid: out_point_range.txid(),
388 }),
389 }),
390 GatewayClientStateMachines::Complete(GatewayCompleteStateMachine {
391 common: GatewayCompleteCommon {
392 operation_id,
393 payment_hash: htlc.payment_hash,
394 incoming_chan_id: htlc.incoming_chan_id,
395 htlc_id: htlc.htlc_id,
396 },
397 state: GatewayCompleteStates::WaitForPreimage(WaitForPreimageState),
398 }),
399 ]
400 }),
401 };
402 Ok((
403 operation_id,
404 amount,
405 client_output,
406 client_output_sm,
407 contract_id,
408 ))
409 }
410
411 async fn create_funding_incoming_contract_output_from_swap(
412 &self,
413 swap: SwapParameters,
414 ) -> Result<
415 (
416 OperationId,
417 ClientOutput<LightningOutputV0>,
418 ClientOutputSM<GatewayClientStateMachines>,
419 ),
420 IncomingSmError,
421 > {
422 let payment_hash = swap.payment_hash;
423 let operation_id = OperationId(payment_hash.to_byte_array());
424 let (incoming_output, amount, contract_id) = create_incoming_contract_output(
425 &self.module_api,
426 payment_hash,
427 swap.amount_msat,
428 &self.redeem_key,
429 )
430 .await?;
431
432 let client_output = ClientOutput::<LightningOutputV0> {
433 output: incoming_output,
434 amounts: Amounts::new_bitcoin(amount),
435 };
436 let client_output_sm = ClientOutputSM::<GatewayClientStateMachines> {
437 state_machines: Arc::new(move |out_point_range| {
438 assert_eq!(out_point_range.count(), 1);
439 vec![GatewayClientStateMachines::Receive(IncomingStateMachine {
440 common: IncomingSmCommon {
441 operation_id,
442 contract_id,
443 payment_hash,
444 },
445 state: IncomingSmStates::FundingOffer(FundingOfferState {
446 txid: out_point_range.txid(),
447 }),
448 })]
449 }),
450 };
451 Ok((operation_id, client_output, client_output_sm))
452 }
453
454 pub async fn try_register_with_federation(
460 &self,
461 route_hints: Vec<RouteHint>,
462 time_to_live: Duration,
463 fees: RoutingFees,
464 lightning_context: LightningContext,
465 api: SafeUrl,
466 gateway_keypair: Keypair,
467 ) -> bool {
468 let registration_info = self.to_gateway_registration_info(
469 route_hints,
470 time_to_live,
471 fees,
472 lightning_context,
473 api,
474 gateway_keypair,
475 );
476 let gateway_id = registration_info.info.gateway_id;
477
478 let federation_id = self
479 .client_ctx
480 .get_config()
481 .await
482 .global
483 .calculate_federation_id();
484 match self.module_api.register_gateway(®istration_info).await {
485 Err(e) => {
486 warn!(
487 e = %e.fmt_compact(),
488 "Failed to register gateway {gateway_id} with federation {federation_id}"
489 );
490 false
491 }
492 _ => {
493 info!(
494 "Successfully registered gateway {gateway_id} with federation {federation_id}"
495 );
496 true
497 }
498 }
499 }
500
501 pub async fn remove_from_federation(&self, gateway_keypair: Keypair) {
509 let gateway_id = gateway_keypair.public_key();
510 let challenges = self
511 .module_api
512 .get_remove_gateway_challenge(gateway_id)
513 .await;
514
515 let fed_public_key = self.cfg.threshold_pub_key;
516 let signatures = challenges
517 .into_iter()
518 .filter_map(|(peer_id, challenge)| {
519 let msg = create_gateway_remove_message(fed_public_key, peer_id, challenge?);
520 let signature = gateway_keypair.sign_schnorr(msg);
521 Some((peer_id, signature))
522 })
523 .collect::<BTreeMap<_, _>>();
524
525 let remove_gateway_request = RemoveGatewayRequest {
526 gateway_id,
527 signatures,
528 };
529
530 self.module_api.remove_gateway(remove_gateway_request).await;
531 }
532
533 pub async fn gateway_handle_intercepted_htlc(
561 &self,
562 htlc: Htlc,
563 current_block_height: impl Future<Output = Result<u32, LightningRpcError>>,
564 ) -> Result<OperationId, HandleInterceptedHtlcError> {
565 debug!("Handling intercepted HTLC {htlc:?}");
566
567 let operation_id = OperationId(htlc.payment_hash.to_byte_array());
568 let circuit_key = HtlcCircuitKey {
569 operation_id,
570 incoming_chan_id: htlc.incoming_chan_id,
571 htlc_id: htlc.htlc_id,
572 };
573
574 let _circuit_lock_guard = self
577 .intercepted_htlc_lock_pool
578 .async_lock(circuit_key)
579 .await;
580
581 let replay_of_active_circuit = self
584 .client_ctx
585 .get_own_operation_active_states(operation_id)
586 .await
587 .into_iter()
588 .any(|(state, _)| circuit_key.matches_state(&state));
589 if replay_of_active_circuit {
590 debug!(
591 ?operation_id,
592 incoming_chan_id = htlc.incoming_chan_id,
593 htlc_id = htlc.htlc_id,
594 "HTLC circuit already being handled by an active completion state machine, treating as in-flight (likely an LND stream-reconnect replay)"
595 );
596 return Ok(operation_id);
597 }
598 let replay_of_inactive_circuit = self
599 .client_ctx
600 .get_own_operation_inactive_states(operation_id)
601 .await
602 .into_iter()
603 .any(|(state, _)| circuit_key.matches_state(&state));
604 if replay_of_inactive_circuit {
605 debug!(
606 ?operation_id,
607 incoming_chan_id = htlc.incoming_chan_id,
608 htlc_id = htlc.htlc_id,
609 "HTLC circuit was already handled by a completion state machine, treating as idempotent replay"
610 );
611 return Ok(operation_id);
612 }
613
614 let current_block_height = current_block_height
615 .await
616 .map_err(HandleInterceptedHtlcError::BlockHeight)?;
617 htlc.ensure_safe_expiry(current_block_height)?;
618 let remaining_blocks = htlc.incoming_expiry.saturating_sub(current_block_height);
619 if remaining_blocks <= u32::from(LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA) {
620 warn!(
624 payment_hash = %htlc.payment_hash,
625 remaining_blocks,
626 advertised_delta = LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA,
627 "Accepting LNv1 HTLC with less remaining expiry than newly created invoices advertise, likely paid to a pre-upgrade invoice"
628 );
629 }
630
631 let (op_id_from_funding, amount, client_output, client_output_sm, contract_id) = self
632 .create_funding_incoming_contract_output_from_htlc(htlc.clone())
633 .await?;
634 if op_id_from_funding != operation_id {
637 return Err(HandleInterceptedHtlcError::OperationIdMismatch {
638 expected: operation_id,
639 derived: op_id_from_funding,
640 });
641 }
642
643 let output = ClientOutput {
644 output: LightningOutput::V0(client_output.output),
645 amounts: Amounts::new_bitcoin(amount),
646 };
647
648 let tx = TransactionBuilder::new().with_outputs(self.client_ctx.make_client_outputs(
649 ClientOutputBundle::new(vec![output], vec![client_output_sm]),
650 ));
651 let operation_meta_gen = |_: OutPointRange| GatewayMeta::Receive;
652 self.client_ctx
653 .finalize_and_submit_transaction(operation_id, KIND.as_str(), operation_meta_gen, tx)
654 .await?;
655 debug!(?operation_id, "Submitted transaction for HTLC {htlc:?}");
656 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
657 self.client_ctx
658 .log_event(
659 &mut dbtx,
660 IncomingPaymentStarted {
661 contract_id,
662 payment_hash: htlc.payment_hash,
663 invoice_amount: htlc.incoming_amount_msat,
664 contract_amount: amount,
665 operation_id,
666 },
667 )
668 .await;
669 dbtx.commit_tx().await;
670 Ok(operation_id)
671 }
672
673 pub async fn gateway_handle_direct_swap(
700 &self,
701 swap_params: SwapParameters,
702 allow_fresh_dispatch: bool,
703 ) -> Result<Option<OperationId>, HandleDirectSwapError> {
704 debug!("Handling direct swap {swap_params:?}");
705
706 let payment_hash = swap_params.payment_hash;
707 let operation_id = OperationId(payment_hash.to_byte_array());
708
709 if self.client_ctx.operation_exists(operation_id).await {
714 debug!(
715 operation_id = %operation_id.fmt_short(),
716 %payment_hash,
717 "Direct swap already in progress, returning the operation already funding it"
718 );
719
720 return Ok(Some(operation_id));
721 }
722
723 if !allow_fresh_dispatch {
724 return Ok(None);
725 }
726
727 let (op_id_from_funding, client_output, client_output_sm) = self
728 .create_funding_incoming_contract_output_from_swap(swap_params.clone())
729 .await?;
730 if op_id_from_funding != operation_id {
732 return Err(HandleDirectSwapError::OperationIdMismatch {
733 expected: operation_id,
734 derived: op_id_from_funding,
735 });
736 }
737
738 self.client_ctx
739 .module_db()
740 .autocommit(
741 |dbtx, _| {
742 let client_output = client_output.clone();
743 let client_output_sm = client_output_sm.clone();
744 Box::pin(async move {
745 if self
750 .client_ctx
751 .get_operation_dbtx(dbtx, operation_id)
752 .await
753 .is_some()
754 {
755 debug!(
756 operation_id = %operation_id.fmt_short(),
757 %payment_hash,
758 "Concurrent direct swap won the race, returning the operation already funding it"
759 );
760
761 return Ok(Some(operation_id));
762 }
763
764 let output = ClientOutput {
765 output: LightningOutput::V0(client_output.output),
766 amounts: client_output.amounts,
767 };
768 let tx = TransactionBuilder::new().with_outputs(
769 self.client_ctx.make_client_outputs(ClientOutputBundle::new(
770 vec![output],
771 vec![client_output_sm],
772 )),
773 );
774
775 self.client_ctx
776 .finalize_and_submit_transaction_dbtx(
777 dbtx,
778 operation_id,
779 KIND.as_str(),
780 |_: OutPointRange| GatewayMeta::Receive,
781 tx,
782 )
783 .await?;
784
785 debug!(
786 ?operation_id,
787 %payment_hash,
788 "Submitted funding transaction for direct swap"
789 );
790
791 Ok::<_, HandleDirectSwapError>(Some(operation_id))
792 })
793 },
794 Some(100),
795 )
796 .await
797 .map_err(HandleDirectSwapError::from)
798 }
799
800 pub async fn gateway_subscribe_ln_receive(
808 &self,
809 operation_id: OperationId,
810 ) -> Result<UpdateStreamOrOutcome<GatewayExtReceiveStates>, OperationLookupError> {
811 let operation = self.client_ctx.get_operation(operation_id).await?;
812 let mut stream = self.notifier.subscribe(operation_id).await;
813 let client_ctx = self.client_ctx.clone();
814
815 Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
816 GatewayExtReceiveStates::Funding => false,
817 GatewayExtReceiveStates::Preimage(_)
818 | GatewayExtReceiveStates::RefundSuccess { .. }
819 | GatewayExtReceiveStates::RefundError { .. }
820 | GatewayExtReceiveStates::FundingFailed { .. } => true,
821 }, move || {
822 stream! {
823
824 yield GatewayExtReceiveStates::Funding;
825
826 let state = loop {
827 debug!("Getting next ln receive state for {}", operation_id.fmt_short());
828 if let Some(GatewayClientStateMachines::Receive(state)) = stream.next().await {
829 match state.state {
830 IncomingSmStates::Preimage(preimage) =>{
831 debug!(?operation_id, "Received preimage");
832 break GatewayExtReceiveStates::Preimage(preimage)
833 },
834 IncomingSmStates::RefundSubmitted { out_points, error } => {
835 debug!(?operation_id, "Refund submitted for {out_points:?} {error}");
836 match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
837 Ok(()) => {
838 debug!(?operation_id, "Refund success");
839 break GatewayExtReceiveStates::RefundSuccess { out_points, error }
840 },
841 Err(e) => {
842 warn!(?operation_id, "Got failure {e:?} while awaiting for refund outputs {out_points:?}");
843 break GatewayExtReceiveStates::RefundError{ error_message: e.fmt_compact().to_string(), error }
844 },
845 }
846 },
847 IncomingSmStates::FundingFailed { error } => {
848 warn!(?operation_id, "Funding failed: {error:?}");
849 break GatewayExtReceiveStates::FundingFailed{ error }
850 },
851 other => {
852 debug!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
853 }
854 }
855 }
856 };
857 yield state;
858 }
859 }))
860 }
861
862 pub async fn await_completion(&self, operation_id: OperationId) {
865 let mut stream = self.notifier.subscribe(operation_id).await;
866 loop {
867 match stream.next().await {
868 Some(GatewayClientStateMachines::Complete(state)) => match state.state {
869 GatewayCompleteStates::HtlcFinished => {
870 info!(%state, "LNv1 completion state machine finished");
871 return;
872 }
873 GatewayCompleteStates::Failure => {
874 error!(%state, "LNv1 completion state machine failed");
875 return;
876 }
877 _ => {
878 info!(%state, "Waiting for LNv1 completion state machine");
879 continue;
880 }
881 },
882 Some(GatewayClientStateMachines::Receive(state)) => {
883 info!(%state, "Waiting for LNv1 completion state machine");
884 continue;
885 }
886 Some(state) => {
887 warn!(%state, "Operation is not an LNv1 completion state machine");
888 return;
889 }
890 None => return,
891 }
892 }
893 }
894
895 pub async fn gateway_pay_bolt11_invoice(
917 &self,
918 pay_invoice_payload: PayInvoicePayload,
919 ) -> Result<OperationId, GatewayPayInvoiceError> {
920 let payload = pay_invoice_payload.clone();
921
922 let invoice_amount = pay_invoice_payload
928 .payment_data
929 .amount()
930 .ok_or(GatewayPayInvoiceError::MissingInvoiceAmount)?;
931
932 self.lightning_manager
933 .verify_pruned_invoice(pay_invoice_payload.payment_data)
934 .await
935 .map_err(GatewayPayInvoiceError::PrunedInvoiceRejected)?;
936
937 let initial_state = if self
942 .lightning_manager
943 .claim_payment_image(
944 &fedimint_lnv2_common::contracts::PaymentImage::Hash(
945 payload.payment_data.payment_hash(),
946 ),
947 OperationId(payload.contract_id.to_byte_array()),
948 )
949 .await
950 {
951 GatewayPayStates::PayInvoice(GatewayPayInvoice {
952 pay_invoice_payload: payload.clone(),
953 })
954 } else {
955 let account = self
956 .module_api
957 .fetch_contract(payload.contract_id)
958 .await
959 .map_err(GatewayPayInvoiceError::ContractLookup)?
960 .ok_or(GatewayPayInvoiceError::PaymentHashAlreadyClaimed)?;
961 let FundedContract::Outgoing(contract) = account.contract else {
962 return Err(GatewayPayInvoiceError::PaymentHashAlreadyClaimed);
963 };
964
965 if account.amount == Amount::ZERO
966 || contract.gateway_key != self.redeem_key.public_key()
967 || contract.hash != payload.payment_data.payment_hash()
968 {
969 return Err(GatewayPayInvoiceError::PaymentHashAlreadyClaimed);
970 }
971 GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract::for_claimed_hash(
972 OutgoingContractAccount {
973 amount: account.amount,
974 contract,
975 },
976 )))
977 };
978
979 self.client_ctx.module_db()
980 .autocommit(
981 |dbtx, _| {
982 Box::pin(async {
983 let operation_id = OperationId(payload.contract_id.to_byte_array());
984
985 if let Some(entry) =
993 self.client_ctx.get_operation_dbtx(dbtx, operation_id).await
994 {
995 if !matches!(
1003 entry.try_meta::<GatewayMeta>(),
1004 Ok(GatewayMeta::Pay { preimage_auth })
1005 if PreimageAuth::new(preimage_auth)
1006 .verifies(payload.preimage_auth)
1007 ) {
1008 return Err(GatewayPayInvoiceError::Unauthorized {
1009 contract_id: payload.contract_id,
1010 });
1011 }
1012
1013 debug!(
1014 operation_id = %operation_id.fmt_short(),
1015 contract_id = %payload.contract_id,
1016 "Duplicate request to pay an outgoing contract, returning the operation already in progress"
1017 );
1018
1019 return Ok(operation_id);
1020 }
1021
1022 self.client_ctx.log_event(dbtx, OutgoingPaymentStarted {
1023 contract_id: payload.contract_id,
1024 invoice_amount,
1025 operation_id,
1026 }).await;
1027
1028 let state_machines =
1029 vec![GatewayClientStateMachines::Pay(GatewayPayStateMachine {
1030 common: GatewayPayCommon { operation_id },
1031 state: initial_state.clone(),
1032 })];
1033
1034 let dyn_states = state_machines
1035 .into_iter()
1036 .map(|s| self.client_ctx.make_dyn(s))
1037 .collect();
1038
1039 match self.client_ctx.add_state_machines_dbtx(dbtx, dyn_states).await {
1040 Ok(()) => {
1041 self.client_ctx
1042 .add_operation_log_entry_dbtx(
1043 dbtx,
1044 operation_id,
1045 KIND.as_str(),
1046 GatewayMeta::Pay {
1047 preimage_auth: payload.preimage_auth,
1048 },
1049 )
1050 .await;
1051 }
1052 Err(AddStateMachinesError::StateAlreadyExists) => {
1053 info!("State machine for operation {} already exists, will not add a new one", operation_id.fmt_short());
1054 }
1055 Err(other) => {
1056 return Err(GatewayPayInvoiceError::StateMachines(other));
1057 }
1058 }
1059 Ok::<_, GatewayPayInvoiceError>(operation_id)
1060 })
1061 },
1062 Some(100),
1063 )
1064 .await
1065 .map_err(GatewayPayInvoiceError::from)
1066 }
1067
1068 pub async fn gateway_subscribe_ln_pay(
1076 &self,
1077 operation_id: OperationId,
1078 ) -> Result<UpdateStreamOrOutcome<GatewayExtPayStates>, OperationLookupError> {
1079 let mut stream = self.notifier.subscribe(operation_id).await;
1080 let operation = self.client_ctx.get_operation(operation_id).await?;
1081 let client_ctx = self.client_ctx.clone();
1082
1083 Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
1084 GatewayExtPayStates::Created | GatewayExtPayStates::Preimage { .. } => false,
1085 GatewayExtPayStates::Success { .. }
1086 | GatewayExtPayStates::Canceled { .. }
1087 | GatewayExtPayStates::Fail { .. }
1088 | GatewayExtPayStates::OfferDoesNotExist { .. } => true,
1089 }, move || {
1090 stream! {
1091 yield GatewayExtPayStates::Created;
1092
1093 loop {
1094 debug!("Getting next ln pay state for {}", operation_id.fmt_short());
1095 match stream.next().await { Some(GatewayClientStateMachines::Pay(state)) => {
1096 match state.state {
1097 GatewayPayStates::Preimage(out_points, preimage) => {
1098 yield GatewayExtPayStates::Preimage{ preimage: preimage.clone() };
1099
1100 match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
1101 Ok(()) => {
1102 debug!(?operation_id, "Success");
1103 yield GatewayExtPayStates::Success{ preimage: preimage.clone(), out_points };
1104 return;
1105
1106 }
1107 Err(e) => {
1108 warn!(?operation_id, "Got failure {e:?} while awaiting for outputs {out_points:?}");
1109 }
1111 }
1112 }
1113 GatewayPayStates::Canceled { txid, contract_id, error } => {
1114 debug!(?operation_id, "Trying to cancel contract {contract_id:?} due to {error:?}");
1115 match client_ctx.transaction_updates(operation_id).await.await_tx_accepted(txid).await {
1116 Ok(()) => {
1117 debug!(?operation_id, "Canceled contract {contract_id:?} due to {error:?}");
1118 yield GatewayExtPayStates::Canceled{ error };
1119 return;
1120 }
1121 Err(e) => {
1122 warn!(?operation_id, "Got failure {e:?} while awaiting for transaction {txid} to be accepted for");
1123 yield GatewayExtPayStates::Fail { error, error_message: format!("Refund transaction {txid} was not accepted by the federation. OperationId: {} Error: {e:?}", operation_id.fmt_short()) };
1124 }
1125 }
1126 }
1127 GatewayPayStates::OfferDoesNotExist(contract_id) => {
1128 warn!("Yielding OfferDoesNotExist state for {} and contract {contract_id}", operation_id.fmt_short());
1129 yield GatewayExtPayStates::OfferDoesNotExist { contract_id };
1130 }
1131 GatewayPayStates::Failed{ error, error_message } => {
1132 warn!("Yielding Fail state for {} due to {error:?} {error_message:?}", operation_id.fmt_short());
1133 yield GatewayExtPayStates::Fail{ error, error_message };
1134 },
1135 GatewayPayStates::PayInvoice(_) => {
1136 debug!("Got initial state PayInvoice while awaiting for output of {}", operation_id.fmt_short());
1137 }
1138 other => {
1139 info!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
1140 }
1141 }
1142 } _ => {
1143 warn!("Got None while getting next ln pay state for {}", operation_id.fmt_short());
1144 }}
1145 }
1146 }
1147 }))
1148 }
1149}
1150
1151#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
1152struct HtlcCircuitKey {
1153 operation_id: OperationId,
1154 incoming_chan_id: u64,
1155 htlc_id: u64,
1156}
1157
1158impl HtlcCircuitKey {
1159 fn matches_state(self, state: &GatewayClientStateMachines) -> bool {
1160 matches!(
1161 state,
1162 GatewayClientStateMachines::Complete(sm)
1163 if sm.common.operation_id == self.operation_id
1164 && sm.common.incoming_chan_id == self.incoming_chan_id
1165 && sm.common.htlc_id == self.htlc_id
1166 )
1167 }
1168}
1169
1170#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1171pub enum GatewayClientStateMachines {
1172 Pay(GatewayPayStateMachine),
1173 Receive(IncomingStateMachine),
1174 Complete(GatewayCompleteStateMachine),
1175}
1176
1177impl fmt::Display for GatewayClientStateMachines {
1178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1179 match self {
1180 GatewayClientStateMachines::Pay(pay) => {
1181 write!(f, "{pay}")
1182 }
1183 GatewayClientStateMachines::Receive(receive) => {
1184 write!(f, "{receive}")
1185 }
1186 GatewayClientStateMachines::Complete(complete) => {
1187 write!(f, "{complete}")
1188 }
1189 }
1190 }
1191}
1192
1193impl IntoDynInstance for GatewayClientStateMachines {
1194 type DynType = DynState;
1195
1196 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1197 DynState::from_typed(instance_id, self)
1198 }
1199}
1200
1201impl State for GatewayClientStateMachines {
1202 type ModuleContext = GatewayClientContext;
1203
1204 fn transitions(
1205 &self,
1206 context: &Self::ModuleContext,
1207 global_context: &DynGlobalClientContext,
1208 ) -> Vec<StateTransition<Self>> {
1209 match self {
1210 GatewayClientStateMachines::Pay(pay_state) => {
1211 sm_enum_variant_translation!(
1212 pay_state.transitions(context, global_context),
1213 GatewayClientStateMachines::Pay
1214 )
1215 }
1216 GatewayClientStateMachines::Receive(receive_state) => {
1217 sm_enum_variant_translation!(
1218 receive_state.transitions(&context.into(), global_context),
1219 GatewayClientStateMachines::Receive
1220 )
1221 }
1222 GatewayClientStateMachines::Complete(complete_state) => {
1223 sm_enum_variant_translation!(
1224 complete_state.transitions(context, global_context),
1225 GatewayClientStateMachines::Complete
1226 )
1227 }
1228 }
1229 }
1230
1231 fn operation_id(&self) -> fedimint_core::core::OperationId {
1232 match self {
1233 GatewayClientStateMachines::Pay(pay_state) => pay_state.operation_id(),
1234 GatewayClientStateMachines::Receive(receive_state) => receive_state.operation_id(),
1235 GatewayClientStateMachines::Complete(complete_state) => complete_state.operation_id(),
1236 }
1237 }
1238}
1239
1240#[derive(Debug, Clone, Eq, PartialEq)]
1241pub struct Htlc {
1242 pub payment_hash: sha256::Hash,
1244 pub incoming_amount_msat: Amount,
1246 pub outgoing_amount_msat: Amount,
1248 pub incoming_expiry: u32,
1253 pub short_channel_id: Option<u64>,
1255 pub incoming_chan_id: u64,
1257 pub htlc_id: u64,
1259}
1260
1261#[derive(Debug, Error, Clone, Eq, PartialEq)]
1263#[error(
1264 "incoming HTLC expiry is unsafe: expiry {incoming_expiry}, current height {current_block_height}, required remaining blocks greater than {expiry_safety_margin}"
1265)]
1266pub struct UnsafeHtlcExpiry {
1267 pub incoming_expiry: u32,
1269 pub current_block_height: u32,
1271 pub expiry_safety_margin: u32,
1273}
1274
1275impl Htlc {
1276 pub fn ensure_safe_expiry(&self, current_block_height: u32) -> Result<(), UnsafeHtlcExpiry> {
1279 let remaining_blocks = self.incoming_expiry.saturating_sub(current_block_height);
1280 if LNV1_HTLC_EXPIRY_SAFETY_MARGIN < remaining_blocks {
1281 return Ok(());
1282 }
1283
1284 Err(UnsafeHtlcExpiry {
1285 incoming_expiry: self.incoming_expiry,
1286 current_block_height,
1287 expiry_safety_margin: LNV1_HTLC_EXPIRY_SAFETY_MARGIN,
1288 })
1289 }
1290}
1291
1292impl From<InterceptPaymentRequest> for Htlc {
1293 fn from(s: InterceptPaymentRequest) -> Self {
1294 Self {
1295 payment_hash: s.payment_hash,
1296 incoming_amount_msat: Amount::from_msats(s.incoming_amount_msat),
1301 outgoing_amount_msat: Amount::from_msats(s.amount_msat),
1302 incoming_expiry: s.expiry,
1303 short_channel_id: s.short_channel_id,
1304 incoming_chan_id: s.incoming_chan_id,
1305 htlc_id: s.htlc_id,
1306 }
1307 }
1308}
1309
1310#[derive(Debug, Clone)]
1311pub struct SwapParameters {
1312 pub payment_hash: sha256::Hash,
1313 pub amount_msat: Amount,
1314}
1315
1316impl TryFrom<PaymentData> for SwapParameters {
1317 type Error = MissingInvoiceAmountError;
1318
1319 fn try_from(s: PaymentData) -> Result<Self, Self::Error> {
1320 let payment_hash = s.payment_hash();
1321 let amount_msat = s.amount().ok_or(MissingInvoiceAmountError)?;
1322 Ok(Self {
1323 payment_hash,
1324 amount_msat,
1325 })
1326 }
1327}
1328
1329#[async_trait]
1335pub trait IGatewayClientV1: Debug + Send + Sync {
1336 async fn verify_preimage_authentication(
1342 &self,
1343 payment_hash: sha256::Hash,
1344 preimage_auth: sha256::Hash,
1345 contract: OutgoingContractAccount,
1346 ) -> Result<(), OutgoingPaymentError>;
1347
1348 async fn verify_pruned_invoice(
1356 &self,
1357 payment_data: PaymentData,
1358 ) -> Result<(), GatewayClientV1Error>;
1359
1360 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees>;
1362
1363 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>>;
1366
1367 async fn get_client_for_invoice(
1375 &self,
1376 payment_data: PaymentData,
1377 ) -> Option<Spanned<ClientHandleArc>>;
1378
1379 async fn pay(
1381 &self,
1382 payment_data: PaymentData,
1383 max_delay: u64,
1384 max_fee: Amount,
1385 ) -> Result<PayInvoiceResponse, LightningRpcError>;
1386
1387 async fn outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool;
1398
1399 async fn complete_htlc(
1411 &self,
1412 htlc_response: InterceptPaymentResponse,
1413 ) -> Result<(), LightningRpcError>;
1414
1415 async fn is_lnv2_direct_swap(
1424 &self,
1425 payment_hash: sha256::Hash,
1426 amount: Amount,
1427 ) -> Result<
1428 Option<(
1429 fedimint_lnv2_common::contracts::IncomingContract,
1430 ClientHandleArc,
1431 )>,
1432 GatewayClientV1Error,
1433 >;
1434
1435 async fn claim_payment_image(
1438 &self,
1439 payment_image: &fedimint_lnv2_common::contracts::PaymentImage,
1440 operation_id: OperationId,
1441 ) -> bool;
1442}