1mod complete;
2pub mod events;
3pub mod pay;
4#[cfg(test)]
5mod tests;
6
7use std::collections::BTreeMap;
8use std::fmt;
9use std::fmt::Debug;
10use std::future::Future;
11use std::sync::Arc;
12use std::time::Duration;
13
14use async_stream::stream;
15use async_trait::async_trait;
16use bitcoin::hashes::{Hash, sha256};
17use bitcoin::key::Secp256k1;
18use bitcoin::secp256k1::All;
19use complete::{GatewayCompleteCommon, GatewayCompleteStates, WaitForPreimageState};
20use events::{IncomingPaymentStarted, OutgoingPaymentStarted};
21use fedimint_api_client::api::DynModuleApi;
22use fedimint_client::ClientHandleArc;
23use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
24use fedimint_client_module::module::recovery::NoModuleBackup;
25use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
26use fedimint_client_module::oplog::UpdateStreamOrOutcome;
27use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
28use fedimint_client_module::transaction::{
29 ClientOutput, ClientOutputBundle, ClientOutputSM, TransactionBuilder,
30};
31use fedimint_client_module::{
32 AddStateMachinesError, DynGlobalClientContext, sm_enum_variant_translation,
33};
34use fedimint_connectors::ConnectorRegistry;
35use fedimint_core::config::FederationId;
36use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
37use fedimint_core::db::{AutocommitError, DatabaseTransaction};
38use fedimint_core::encoding::{Decodable, Encodable};
39use fedimint_core::module::{Amounts, ApiVersion, ModuleInit, MultiApiVersion};
40use fedimint_core::time::duration_since_epoch;
41use fedimint_core::util::{FmtCompact, SafeUrl, Spanned};
42use fedimint_core::{Amount, OutPoint, apply, async_trait_maybe_send, secp256k1};
43use fedimint_derive_secret::ChildId;
44use fedimint_lightning::{
45 InterceptPaymentRequest, InterceptPaymentResponse, LightningContext, LightningRpcError,
46 PayInvoiceResponse,
47};
48use fedimint_ln_client::api::LnFederationApi;
49use fedimint_ln_client::incoming::{
50 FundingOfferState, IncomingSmCommon, IncomingSmError, IncomingSmStates, IncomingStateMachine,
51};
52use fedimint_ln_client::pay::{PayInvoicePayload, PaymentData};
53use fedimint_ln_client::{
54 LightningClientContext, LightningClientInit, RealGatewayConnection,
55 create_incoming_contract_output,
56};
57use fedimint_ln_common::config::LightningClientConfig;
58use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
59use fedimint_ln_common::contracts::{ContractId, Preimage};
60use fedimint_ln_common::route_hints::RouteHint;
61use fedimint_ln_common::{
62 GatewayRegistrationAuth, KIND, LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA,
63 LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN, LightningCommonInit, LightningGateway,
64 LightningGatewayAnnouncement, LightningModuleTypes, LightningOutput, LightningOutputV0,
65 RemoveGatewayRequest, create_gateway_registration_message, create_gateway_remove_message,
66};
67use fedimint_lnv2_common::GatewayApi;
68use futures::StreamExt;
69use lightning_invoice::RoutingFees;
70use secp256k1::Keypair;
71use serde::{Deserialize, Serialize};
72use thiserror::Error;
73use tracing::{debug, error, info, warn};
74
75use self::complete::GatewayCompleteStateMachine;
76use self::pay::{
77 GatewayPayCommon, GatewayPayInvoice, GatewayPayStateMachine, GatewayPayStates,
78 OutgoingContractError, OutgoingPaymentError,
79};
80
81pub const LNV1_HTLC_EXPIRY_SAFETY_MARGIN: u32 = LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN as u32;
90
91#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
94pub enum GatewayExtPayStates {
95 Created,
96 Preimage {
97 preimage: Preimage,
98 },
99 Success {
100 preimage: Preimage,
101 out_points: Vec<OutPoint>,
102 },
103 Canceled {
104 error: OutgoingPaymentError,
105 },
106 Fail {
107 error: OutgoingPaymentError,
108 error_message: String,
109 },
110 OfferDoesNotExist {
111 contract_id: ContractId,
112 },
113}
114
115#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
118pub enum GatewayExtReceiveStates {
119 Funding,
120 Preimage(Preimage),
121 RefundSuccess {
122 out_points: Vec<OutPoint>,
123 error: IncomingSmError,
124 },
125 RefundError {
126 error_message: String,
127 error: IncomingSmError,
128 },
129 FundingFailed {
130 error: IncomingSmError,
131 },
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub enum GatewayMeta {
136 Pay {
144 preimage_auth: sha256::Hash,
145 },
146 Receive,
147}
148
149#[derive(Debug, Clone)]
150pub struct GatewayClientInit {
151 pub federation_index: u64,
152 pub lightning_manager: Arc<dyn IGatewayClientV1>,
153}
154
155impl ModuleInit for GatewayClientInit {
156 type Common = LightningCommonInit;
157
158 async fn dump_database(
159 &self,
160 _dbtx: &mut DatabaseTransaction<'_>,
161 _prefix_names: Vec<String>,
162 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
163 Box::new(vec![].into_iter())
164 }
165}
166
167#[apply(async_trait_maybe_send!)]
168impl ClientModuleInit for GatewayClientInit {
169 type Module = GatewayClientModule;
170
171 fn supported_api_versions(&self) -> MultiApiVersion {
172 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
173 .expect("no version conflicts")
174 }
175
176 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
177 Ok(GatewayClientModule {
178 cfg: args.cfg().clone(),
179 notifier: args.notifier().clone(),
180 redeem_key: args
181 .module_root_secret()
182 .child_key(ChildId(0))
183 .to_secp_key(&fedimint_core::secp256k1::Secp256k1::new()),
184 module_api: args.module_api().clone(),
185 federation_index: self.federation_index,
186 client_ctx: args.context(),
187 lightning_manager: self.lightning_manager.clone(),
188 connector_registry: args.connector_registry.clone(),
189 intercepted_htlc_lock_pool: lockable::LockPool::new(),
190 })
191 }
192}
193
194#[derive(Debug, Clone)]
195pub struct GatewayClientContext {
196 redeem_key: Keypair,
197 secp: Secp256k1<All>,
198 pub ln_decoder: Decoder,
199 notifier: ModuleNotifier<GatewayClientStateMachines>,
200 pub client_ctx: ClientContext<GatewayClientModule>,
201 pub lightning_manager: Arc<dyn IGatewayClientV1>,
202 pub connector_registry: ConnectorRegistry,
203}
204
205impl Context for GatewayClientContext {
206 const KIND: Option<ModuleKind> = Some(fedimint_ln_common::KIND);
207}
208
209impl From<&GatewayClientContext> for LightningClientContext {
210 fn from(ctx: &GatewayClientContext) -> Self {
211 let gateway_conn = RealGatewayConnection {
212 api: GatewayApi::new(None, ctx.connector_registry.clone()),
213 };
214 LightningClientContext {
215 ln_decoder: ctx.ln_decoder.clone(),
216 redeem_key: ctx.redeem_key,
217 gateway_conn: Arc::new(gateway_conn),
218 client_ctx: None,
219 }
220 }
221}
222
223pub struct GatewayClientModule {
228 cfg: LightningClientConfig,
229 pub notifier: ModuleNotifier<GatewayClientStateMachines>,
230 pub redeem_key: Keypair,
231 federation_index: u64,
232 module_api: DynModuleApi,
233 client_ctx: ClientContext<Self>,
234 pub lightning_manager: Arc<dyn IGatewayClientV1>,
235 connector_registry: ConnectorRegistry,
236 intercepted_htlc_lock_pool: lockable::LockPool<HtlcCircuitKey>,
237}
238
239impl fmt::Debug for GatewayClientModule {
240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 f.debug_struct("GatewayClientModule")
242 .finish_non_exhaustive()
243 }
244}
245
246impl ClientModule for GatewayClientModule {
247 type Init = LightningClientInit;
248 type Common = LightningModuleTypes;
249 type Backup = NoModuleBackup;
250 type ModuleStateMachineContext = GatewayClientContext;
251 type States = GatewayClientStateMachines;
252
253 fn context(&self) -> Self::ModuleStateMachineContext {
254 Self::ModuleStateMachineContext {
255 redeem_key: self.redeem_key,
256 secp: Secp256k1::new(),
257 ln_decoder: self.decoder(),
258 notifier: self.notifier.clone(),
259 client_ctx: self.client_ctx.clone(),
260 lightning_manager: self.lightning_manager.clone(),
261 connector_registry: self.connector_registry.clone(),
262 }
263 }
264
265 fn input_fee(
266 &self,
267 _amount: &Amounts,
268 _input: &<Self::Common as fedimint_core::module::ModuleCommon>::Input,
269 ) -> Option<Amounts> {
270 Some(Amounts::new_bitcoin(self.cfg.fee_consensus.contract_input))
271 }
272
273 fn output_fee(
274 &self,
275 _amount: &Amounts,
276 output: &<Self::Common as fedimint_core::module::ModuleCommon>::Output,
277 ) -> Option<Amounts> {
278 match output.maybe_v0_ref()? {
279 LightningOutputV0::Contract(_) => {
280 Some(Amounts::new_bitcoin(self.cfg.fee_consensus.contract_output))
281 }
282 LightningOutputV0::Offer(_) | LightningOutputV0::CancelOutgoing { .. } => {
283 Some(Amounts::ZERO)
284 }
285 }
286 }
287}
288
289impl GatewayClientModule {
290 fn to_gateway_registration_info(
291 &self,
292 route_hints: Vec<RouteHint>,
293 ttl: Duration,
294 fees: RoutingFees,
295 lightning_context: LightningContext,
296 api: SafeUrl,
297 gateway_keypair: Keypair,
298 ) -> LightningGatewayAnnouncement {
299 let info = LightningGateway {
300 federation_index: self.federation_index,
301 gateway_redeem_key: self.redeem_key.public_key(),
302 node_pub_key: lightning_context.lightning_public_key,
303 lightning_alias: lightning_context.lightning_alias,
304 api,
305 route_hints,
306 fees,
307 gateway_id: gateway_keypair.public_key(),
308 supports_private_payments: lightning_context.lnrpc.supports_private_payments(),
309 };
310
311 let nonce = u64::try_from(duration_since_epoch().as_millis())
317 .expect("milliseconds since the epoch do not exceed u64 for another 500m years");
318 let signature = gateway_keypair.sign_schnorr(create_gateway_registration_message(
319 self.cfg.threshold_pub_key,
320 nonce,
321 &info,
322 ));
323
324 LightningGatewayAnnouncement {
325 info,
326 ttl,
327 vetted: false,
328 auth: Some(GatewayRegistrationAuth { nonce, signature }),
329 }
330 }
331
332 async fn create_funding_incoming_contract_output_from_htlc(
333 &self,
334 htlc: Htlc,
335 ) -> Result<
336 (
337 OperationId,
338 Amount,
339 ClientOutput<LightningOutputV0>,
340 ClientOutputSM<GatewayClientStateMachines>,
341 ContractId,
342 ),
343 IncomingSmError,
344 > {
345 let operation_id = OperationId(htlc.payment_hash.to_byte_array());
346 let (incoming_output, amount, contract_id) = create_incoming_contract_output(
347 &self.module_api,
348 htlc.payment_hash,
349 htlc.outgoing_amount_msat,
350 &self.redeem_key,
351 )
352 .await?;
353
354 let client_output = ClientOutput::<LightningOutputV0> {
355 output: incoming_output,
356 amounts: Amounts::new_bitcoin(amount),
357 };
358 let client_output_sm = ClientOutputSM::<GatewayClientStateMachines> {
359 state_machines: Arc::new(move |out_point_range: OutPointRange| {
360 assert_eq!(out_point_range.count(), 1);
361 vec![
362 GatewayClientStateMachines::Receive(IncomingStateMachine {
363 common: IncomingSmCommon {
364 operation_id,
365 contract_id,
366 payment_hash: htlc.payment_hash,
367 },
368 state: IncomingSmStates::FundingOffer(FundingOfferState {
369 txid: out_point_range.txid(),
370 }),
371 }),
372 GatewayClientStateMachines::Complete(GatewayCompleteStateMachine {
373 common: GatewayCompleteCommon {
374 operation_id,
375 payment_hash: htlc.payment_hash,
376 incoming_chan_id: htlc.incoming_chan_id,
377 htlc_id: htlc.htlc_id,
378 },
379 state: GatewayCompleteStates::WaitForPreimage(WaitForPreimageState),
380 }),
381 ]
382 }),
383 };
384 Ok((
385 operation_id,
386 amount,
387 client_output,
388 client_output_sm,
389 contract_id,
390 ))
391 }
392
393 async fn create_funding_incoming_contract_output_from_swap(
394 &self,
395 swap: SwapParameters,
396 ) -> Result<
397 (
398 OperationId,
399 ClientOutput<LightningOutputV0>,
400 ClientOutputSM<GatewayClientStateMachines>,
401 ),
402 IncomingSmError,
403 > {
404 let payment_hash = swap.payment_hash;
405 let operation_id = OperationId(payment_hash.to_byte_array());
406 let (incoming_output, amount, contract_id) = create_incoming_contract_output(
407 &self.module_api,
408 payment_hash,
409 swap.amount_msat,
410 &self.redeem_key,
411 )
412 .await?;
413
414 let client_output = ClientOutput::<LightningOutputV0> {
415 output: incoming_output,
416 amounts: Amounts::new_bitcoin(amount),
417 };
418 let client_output_sm = ClientOutputSM::<GatewayClientStateMachines> {
419 state_machines: Arc::new(move |out_point_range| {
420 assert_eq!(out_point_range.count(), 1);
421 vec![GatewayClientStateMachines::Receive(IncomingStateMachine {
422 common: IncomingSmCommon {
423 operation_id,
424 contract_id,
425 payment_hash,
426 },
427 state: IncomingSmStates::FundingOffer(FundingOfferState {
428 txid: out_point_range.txid(),
429 }),
430 })]
431 }),
432 };
433 Ok((operation_id, client_output, client_output_sm))
434 }
435
436 pub async fn try_register_with_federation(
442 &self,
443 route_hints: Vec<RouteHint>,
444 time_to_live: Duration,
445 fees: RoutingFees,
446 lightning_context: LightningContext,
447 api: SafeUrl,
448 gateway_keypair: Keypair,
449 ) -> bool {
450 let registration_info = self.to_gateway_registration_info(
451 route_hints,
452 time_to_live,
453 fees,
454 lightning_context,
455 api,
456 gateway_keypair,
457 );
458 let gateway_id = registration_info.info.gateway_id;
459
460 let federation_id = self
461 .client_ctx
462 .get_config()
463 .await
464 .global
465 .calculate_federation_id();
466 match self.module_api.register_gateway(®istration_info).await {
467 Err(e) => {
468 warn!(
469 e = %e.fmt_compact(),
470 "Failed to register gateway {gateway_id} with federation {federation_id}"
471 );
472 false
473 }
474 _ => {
475 info!(
476 "Successfully registered gateway {gateway_id} with federation {federation_id}"
477 );
478 true
479 }
480 }
481 }
482
483 pub async fn remove_from_federation(&self, gateway_keypair: Keypair) {
488 if let Err(e) = self.remove_from_federation_inner(gateway_keypair).await {
491 let gateway_id = gateway_keypair.public_key();
492 let federation_id = self
493 .client_ctx
494 .get_config()
495 .await
496 .global
497 .calculate_federation_id();
498 warn!("Failed to remove gateway {gateway_id} from federation {federation_id}: {e:?}");
499 }
500 }
501
502 async fn remove_from_federation_inner(&self, gateway_keypair: Keypair) -> anyhow::Result<()> {
507 let gateway_id = gateway_keypair.public_key();
508 let challenges = self
509 .module_api
510 .get_remove_gateway_challenge(gateway_id)
511 .await;
512
513 let fed_public_key = self.cfg.threshold_pub_key;
514 let signatures = challenges
515 .into_iter()
516 .filter_map(|(peer_id, challenge)| {
517 let msg = create_gateway_remove_message(fed_public_key, peer_id, challenge?);
518 let signature = gateway_keypair.sign_schnorr(msg);
519 Some((peer_id, signature))
520 })
521 .collect::<BTreeMap<_, _>>();
522
523 let remove_gateway_request = RemoveGatewayRequest {
524 gateway_id,
525 signatures,
526 };
527
528 self.module_api.remove_gateway(remove_gateway_request).await;
529
530 Ok(())
531 }
532
533 pub async fn gateway_handle_intercepted_htlc(
550 &self,
551 htlc: Htlc,
552 current_block_height: impl Future<Output = anyhow::Result<u32>>,
553 ) -> anyhow::Result<OperationId> {
554 debug!("Handling intercepted HTLC {htlc:?}");
555
556 let operation_id = OperationId(htlc.payment_hash.to_byte_array());
557 let circuit_key = HtlcCircuitKey {
558 operation_id,
559 incoming_chan_id: htlc.incoming_chan_id,
560 htlc_id: htlc.htlc_id,
561 };
562
563 let _circuit_lock_guard = self
566 .intercepted_htlc_lock_pool
567 .async_lock(circuit_key)
568 .await;
569
570 let replay_of_active_circuit = self
573 .client_ctx
574 .get_own_operation_active_states(operation_id)
575 .await
576 .into_iter()
577 .any(|(state, _)| circuit_key.matches_state(&state));
578 if replay_of_active_circuit {
579 debug!(
580 ?operation_id,
581 incoming_chan_id = htlc.incoming_chan_id,
582 htlc_id = htlc.htlc_id,
583 "HTLC circuit already being handled by an active completion state machine, treating as in-flight (likely an LND stream-reconnect replay)"
584 );
585 return Ok(operation_id);
586 }
587 let replay_of_inactive_circuit = self
588 .client_ctx
589 .get_own_operation_inactive_states(operation_id)
590 .await
591 .into_iter()
592 .any(|(state, _)| circuit_key.matches_state(&state));
593 if replay_of_inactive_circuit {
594 debug!(
595 ?operation_id,
596 incoming_chan_id = htlc.incoming_chan_id,
597 htlc_id = htlc.htlc_id,
598 "HTLC circuit was already handled by a completion state machine, treating as idempotent replay"
599 );
600 return Ok(operation_id);
601 }
602
603 let current_block_height = current_block_height.await?;
604 htlc.ensure_safe_expiry(current_block_height)?;
605 let remaining_blocks = htlc.incoming_expiry.saturating_sub(current_block_height);
606 if remaining_blocks <= u32::from(LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA) {
607 warn!(
611 payment_hash = %htlc.payment_hash,
612 remaining_blocks,
613 advertised_delta = LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA,
614 "Accepting LNv1 HTLC with less remaining expiry than newly created invoices advertise, likely paid to a pre-upgrade invoice"
615 );
616 }
617
618 let (op_id_from_funding, amount, client_output, client_output_sm, contract_id) = self
619 .create_funding_incoming_contract_output_from_htlc(htlc.clone())
620 .await?;
621 anyhow::ensure!(
624 op_id_from_funding == operation_id,
625 "operation id derivation must match: {op_id_from_funding:?} != {operation_id:?}"
626 );
627
628 let output = ClientOutput {
629 output: LightningOutput::V0(client_output.output),
630 amounts: Amounts::new_bitcoin(amount),
631 };
632
633 let tx = TransactionBuilder::new().with_outputs(self.client_ctx.make_client_outputs(
634 ClientOutputBundle::new(vec![output], vec![client_output_sm]),
635 ));
636 let operation_meta_gen = |_: OutPointRange| GatewayMeta::Receive;
637 self.client_ctx
638 .finalize_and_submit_transaction(operation_id, KIND.as_str(), operation_meta_gen, tx)
639 .await?;
640 debug!(?operation_id, "Submitted transaction for HTLC {htlc:?}");
641 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
642 self.client_ctx
643 .log_event(
644 &mut dbtx,
645 IncomingPaymentStarted {
646 contract_id,
647 payment_hash: htlc.payment_hash,
648 invoice_amount: htlc.outgoing_amount_msat,
649 contract_amount: amount,
650 operation_id,
651 },
652 )
653 .await;
654 dbtx.commit_tx().await;
655 Ok(operation_id)
656 }
657
658 pub async fn gateway_handle_direct_swap(
672 &self,
673 swap_params: SwapParameters,
674 ) -> anyhow::Result<OperationId> {
675 debug!("Handling direct swap {swap_params:?}");
676
677 let payment_hash = swap_params.payment_hash;
678 let operation_id = OperationId(payment_hash.to_byte_array());
679
680 if self.client_ctx.operation_exists(operation_id).await {
685 debug!(
686 operation_id = %operation_id.fmt_short(),
687 %payment_hash,
688 "Direct swap already in progress, returning the operation already funding it"
689 );
690
691 return Ok(operation_id);
692 }
693
694 let (op_id_from_funding, client_output, client_output_sm) = self
695 .create_funding_incoming_contract_output_from_swap(swap_params.clone())
696 .await?;
697 anyhow::ensure!(
699 op_id_from_funding == operation_id,
700 "operation id derivation must match: {op_id_from_funding:?} != {operation_id:?}"
701 );
702
703 self.client_ctx
704 .module_db()
705 .autocommit(
706 |dbtx, _| {
707 let client_output = client_output.clone();
708 let client_output_sm = client_output_sm.clone();
709 Box::pin(async move {
710 if self
715 .client_ctx
716 .get_operation_dbtx(dbtx, operation_id)
717 .await
718 .is_some()
719 {
720 debug!(
721 operation_id = %operation_id.fmt_short(),
722 %payment_hash,
723 "Concurrent direct swap won the race, returning the operation already funding it"
724 );
725
726 return Ok(operation_id);
727 }
728
729 let output = ClientOutput {
730 output: LightningOutput::V0(client_output.output),
731 amounts: client_output.amounts,
732 };
733 let tx = TransactionBuilder::new().with_outputs(
734 self.client_ctx.make_client_outputs(ClientOutputBundle::new(
735 vec![output],
736 vec![client_output_sm],
737 )),
738 );
739
740 self.client_ctx
741 .finalize_and_submit_transaction_dbtx(
742 dbtx,
743 operation_id,
744 KIND.as_str(),
745 |_: OutPointRange| GatewayMeta::Receive,
746 tx,
747 )
748 .await?;
749
750 debug!(
751 ?operation_id,
752 %payment_hash,
753 "Submitted funding transaction for direct swap"
754 );
755
756 Ok(operation_id)
757 })
758 },
759 Some(100),
760 )
761 .await
762 .map_err(|e| match e {
763 AutocommitError::ClosureError { error, .. } => error,
764 AutocommitError::CommitFailed { last_error, .. } => {
765 anyhow::anyhow!("Commit to DB failed: {last_error}")
766 }
767 })
768 }
769
770 pub async fn gateway_subscribe_ln_receive(
773 &self,
774 operation_id: OperationId,
775 ) -> anyhow::Result<UpdateStreamOrOutcome<GatewayExtReceiveStates>> {
776 let operation = self.client_ctx.get_operation(operation_id).await?;
777 let mut stream = self.notifier.subscribe(operation_id).await;
778 let client_ctx = self.client_ctx.clone();
779
780 Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
781 GatewayExtReceiveStates::Funding => false,
782 GatewayExtReceiveStates::Preimage(_)
783 | GatewayExtReceiveStates::RefundSuccess { .. }
784 | GatewayExtReceiveStates::RefundError { .. }
785 | GatewayExtReceiveStates::FundingFailed { .. } => true,
786 }, move || {
787 stream! {
788
789 yield GatewayExtReceiveStates::Funding;
790
791 let state = loop {
792 debug!("Getting next ln receive state for {}", operation_id.fmt_short());
793 if let Some(GatewayClientStateMachines::Receive(state)) = stream.next().await {
794 match state.state {
795 IncomingSmStates::Preimage(preimage) =>{
796 debug!(?operation_id, "Received preimage");
797 break GatewayExtReceiveStates::Preimage(preimage)
798 },
799 IncomingSmStates::RefundSubmitted { out_points, error } => {
800 debug!(?operation_id, "Refund submitted for {out_points:?} {error}");
801 match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
802 Ok(()) => {
803 debug!(?operation_id, "Refund success");
804 break GatewayExtReceiveStates::RefundSuccess { out_points, error }
805 },
806 Err(e) => {
807 warn!(?operation_id, "Got failure {e:?} while awaiting for refund outputs {out_points:?}");
808 break GatewayExtReceiveStates::RefundError{ error_message: e.to_string(), error }
809 },
810 }
811 },
812 IncomingSmStates::FundingFailed { error } => {
813 warn!(?operation_id, "Funding failed: {error:?}");
814 break GatewayExtReceiveStates::FundingFailed{ error }
815 },
816 other => {
817 debug!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
818 }
819 }
820 }
821 };
822 yield state;
823 }
824 }))
825 }
826
827 pub async fn await_completion(&self, operation_id: OperationId) {
830 let mut stream = self.notifier.subscribe(operation_id).await;
831 loop {
832 match stream.next().await {
833 Some(GatewayClientStateMachines::Complete(state)) => match state.state {
834 GatewayCompleteStates::HtlcFinished => {
835 info!(%state, "LNv1 completion state machine finished");
836 return;
837 }
838 GatewayCompleteStates::Failure => {
839 error!(%state, "LNv1 completion state machine failed");
840 return;
841 }
842 _ => {
843 info!(%state, "Waiting for LNv1 completion state machine");
844 continue;
845 }
846 },
847 Some(GatewayClientStateMachines::Receive(state)) => {
848 info!(%state, "Waiting for LNv1 completion state machine");
849 continue;
850 }
851 Some(state) => {
852 warn!(%state, "Operation is not an LNv1 completion state machine");
853 return;
854 }
855 None => return,
856 }
857 }
858 }
859
860 pub async fn gateway_pay_bolt11_invoice(
862 &self,
863 pay_invoice_payload: PayInvoicePayload,
864 ) -> anyhow::Result<OperationId> {
865 let payload = pay_invoice_payload.clone();
866
867 let invoice_amount = pay_invoice_payload
873 .payment_data
874 .amount()
875 .ok_or(OutgoingContractError::InvoiceMissingAmount)?;
876
877 self.lightning_manager
878 .verify_pruned_invoice(pay_invoice_payload.payment_data)
879 .await?;
880
881 self.client_ctx.module_db()
882 .autocommit(
883 |dbtx, _| {
884 Box::pin(async {
885 let operation_id = OperationId(payload.contract_id.to_byte_array());
886
887 if let Some(entry) =
895 self.client_ctx.get_operation_dbtx(dbtx, operation_id).await
896 {
897 if !matches!(
905 entry.try_meta::<GatewayMeta>(),
906 Ok(GatewayMeta::Pay { preimage_auth })
907 if preimage_auth == payload.preimage_auth
908 ) {
909 anyhow::bail!(
910 "Not authorized to receive the preimage for contract {}",
911 payload.contract_id
912 );
913 }
914
915 debug!(
916 operation_id = %operation_id.fmt_short(),
917 contract_id = %payload.contract_id,
918 "Duplicate request to pay an outgoing contract, returning the operation already in progress"
919 );
920
921 return Ok(operation_id);
922 }
923
924 self.client_ctx.log_event(dbtx, OutgoingPaymentStarted {
925 contract_id: payload.contract_id,
926 invoice_amount,
927 operation_id,
928 }).await;
929
930 let state_machines =
931 vec![GatewayClientStateMachines::Pay(GatewayPayStateMachine {
932 common: GatewayPayCommon { operation_id },
933 state: GatewayPayStates::PayInvoice(GatewayPayInvoice {
934 pay_invoice_payload: payload.clone(),
935 }),
936 })];
937
938 let dyn_states = state_machines
939 .into_iter()
940 .map(|s| self.client_ctx.make_dyn(s))
941 .collect();
942
943 match self.client_ctx.add_state_machines_dbtx(dbtx, dyn_states).await {
944 Ok(()) => {
945 self.client_ctx
946 .add_operation_log_entry_dbtx(
947 dbtx,
948 operation_id,
949 KIND.as_str(),
950 GatewayMeta::Pay {
951 preimage_auth: payload.preimage_auth,
952 },
953 )
954 .await;
955 }
956 Err(AddStateMachinesError::StateAlreadyExists) => {
957 info!("State machine for operation {} already exists, will not add a new one", operation_id.fmt_short());
958 }
959 Err(other) => {
960 anyhow::bail!("Failed to add state machines: {other:?}")
961 }
962 }
963 Ok(operation_id)
964 })
965 },
966 Some(100),
967 )
968 .await
969 .map_err(|e| match e {
970 AutocommitError::ClosureError { error, .. } => error,
971 AutocommitError::CommitFailed { last_error, .. } => {
972 anyhow::anyhow!("Commit to DB failed: {last_error}")
973 }
974 })
975 }
976
977 pub async fn gateway_subscribe_ln_pay(
978 &self,
979 operation_id: OperationId,
980 ) -> anyhow::Result<UpdateStreamOrOutcome<GatewayExtPayStates>> {
981 let mut stream = self.notifier.subscribe(operation_id).await;
982 let operation = self.client_ctx.get_operation(operation_id).await?;
983 let client_ctx = self.client_ctx.clone();
984
985 Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
986 GatewayExtPayStates::Created | GatewayExtPayStates::Preimage { .. } => false,
987 GatewayExtPayStates::Success { .. }
988 | GatewayExtPayStates::Canceled { .. }
989 | GatewayExtPayStates::Fail { .. }
990 | GatewayExtPayStates::OfferDoesNotExist { .. } => true,
991 }, move || {
992 stream! {
993 yield GatewayExtPayStates::Created;
994
995 loop {
996 debug!("Getting next ln pay state for {}", operation_id.fmt_short());
997 match stream.next().await { Some(GatewayClientStateMachines::Pay(state)) => {
998 match state.state {
999 GatewayPayStates::Preimage(out_points, preimage) => {
1000 yield GatewayExtPayStates::Preimage{ preimage: preimage.clone() };
1001
1002 match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
1003 Ok(()) => {
1004 debug!(?operation_id, "Success");
1005 yield GatewayExtPayStates::Success{ preimage: preimage.clone(), out_points };
1006 return;
1007
1008 }
1009 Err(e) => {
1010 warn!(?operation_id, "Got failure {e:?} while awaiting for outputs {out_points:?}");
1011 }
1013 }
1014 }
1015 GatewayPayStates::Canceled { txid, contract_id, error } => {
1016 debug!(?operation_id, "Trying to cancel contract {contract_id:?} due to {error:?}");
1017 match client_ctx.transaction_updates(operation_id).await.await_tx_accepted(txid).await {
1018 Ok(()) => {
1019 debug!(?operation_id, "Canceled contract {contract_id:?} due to {error:?}");
1020 yield GatewayExtPayStates::Canceled{ error };
1021 return;
1022 }
1023 Err(e) => {
1024 warn!(?operation_id, "Got failure {e:?} while awaiting for transaction {txid} to be accepted for");
1025 yield GatewayExtPayStates::Fail { error, error_message: format!("Refund transaction {txid} was not accepted by the federation. OperationId: {} Error: {e:?}", operation_id.fmt_short()) };
1026 }
1027 }
1028 }
1029 GatewayPayStates::OfferDoesNotExist(contract_id) => {
1030 warn!("Yielding OfferDoesNotExist state for {} and contract {contract_id}", operation_id.fmt_short());
1031 yield GatewayExtPayStates::OfferDoesNotExist { contract_id };
1032 }
1033 GatewayPayStates::Failed{ error, error_message } => {
1034 warn!("Yielding Fail state for {} due to {error:?} {error_message:?}", operation_id.fmt_short());
1035 yield GatewayExtPayStates::Fail{ error, error_message };
1036 },
1037 GatewayPayStates::PayInvoice(_) => {
1038 debug!("Got initial state PayInvoice while awaiting for output of {}", operation_id.fmt_short());
1039 }
1040 other => {
1041 info!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
1042 }
1043 }
1044 } _ => {
1045 warn!("Got None while getting next ln pay state for {}", operation_id.fmt_short());
1046 }}
1047 }
1048 }
1049 }))
1050 }
1051}
1052
1053#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
1054struct HtlcCircuitKey {
1055 operation_id: OperationId,
1056 incoming_chan_id: u64,
1057 htlc_id: u64,
1058}
1059
1060impl HtlcCircuitKey {
1061 fn matches_state(self, state: &GatewayClientStateMachines) -> bool {
1062 matches!(
1063 state,
1064 GatewayClientStateMachines::Complete(sm)
1065 if sm.common.operation_id == self.operation_id
1066 && sm.common.incoming_chan_id == self.incoming_chan_id
1067 && sm.common.htlc_id == self.htlc_id
1068 )
1069 }
1070}
1071
1072#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1073pub enum GatewayClientStateMachines {
1074 Pay(GatewayPayStateMachine),
1075 Receive(IncomingStateMachine),
1076 Complete(GatewayCompleteStateMachine),
1077}
1078
1079impl fmt::Display for GatewayClientStateMachines {
1080 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1081 match self {
1082 GatewayClientStateMachines::Pay(pay) => {
1083 write!(f, "{pay}")
1084 }
1085 GatewayClientStateMachines::Receive(receive) => {
1086 write!(f, "{receive}")
1087 }
1088 GatewayClientStateMachines::Complete(complete) => {
1089 write!(f, "{complete}")
1090 }
1091 }
1092 }
1093}
1094
1095impl IntoDynInstance for GatewayClientStateMachines {
1096 type DynType = DynState;
1097
1098 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1099 DynState::from_typed(instance_id, self)
1100 }
1101}
1102
1103impl State for GatewayClientStateMachines {
1104 type ModuleContext = GatewayClientContext;
1105
1106 fn transitions(
1107 &self,
1108 context: &Self::ModuleContext,
1109 global_context: &DynGlobalClientContext,
1110 ) -> Vec<StateTransition<Self>> {
1111 match self {
1112 GatewayClientStateMachines::Pay(pay_state) => {
1113 sm_enum_variant_translation!(
1114 pay_state.transitions(context, global_context),
1115 GatewayClientStateMachines::Pay
1116 )
1117 }
1118 GatewayClientStateMachines::Receive(receive_state) => {
1119 sm_enum_variant_translation!(
1120 receive_state.transitions(&context.into(), global_context),
1121 GatewayClientStateMachines::Receive
1122 )
1123 }
1124 GatewayClientStateMachines::Complete(complete_state) => {
1125 sm_enum_variant_translation!(
1126 complete_state.transitions(context, global_context),
1127 GatewayClientStateMachines::Complete
1128 )
1129 }
1130 }
1131 }
1132
1133 fn operation_id(&self) -> fedimint_core::core::OperationId {
1134 match self {
1135 GatewayClientStateMachines::Pay(pay_state) => pay_state.operation_id(),
1136 GatewayClientStateMachines::Receive(receive_state) => receive_state.operation_id(),
1137 GatewayClientStateMachines::Complete(complete_state) => complete_state.operation_id(),
1138 }
1139 }
1140}
1141
1142#[derive(Debug, Clone, Eq, PartialEq)]
1143pub struct Htlc {
1144 pub payment_hash: sha256::Hash,
1146 pub incoming_amount_msat: Amount,
1148 pub outgoing_amount_msat: Amount,
1150 pub incoming_expiry: u32,
1155 pub short_channel_id: Option<u64>,
1157 pub incoming_chan_id: u64,
1159 pub htlc_id: u64,
1161}
1162
1163#[derive(Debug, Error, Clone, Eq, PartialEq)]
1165#[error(
1166 "incoming HTLC expiry is unsafe: expiry {incoming_expiry}, current height {current_block_height}, required remaining blocks greater than {expiry_safety_margin}"
1167)]
1168pub struct UnsafeHtlcExpiry {
1169 pub incoming_expiry: u32,
1171 pub current_block_height: u32,
1173 pub expiry_safety_margin: u32,
1175}
1176
1177impl Htlc {
1178 pub fn ensure_safe_expiry(&self, current_block_height: u32) -> Result<(), UnsafeHtlcExpiry> {
1181 let remaining_blocks = self.incoming_expiry.saturating_sub(current_block_height);
1182 if LNV1_HTLC_EXPIRY_SAFETY_MARGIN < remaining_blocks {
1183 return Ok(());
1184 }
1185
1186 Err(UnsafeHtlcExpiry {
1187 incoming_expiry: self.incoming_expiry,
1188 current_block_height,
1189 expiry_safety_margin: LNV1_HTLC_EXPIRY_SAFETY_MARGIN,
1190 })
1191 }
1192}
1193
1194impl TryFrom<InterceptPaymentRequest> for Htlc {
1195 type Error = anyhow::Error;
1196
1197 fn try_from(s: InterceptPaymentRequest) -> Result<Self, Self::Error> {
1198 Ok(Self {
1199 payment_hash: s.payment_hash,
1200 incoming_amount_msat: Amount::from_msats(s.amount_msat),
1201 outgoing_amount_msat: Amount::from_msats(s.amount_msat),
1202 incoming_expiry: s.expiry,
1203 short_channel_id: s.short_channel_id,
1204 incoming_chan_id: s.incoming_chan_id,
1205 htlc_id: s.htlc_id,
1206 })
1207 }
1208}
1209
1210#[derive(Debug, Clone)]
1211pub struct SwapParameters {
1212 pub payment_hash: sha256::Hash,
1213 pub amount_msat: Amount,
1214}
1215
1216impl TryFrom<PaymentData> for SwapParameters {
1217 type Error = anyhow::Error;
1218
1219 fn try_from(s: PaymentData) -> Result<Self, Self::Error> {
1220 let payment_hash = s.payment_hash();
1221 let amount_msat = s
1222 .amount()
1223 .ok_or_else(|| anyhow::anyhow!("Amountless invoice cannot be used in direct swap"))?;
1224 Ok(Self {
1225 payment_hash,
1226 amount_msat,
1227 })
1228 }
1229}
1230
1231#[async_trait]
1237pub trait IGatewayClientV1: Debug + Send + Sync {
1238 async fn verify_preimage_authentication(
1244 &self,
1245 payment_hash: sha256::Hash,
1246 preimage_auth: sha256::Hash,
1247 contract: OutgoingContractAccount,
1248 ) -> Result<(), OutgoingPaymentError>;
1249
1250 async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()>;
1253
1254 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees>;
1256
1257 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>>;
1260
1261 async fn get_client_for_invoice(
1269 &self,
1270 payment_data: PaymentData,
1271 ) -> Option<Spanned<ClientHandleArc>>;
1272
1273 async fn pay(
1275 &self,
1276 payment_data: PaymentData,
1277 max_delay: u64,
1278 max_fee: Amount,
1279 ) -> Result<PayInvoiceResponse, LightningRpcError>;
1280
1281 async fn complete_htlc(
1283 &self,
1284 htlc_response: InterceptPaymentResponse,
1285 ) -> Result<(), LightningRpcError>;
1286
1287 async fn is_lnv2_direct_swap(
1290 &self,
1291 payment_hash: sha256::Hash,
1292 amount: Amount,
1293 ) -> anyhow::Result<
1294 Option<(
1295 fedimint_lnv2_common::contracts::IncomingContract,
1296 ClientHandleArc,
1297 )>,
1298 >;
1299}