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
653 let federation_id = self
659 .client_ctx
660 .get_config()
661 .await
662 .global
663 .calculate_federation_id();
664 let _policy_guard = self
665 .lightning_manager
666 .begin_fresh_receive(federation_id)
667 .await
668 .ok_or(HandleInterceptedHtlcError::ReceiveDisabled { federation_id })?;
669
670 self.client_ctx
671 .finalize_and_submit_transaction(operation_id, KIND.as_str(), operation_meta_gen, tx)
672 .await?;
673 debug!(?operation_id, "Submitted transaction for HTLC {htlc:?}");
674 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
675 self.client_ctx
676 .log_event(
677 &mut dbtx,
678 IncomingPaymentStarted {
679 contract_id,
680 payment_hash: htlc.payment_hash,
681 invoice_amount: htlc.incoming_amount_msat,
682 contract_amount: amount,
683 operation_id,
684 },
685 )
686 .await;
687 dbtx.commit_tx().await;
688 Ok(operation_id)
689 }
690
691 pub async fn gateway_handle_direct_swap(
718 &self,
719 swap_params: SwapParameters,
720 allow_fresh_dispatch: bool,
721 ) -> Result<Option<OperationId>, HandleDirectSwapError> {
722 debug!("Handling direct swap {swap_params:?}");
723
724 let payment_hash = swap_params.payment_hash;
725 let operation_id = OperationId(payment_hash.to_byte_array());
726
727 if self.client_ctx.operation_exists(operation_id).await {
732 debug!(
733 operation_id = %operation_id.fmt_short(),
734 %payment_hash,
735 "Direct swap already in progress, returning the operation already funding it"
736 );
737
738 return Ok(Some(operation_id));
739 }
740
741 if !allow_fresh_dispatch {
742 return Ok(None);
743 }
744
745 let (op_id_from_funding, client_output, client_output_sm) = self
746 .create_funding_incoming_contract_output_from_swap(swap_params.clone())
747 .await?;
748 if op_id_from_funding != operation_id {
750 return Err(HandleDirectSwapError::OperationIdMismatch {
751 expected: operation_id,
752 derived: op_id_from_funding,
753 });
754 }
755
756 let federation_id = self
761 .client_ctx
762 .get_config()
763 .await
764 .global
765 .calculate_federation_id();
766 let _policy_guard = self
767 .lightning_manager
768 .begin_fresh_receive(federation_id)
769 .await
770 .ok_or(HandleDirectSwapError::ReceiveDisabled { federation_id })?;
771
772 self.client_ctx
773 .module_db()
774 .autocommit(
775 |dbtx, _| {
776 let client_output = client_output.clone();
777 let client_output_sm = client_output_sm.clone();
778 Box::pin(async move {
779 if self
784 .client_ctx
785 .get_operation_dbtx(dbtx, operation_id)
786 .await
787 .is_some()
788 {
789 debug!(
790 operation_id = %operation_id.fmt_short(),
791 %payment_hash,
792 "Concurrent direct swap won the race, returning the operation already funding it"
793 );
794
795 return Ok(Some(operation_id));
796 }
797
798 let output = ClientOutput {
799 output: LightningOutput::V0(client_output.output),
800 amounts: client_output.amounts,
801 };
802 let tx = TransactionBuilder::new().with_outputs(
803 self.client_ctx.make_client_outputs(ClientOutputBundle::new(
804 vec![output],
805 vec![client_output_sm],
806 )),
807 );
808
809 self.client_ctx
810 .finalize_and_submit_transaction_dbtx(
811 dbtx,
812 operation_id,
813 KIND.as_str(),
814 |_: OutPointRange| GatewayMeta::Receive,
815 tx,
816 )
817 .await?;
818
819 debug!(
820 ?operation_id,
821 %payment_hash,
822 "Submitted funding transaction for direct swap"
823 );
824
825 Ok::<_, HandleDirectSwapError>(Some(operation_id))
826 })
827 },
828 Some(100),
829 )
830 .await
831 .map_err(HandleDirectSwapError::from)
832 }
833
834 pub async fn gateway_subscribe_ln_receive(
842 &self,
843 operation_id: OperationId,
844 ) -> Result<UpdateStreamOrOutcome<GatewayExtReceiveStates>, OperationLookupError> {
845 let operation = self.client_ctx.get_operation(operation_id).await?;
846 let mut stream = self.notifier.subscribe(operation_id).await;
847 let client_ctx = self.client_ctx.clone();
848
849 Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
850 GatewayExtReceiveStates::Funding => false,
851 GatewayExtReceiveStates::Preimage(_)
852 | GatewayExtReceiveStates::RefundSuccess { .. }
853 | GatewayExtReceiveStates::RefundError { .. }
854 | GatewayExtReceiveStates::FundingFailed { .. } => true,
855 }, move || {
856 stream! {
857
858 yield GatewayExtReceiveStates::Funding;
859
860 let state = loop {
861 debug!("Getting next ln receive state for {}", operation_id.fmt_short());
862 if let Some(GatewayClientStateMachines::Receive(state)) = stream.next().await {
863 match state.state {
864 IncomingSmStates::Preimage(preimage) =>{
865 debug!(?operation_id, "Received preimage");
866 break GatewayExtReceiveStates::Preimage(preimage)
867 },
868 IncomingSmStates::RefundSubmitted { out_points, error } => {
869 debug!(?operation_id, "Refund submitted for {out_points:?} {error}");
870 match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
871 Ok(()) => {
872 debug!(?operation_id, "Refund success");
873 break GatewayExtReceiveStates::RefundSuccess { out_points, error }
874 },
875 Err(e) => {
876 warn!(?operation_id, "Got failure {e:?} while awaiting for refund outputs {out_points:?}");
877 break GatewayExtReceiveStates::RefundError{ error_message: e.fmt_compact().to_string(), error }
878 },
879 }
880 },
881 IncomingSmStates::FundingFailed { error } => {
882 warn!(?operation_id, "Funding failed: {error:?}");
883 break GatewayExtReceiveStates::FundingFailed{ error }
884 },
885 other => {
886 debug!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
887 }
888 }
889 }
890 };
891 yield state;
892 }
893 }))
894 }
895
896 pub async fn await_completion(&self, operation_id: OperationId) {
899 let mut stream = self.notifier.subscribe(operation_id).await;
900 loop {
901 match stream.next().await {
902 Some(GatewayClientStateMachines::Complete(state)) => match state.state {
903 GatewayCompleteStates::HtlcFinished => {
904 info!(%state, "LNv1 completion state machine finished");
905 return;
906 }
907 GatewayCompleteStates::Failure => {
908 error!(%state, "LNv1 completion state machine failed");
909 return;
910 }
911 _ => {
912 info!(%state, "Waiting for LNv1 completion state machine");
913 continue;
914 }
915 },
916 Some(GatewayClientStateMachines::Receive(state)) => {
917 info!(%state, "Waiting for LNv1 completion state machine");
918 continue;
919 }
920 Some(state) => {
921 warn!(%state, "Operation is not an LNv1 completion state machine");
922 return;
923 }
924 None => return,
925 }
926 }
927 }
928
929 pub async fn gateway_pay_bolt11_invoice(
951 &self,
952 pay_invoice_payload: PayInvoicePayload,
953 ) -> Result<OperationId, GatewayPayInvoiceError> {
954 let payload = pay_invoice_payload.clone();
955
956 let invoice_amount = pay_invoice_payload
962 .payment_data
963 .amount()
964 .ok_or(GatewayPayInvoiceError::MissingInvoiceAmount)?;
965
966 self.lightning_manager
967 .verify_pruned_invoice(pay_invoice_payload.payment_data)
968 .await
969 .map_err(GatewayPayInvoiceError::PrunedInvoiceRejected)?;
970
971 let initial_state = if self
976 .lightning_manager
977 .claim_payment_image(
978 &fedimint_lnv2_common::contracts::PaymentImage::Hash(
979 payload.payment_data.payment_hash(),
980 ),
981 OperationId(payload.contract_id.to_byte_array()),
982 )
983 .await
984 {
985 GatewayPayStates::PayInvoice(GatewayPayInvoice {
986 pay_invoice_payload: payload.clone(),
987 })
988 } else {
989 let account = self
990 .module_api
991 .fetch_contract(payload.contract_id)
992 .await
993 .map_err(GatewayPayInvoiceError::ContractLookup)?
994 .ok_or(GatewayPayInvoiceError::PaymentHashAlreadyClaimed)?;
995 let FundedContract::Outgoing(contract) = account.contract else {
996 return Err(GatewayPayInvoiceError::PaymentHashAlreadyClaimed);
997 };
998
999 if account.amount == Amount::ZERO
1000 || contract.gateway_key != self.redeem_key.public_key()
1001 || contract.hash != payload.payment_data.payment_hash()
1002 {
1003 return Err(GatewayPayInvoiceError::PaymentHashAlreadyClaimed);
1004 }
1005 GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract::for_claimed_hash(
1006 OutgoingContractAccount {
1007 amount: account.amount,
1008 contract,
1009 },
1010 )))
1011 };
1012
1013 self.client_ctx.module_db()
1014 .autocommit(
1015 |dbtx, _| {
1016 Box::pin(async {
1017 let operation_id = OperationId(payload.contract_id.to_byte_array());
1018
1019 if let Some(entry) =
1027 self.client_ctx.get_operation_dbtx(dbtx, operation_id).await
1028 {
1029 if !matches!(
1037 entry.try_meta::<GatewayMeta>(),
1038 Ok(GatewayMeta::Pay { preimage_auth })
1039 if PreimageAuth::new(preimage_auth)
1040 .verifies(payload.preimage_auth)
1041 ) {
1042 return Err(GatewayPayInvoiceError::Unauthorized {
1043 contract_id: payload.contract_id,
1044 });
1045 }
1046
1047 debug!(
1048 operation_id = %operation_id.fmt_short(),
1049 contract_id = %payload.contract_id,
1050 "Duplicate request to pay an outgoing contract, returning the operation already in progress"
1051 );
1052
1053 return Ok(operation_id);
1054 }
1055
1056 self.client_ctx.log_event(dbtx, OutgoingPaymentStarted {
1057 contract_id: payload.contract_id,
1058 invoice_amount,
1059 operation_id,
1060 }).await;
1061
1062 let state_machines =
1063 vec![GatewayClientStateMachines::Pay(GatewayPayStateMachine {
1064 common: GatewayPayCommon { operation_id },
1065 state: initial_state.clone(),
1066 })];
1067
1068 let dyn_states = state_machines
1069 .into_iter()
1070 .map(|s| self.client_ctx.make_dyn(s))
1071 .collect();
1072
1073 match self.client_ctx.add_state_machines_dbtx(dbtx, dyn_states).await {
1074 Ok(()) => {
1075 self.client_ctx
1076 .add_operation_log_entry_dbtx(
1077 dbtx,
1078 operation_id,
1079 KIND.as_str(),
1080 GatewayMeta::Pay {
1081 preimage_auth: payload.preimage_auth,
1082 },
1083 )
1084 .await;
1085 }
1086 Err(AddStateMachinesError::StateAlreadyExists) => {
1087 info!("State machine for operation {} already exists, will not add a new one", operation_id.fmt_short());
1088 }
1089 Err(other) => {
1090 return Err(GatewayPayInvoiceError::StateMachines(other));
1091 }
1092 }
1093 Ok::<_, GatewayPayInvoiceError>(operation_id)
1094 })
1095 },
1096 Some(100),
1097 )
1098 .await
1099 .map_err(GatewayPayInvoiceError::from)
1100 }
1101
1102 pub async fn gateway_subscribe_ln_pay(
1110 &self,
1111 operation_id: OperationId,
1112 ) -> Result<UpdateStreamOrOutcome<GatewayExtPayStates>, OperationLookupError> {
1113 let mut stream = self.notifier.subscribe(operation_id).await;
1114 let operation = self.client_ctx.get_operation(operation_id).await?;
1115 let client_ctx = self.client_ctx.clone();
1116
1117 Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
1118 GatewayExtPayStates::Created | GatewayExtPayStates::Preimage { .. } => false,
1119 GatewayExtPayStates::Success { .. }
1120 | GatewayExtPayStates::Canceled { .. }
1121 | GatewayExtPayStates::Fail { .. }
1122 | GatewayExtPayStates::OfferDoesNotExist { .. } => true,
1123 }, move || {
1124 stream! {
1125 yield GatewayExtPayStates::Created;
1126
1127 loop {
1128 debug!("Getting next ln pay state for {}", operation_id.fmt_short());
1129 match stream.next().await { Some(GatewayClientStateMachines::Pay(state)) => {
1130 match state.state {
1131 GatewayPayStates::Preimage(out_points, preimage) => {
1132 yield GatewayExtPayStates::Preimage{ preimage: preimage.clone() };
1133
1134 match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
1135 Ok(()) => {
1136 debug!(?operation_id, "Success");
1137 yield GatewayExtPayStates::Success{ preimage: preimage.clone(), out_points };
1138 return;
1139
1140 }
1141 Err(e) => {
1142 warn!(?operation_id, "Got failure {e:?} while awaiting for outputs {out_points:?}");
1143 }
1145 }
1146 }
1147 GatewayPayStates::Canceled { txid, contract_id, error } => {
1148 debug!(?operation_id, "Trying to cancel contract {contract_id:?} due to {error:?}");
1149 match client_ctx.transaction_updates(operation_id).await.await_tx_accepted(txid).await {
1150 Ok(()) => {
1151 debug!(?operation_id, "Canceled contract {contract_id:?} due to {error:?}");
1152 yield GatewayExtPayStates::Canceled{ error };
1153 return;
1154 }
1155 Err(e) => {
1156 warn!(?operation_id, "Got failure {e:?} while awaiting for transaction {txid} to be accepted for");
1157 yield GatewayExtPayStates::Fail { error, error_message: format!("Refund transaction {txid} was not accepted by the federation. OperationId: {} Error: {e:?}", operation_id.fmt_short()) };
1158 }
1159 }
1160 }
1161 GatewayPayStates::OfferDoesNotExist(contract_id) => {
1162 warn!("Yielding OfferDoesNotExist state for {} and contract {contract_id}", operation_id.fmt_short());
1163 yield GatewayExtPayStates::OfferDoesNotExist { contract_id };
1164 }
1165 GatewayPayStates::Failed{ error, error_message } => {
1166 warn!("Yielding Fail state for {} due to {error:?} {error_message:?}", operation_id.fmt_short());
1167 yield GatewayExtPayStates::Fail{ error, error_message };
1168 },
1169 GatewayPayStates::PayInvoice(_) => {
1170 debug!("Got initial state PayInvoice while awaiting for output of {}", operation_id.fmt_short());
1171 }
1172 other => {
1173 info!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
1174 }
1175 }
1176 } _ => {
1177 warn!("Got None while getting next ln pay state for {}", operation_id.fmt_short());
1178 }}
1179 }
1180 }
1181 }))
1182 }
1183}
1184
1185#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
1186struct HtlcCircuitKey {
1187 operation_id: OperationId,
1188 incoming_chan_id: u64,
1189 htlc_id: u64,
1190}
1191
1192impl HtlcCircuitKey {
1193 fn matches_state(self, state: &GatewayClientStateMachines) -> bool {
1194 matches!(
1195 state,
1196 GatewayClientStateMachines::Complete(sm)
1197 if sm.common.operation_id == self.operation_id
1198 && sm.common.incoming_chan_id == self.incoming_chan_id
1199 && sm.common.htlc_id == self.htlc_id
1200 )
1201 }
1202}
1203
1204#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1205pub enum GatewayClientStateMachines {
1206 Pay(GatewayPayStateMachine),
1207 Receive(IncomingStateMachine),
1208 Complete(GatewayCompleteStateMachine),
1209}
1210
1211impl fmt::Display for GatewayClientStateMachines {
1212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1213 match self {
1214 GatewayClientStateMachines::Pay(pay) => {
1215 write!(f, "{pay}")
1216 }
1217 GatewayClientStateMachines::Receive(receive) => {
1218 write!(f, "{receive}")
1219 }
1220 GatewayClientStateMachines::Complete(complete) => {
1221 write!(f, "{complete}")
1222 }
1223 }
1224 }
1225}
1226
1227impl IntoDynInstance for GatewayClientStateMachines {
1228 type DynType = DynState;
1229
1230 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1231 DynState::from_typed(instance_id, self)
1232 }
1233}
1234
1235impl State for GatewayClientStateMachines {
1236 type ModuleContext = GatewayClientContext;
1237
1238 fn transitions(
1239 &self,
1240 context: &Self::ModuleContext,
1241 global_context: &DynGlobalClientContext,
1242 ) -> Vec<StateTransition<Self>> {
1243 match self {
1244 GatewayClientStateMachines::Pay(pay_state) => {
1245 sm_enum_variant_translation!(
1246 pay_state.transitions(context, global_context),
1247 GatewayClientStateMachines::Pay
1248 )
1249 }
1250 GatewayClientStateMachines::Receive(receive_state) => {
1251 sm_enum_variant_translation!(
1252 receive_state.transitions(&context.into(), global_context),
1253 GatewayClientStateMachines::Receive
1254 )
1255 }
1256 GatewayClientStateMachines::Complete(complete_state) => {
1257 sm_enum_variant_translation!(
1258 complete_state.transitions(context, global_context),
1259 GatewayClientStateMachines::Complete
1260 )
1261 }
1262 }
1263 }
1264
1265 fn operation_id(&self) -> fedimint_core::core::OperationId {
1266 match self {
1267 GatewayClientStateMachines::Pay(pay_state) => pay_state.operation_id(),
1268 GatewayClientStateMachines::Receive(receive_state) => receive_state.operation_id(),
1269 GatewayClientStateMachines::Complete(complete_state) => complete_state.operation_id(),
1270 }
1271 }
1272}
1273
1274#[derive(Debug, Clone, Eq, PartialEq)]
1275pub struct Htlc {
1276 pub payment_hash: sha256::Hash,
1278 pub incoming_amount_msat: Amount,
1280 pub outgoing_amount_msat: Amount,
1282 pub incoming_expiry: u32,
1287 pub short_channel_id: Option<u64>,
1289 pub incoming_chan_id: u64,
1291 pub htlc_id: u64,
1293}
1294
1295#[derive(Debug, Error, Clone, Eq, PartialEq)]
1297#[error(
1298 "incoming HTLC expiry is unsafe: expiry {incoming_expiry}, current height {current_block_height}, required remaining blocks greater than {expiry_safety_margin}"
1299)]
1300pub struct UnsafeHtlcExpiry {
1301 pub incoming_expiry: u32,
1303 pub current_block_height: u32,
1305 pub expiry_safety_margin: u32,
1307}
1308
1309impl Htlc {
1310 pub fn ensure_safe_expiry(&self, current_block_height: u32) -> Result<(), UnsafeHtlcExpiry> {
1313 let remaining_blocks = self.incoming_expiry.saturating_sub(current_block_height);
1314 if LNV1_HTLC_EXPIRY_SAFETY_MARGIN < remaining_blocks {
1315 return Ok(());
1316 }
1317
1318 Err(UnsafeHtlcExpiry {
1319 incoming_expiry: self.incoming_expiry,
1320 current_block_height,
1321 expiry_safety_margin: LNV1_HTLC_EXPIRY_SAFETY_MARGIN,
1322 })
1323 }
1324}
1325
1326impl From<InterceptPaymentRequest> for Htlc {
1327 fn from(s: InterceptPaymentRequest) -> Self {
1328 Self {
1329 payment_hash: s.payment_hash,
1330 incoming_amount_msat: Amount::from_msats(s.incoming_amount_msat),
1335 outgoing_amount_msat: Amount::from_msats(s.amount_msat),
1336 incoming_expiry: s.expiry,
1337 short_channel_id: s.short_channel_id,
1338 incoming_chan_id: s.incoming_chan_id,
1339 htlc_id: s.htlc_id,
1340 }
1341 }
1342}
1343
1344#[derive(Debug, Clone)]
1345pub struct SwapParameters {
1346 pub payment_hash: sha256::Hash,
1347 pub amount_msat: Amount,
1348}
1349
1350impl TryFrom<PaymentData> for SwapParameters {
1351 type Error = MissingInvoiceAmountError;
1352
1353 fn try_from(s: PaymentData) -> Result<Self, Self::Error> {
1354 let payment_hash = s.payment_hash();
1355 let amount_msat = s.amount().ok_or(MissingInvoiceAmountError)?;
1356 Ok(Self {
1357 payment_hash,
1358 amount_msat,
1359 })
1360 }
1361}
1362
1363#[async_trait]
1369pub trait IGatewayClientV1: Debug + Send + Sync {
1370 async fn verify_preimage_authentication(
1376 &self,
1377 payment_hash: sha256::Hash,
1378 preimage_auth: sha256::Hash,
1379 contract: OutgoingContractAccount,
1380 ) -> Result<(), OutgoingPaymentError>;
1381
1382 async fn verify_pruned_invoice(
1390 &self,
1391 payment_data: PaymentData,
1392 ) -> Result<(), GatewayClientV1Error>;
1393
1394 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees>;
1396
1397 async fn begin_fresh_receive(
1406 &self,
1407 federation_id: FederationId,
1408 ) -> Option<fedimint_gateway_common::PaymentPolicyGuard>;
1409
1410 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>>;
1413
1414 async fn get_client_for_invoice(
1422 &self,
1423 payment_data: PaymentData,
1424 ) -> Option<Spanned<ClientHandleArc>>;
1425
1426 async fn pay(
1428 &self,
1429 payment_data: PaymentData,
1430 max_delay: u64,
1431 max_fee: Amount,
1432 ) -> Result<PayInvoiceResponse, LightningRpcError>;
1433
1434 async fn outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool;
1445
1446 async fn complete_htlc(
1458 &self,
1459 htlc_response: InterceptPaymentResponse,
1460 ) -> Result<(), LightningRpcError>;
1461
1462 async fn is_lnv2_direct_swap(
1471 &self,
1472 payment_hash: sha256::Hash,
1473 amount: Amount,
1474 ) -> Result<
1475 Option<(
1476 fedimint_lnv2_common::contracts::IncomingContract,
1477 ClientHandleArc,
1478 )>,
1479 GatewayClientV1Error,
1480 >;
1481
1482 async fn claim_payment_image(
1485 &self,
1486 payment_image: &fedimint_lnv2_common::contracts::PaymentImage,
1487 operation_id: OperationId,
1488 ) -> bool;
1489}