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