1mod complete;
2pub mod events;
3pub mod pay;
4
5use std::collections::BTreeMap;
6use std::fmt;
7use std::fmt::Debug;
8use std::sync::Arc;
9use std::time::Duration;
10
11use async_stream::stream;
12use async_trait::async_trait;
13use bitcoin::hashes::{Hash, sha256};
14use bitcoin::key::Secp256k1;
15use bitcoin::secp256k1::{All, PublicKey};
16use complete::{GatewayCompleteCommon, GatewayCompleteStates, WaitForPreimageState};
17use events::{IncomingPaymentStarted, OutgoingPaymentStarted};
18use fedimint_api_client::api::DynModuleApi;
19use fedimint_client::ClientHandleArc;
20use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
21use fedimint_client_module::module::recovery::NoModuleBackup;
22use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
23use fedimint_client_module::oplog::UpdateStreamOrOutcome;
24use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
25use fedimint_client_module::transaction::{
26 ClientOutput, ClientOutputBundle, ClientOutputSM, TransactionBuilder,
27};
28use fedimint_client_module::{
29 AddStateMachinesError, DynGlobalClientContext, sm_enum_variant_translation,
30};
31use fedimint_connectors::ConnectorRegistry;
32use fedimint_core::config::FederationId;
33use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
34use fedimint_core::db::{AutocommitError, DatabaseTransaction};
35use fedimint_core::encoding::{Decodable, Encodable};
36use fedimint_core::module::{Amounts, ApiVersion, ModuleInit, MultiApiVersion};
37use fedimint_core::util::{FmtCompact, SafeUrl, Spanned};
38use fedimint_core::{Amount, OutPoint, apply, async_trait_maybe_send, secp256k1};
39use fedimint_derive_secret::ChildId;
40use fedimint_lightning::{
41 InterceptPaymentRequest, InterceptPaymentResponse, LightningContext, LightningRpcError,
42 PayInvoiceResponse,
43};
44use fedimint_ln_client::api::LnFederationApi;
45use fedimint_ln_client::incoming::{
46 FundingOfferState, IncomingSmCommon, IncomingSmError, IncomingSmStates, IncomingStateMachine,
47};
48use fedimint_ln_client::pay::{PayInvoicePayload, PaymentData};
49use fedimint_ln_client::{
50 LightningClientContext, LightningClientInit, RealGatewayConnection,
51 create_incoming_contract_output,
52};
53use fedimint_ln_common::config::LightningClientConfig;
54use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
55use fedimint_ln_common::contracts::{ContractId, Preimage};
56use fedimint_ln_common::route_hints::RouteHint;
57use fedimint_ln_common::{
58 KIND, LightningCommonInit, LightningGateway, LightningGatewayAnnouncement,
59 LightningModuleTypes, LightningOutput, LightningOutputV0, RemoveGatewayRequest,
60 create_gateway_remove_message,
61};
62use fedimint_lnv2_common::GatewayApi;
63use futures::StreamExt;
64use lightning_invoice::RoutingFees;
65use secp256k1::Keypair;
66use serde::{Deserialize, Serialize};
67use tracing::{debug, error, info, warn};
68
69use self::complete::GatewayCompleteStateMachine;
70use self::pay::{
71 GatewayPayCommon, GatewayPayInvoice, GatewayPayStateMachine, GatewayPayStates,
72 OutgoingPaymentError,
73};
74
75#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
78pub enum GatewayExtPayStates {
79 Created,
80 Preimage {
81 preimage: Preimage,
82 },
83 Success {
84 preimage: Preimage,
85 out_points: Vec<OutPoint>,
86 },
87 Canceled {
88 error: OutgoingPaymentError,
89 },
90 Fail {
91 error: OutgoingPaymentError,
92 error_message: String,
93 },
94 OfferDoesNotExist {
95 contract_id: ContractId,
96 },
97}
98
99#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
102pub enum GatewayExtReceiveStates {
103 Funding,
104 Preimage(Preimage),
105 RefundSuccess {
106 out_points: Vec<OutPoint>,
107 error: IncomingSmError,
108 },
109 RefundError {
110 error_message: String,
111 error: IncomingSmError,
112 },
113 FundingFailed {
114 error: IncomingSmError,
115 },
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub enum GatewayMeta {
120 Pay,
121 Receive,
122}
123
124#[derive(Debug, Clone)]
125pub struct GatewayClientInit {
126 pub federation_index: u64,
127 pub lightning_manager: Arc<dyn IGatewayClientV1>,
128}
129
130impl ModuleInit for GatewayClientInit {
131 type Common = LightningCommonInit;
132
133 async fn dump_database(
134 &self,
135 _dbtx: &mut DatabaseTransaction<'_>,
136 _prefix_names: Vec<String>,
137 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
138 Box::new(vec![].into_iter())
139 }
140}
141
142#[apply(async_trait_maybe_send!)]
143impl ClientModuleInit for GatewayClientInit {
144 type Module = GatewayClientModule;
145
146 fn supported_api_versions(&self) -> MultiApiVersion {
147 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
148 .expect("no version conflicts")
149 }
150
151 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
152 Ok(GatewayClientModule {
153 cfg: args.cfg().clone(),
154 notifier: args.notifier().clone(),
155 redeem_key: args
156 .module_root_secret()
157 .child_key(ChildId(0))
158 .to_secp_key(&fedimint_core::secp256k1::Secp256k1::new()),
159 module_api: args.module_api().clone(),
160 federation_index: self.federation_index,
161 client_ctx: args.context(),
162 lightning_manager: self.lightning_manager.clone(),
163 connector_registry: args.connector_registry.clone(),
164 intercepted_htlc_lock_pool: lockable::LockPool::new(),
165 })
166 }
167}
168
169#[derive(Debug, Clone)]
170pub struct GatewayClientContext {
171 redeem_key: Keypair,
172 secp: Secp256k1<All>,
173 pub ln_decoder: Decoder,
174 notifier: ModuleNotifier<GatewayClientStateMachines>,
175 pub client_ctx: ClientContext<GatewayClientModule>,
176 pub lightning_manager: Arc<dyn IGatewayClientV1>,
177 pub connector_registry: ConnectorRegistry,
178}
179
180impl Context for GatewayClientContext {
181 const KIND: Option<ModuleKind> = Some(fedimint_ln_common::KIND);
182}
183
184impl From<&GatewayClientContext> for LightningClientContext {
185 fn from(ctx: &GatewayClientContext) -> Self {
186 let gateway_conn = RealGatewayConnection {
187 api: GatewayApi::new(None, ctx.connector_registry.clone()),
188 };
189 LightningClientContext {
190 ln_decoder: ctx.ln_decoder.clone(),
191 redeem_key: ctx.redeem_key,
192 gateway_conn: Arc::new(gateway_conn),
193 client_ctx: None,
194 }
195 }
196}
197
198pub struct GatewayClientModule {
203 cfg: LightningClientConfig,
204 pub notifier: ModuleNotifier<GatewayClientStateMachines>,
205 pub redeem_key: Keypair,
206 federation_index: u64,
207 module_api: DynModuleApi,
208 client_ctx: ClientContext<Self>,
209 pub lightning_manager: Arc<dyn IGatewayClientV1>,
210 connector_registry: ConnectorRegistry,
211 intercepted_htlc_lock_pool: lockable::LockPool<HtlcCircuitKey>,
212}
213
214impl fmt::Debug for GatewayClientModule {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 f.debug_struct("GatewayClientModule")
217 .finish_non_exhaustive()
218 }
219}
220
221impl ClientModule for GatewayClientModule {
222 type Init = LightningClientInit;
223 type Common = LightningModuleTypes;
224 type Backup = NoModuleBackup;
225 type ModuleStateMachineContext = GatewayClientContext;
226 type States = GatewayClientStateMachines;
227
228 fn context(&self) -> Self::ModuleStateMachineContext {
229 Self::ModuleStateMachineContext {
230 redeem_key: self.redeem_key,
231 secp: Secp256k1::new(),
232 ln_decoder: self.decoder(),
233 notifier: self.notifier.clone(),
234 client_ctx: self.client_ctx.clone(),
235 lightning_manager: self.lightning_manager.clone(),
236 connector_registry: self.connector_registry.clone(),
237 }
238 }
239
240 fn input_fee(
241 &self,
242 _amount: &Amounts,
243 _input: &<Self::Common as fedimint_core::module::ModuleCommon>::Input,
244 ) -> Option<Amounts> {
245 Some(Amounts::new_bitcoin(self.cfg.fee_consensus.contract_input))
246 }
247
248 fn output_fee(
249 &self,
250 _amount: &Amounts,
251 output: &<Self::Common as fedimint_core::module::ModuleCommon>::Output,
252 ) -> Option<Amounts> {
253 match output.maybe_v0_ref()? {
254 LightningOutputV0::Contract(_) => {
255 Some(Amounts::new_bitcoin(self.cfg.fee_consensus.contract_output))
256 }
257 LightningOutputV0::Offer(_) | LightningOutputV0::CancelOutgoing { .. } => {
258 Some(Amounts::ZERO)
259 }
260 }
261 }
262}
263
264impl GatewayClientModule {
265 fn to_gateway_registration_info(
266 &self,
267 route_hints: Vec<RouteHint>,
268 ttl: Duration,
269 fees: RoutingFees,
270 lightning_context: LightningContext,
271 api: SafeUrl,
272 gateway_id: PublicKey,
273 ) -> LightningGatewayAnnouncement {
274 LightningGatewayAnnouncement {
275 info: LightningGateway {
276 federation_index: self.federation_index,
277 gateway_redeem_key: self.redeem_key.public_key(),
278 node_pub_key: lightning_context.lightning_public_key,
279 lightning_alias: lightning_context.lightning_alias,
280 api,
281 route_hints,
282 fees,
283 gateway_id,
284 supports_private_payments: lightning_context.lnrpc.supports_private_payments(),
285 },
286 ttl,
287 vetted: false,
288 }
289 }
290
291 async fn create_funding_incoming_contract_output_from_htlc(
292 &self,
293 htlc: Htlc,
294 ) -> Result<
295 (
296 OperationId,
297 Amount,
298 ClientOutput<LightningOutputV0>,
299 ClientOutputSM<GatewayClientStateMachines>,
300 ContractId,
301 ),
302 IncomingSmError,
303 > {
304 let operation_id = OperationId(htlc.payment_hash.to_byte_array());
305 let (incoming_output, amount, contract_id) = create_incoming_contract_output(
306 &self.module_api,
307 htlc.payment_hash,
308 htlc.outgoing_amount_msat,
309 &self.redeem_key,
310 )
311 .await?;
312
313 let client_output = ClientOutput::<LightningOutputV0> {
314 output: incoming_output,
315 amounts: Amounts::new_bitcoin(amount),
316 };
317 let client_output_sm = ClientOutputSM::<GatewayClientStateMachines> {
318 state_machines: Arc::new(move |out_point_range: OutPointRange| {
319 assert_eq!(out_point_range.count(), 1);
320 vec![
321 GatewayClientStateMachines::Receive(IncomingStateMachine {
322 common: IncomingSmCommon {
323 operation_id,
324 contract_id,
325 payment_hash: htlc.payment_hash,
326 },
327 state: IncomingSmStates::FundingOffer(FundingOfferState {
328 txid: out_point_range.txid(),
329 }),
330 }),
331 GatewayClientStateMachines::Complete(GatewayCompleteStateMachine {
332 common: GatewayCompleteCommon {
333 operation_id,
334 payment_hash: htlc.payment_hash,
335 incoming_chan_id: htlc.incoming_chan_id,
336 htlc_id: htlc.htlc_id,
337 },
338 state: GatewayCompleteStates::WaitForPreimage(WaitForPreimageState),
339 }),
340 ]
341 }),
342 };
343 Ok((
344 operation_id,
345 amount,
346 client_output,
347 client_output_sm,
348 contract_id,
349 ))
350 }
351
352 async fn create_funding_incoming_contract_output_from_swap(
353 &self,
354 swap: SwapParameters,
355 ) -> Result<
356 (
357 OperationId,
358 ClientOutput<LightningOutputV0>,
359 ClientOutputSM<GatewayClientStateMachines>,
360 ),
361 IncomingSmError,
362 > {
363 let payment_hash = swap.payment_hash;
364 let operation_id = OperationId(payment_hash.to_byte_array());
365 let (incoming_output, amount, contract_id) = create_incoming_contract_output(
366 &self.module_api,
367 payment_hash,
368 swap.amount_msat,
369 &self.redeem_key,
370 )
371 .await?;
372
373 let client_output = ClientOutput::<LightningOutputV0> {
374 output: incoming_output,
375 amounts: Amounts::new_bitcoin(amount),
376 };
377 let client_output_sm = ClientOutputSM::<GatewayClientStateMachines> {
378 state_machines: Arc::new(move |out_point_range| {
379 assert_eq!(out_point_range.count(), 1);
380 vec![GatewayClientStateMachines::Receive(IncomingStateMachine {
381 common: IncomingSmCommon {
382 operation_id,
383 contract_id,
384 payment_hash,
385 },
386 state: IncomingSmStates::FundingOffer(FundingOfferState {
387 txid: out_point_range.txid(),
388 }),
389 })]
390 }),
391 };
392 Ok((operation_id, client_output, client_output_sm))
393 }
394
395 pub async fn try_register_with_federation(
397 &self,
398 route_hints: Vec<RouteHint>,
399 time_to_live: Duration,
400 fees: RoutingFees,
401 lightning_context: LightningContext,
402 api: SafeUrl,
403 gateway_id: PublicKey,
404 ) {
405 let registration_info = self.to_gateway_registration_info(
406 route_hints,
407 time_to_live,
408 fees,
409 lightning_context,
410 api,
411 gateway_id,
412 );
413 let gateway_id = registration_info.info.gateway_id;
414
415 let federation_id = self
416 .client_ctx
417 .get_config()
418 .await
419 .global
420 .calculate_federation_id();
421 match self.module_api.register_gateway(®istration_info).await {
422 Err(e) => {
423 warn!(
424 e = %e.fmt_compact(),
425 "Failed to register gateway {gateway_id} with federation {federation_id}"
426 );
427 }
428 _ => {
429 info!(
430 "Successfully registered gateway {gateway_id} with federation {federation_id}"
431 );
432 }
433 }
434 }
435
436 pub async fn remove_from_federation(&self, gateway_keypair: Keypair) {
441 if let Err(e) = self.remove_from_federation_inner(gateway_keypair).await {
444 let gateway_id = gateway_keypair.public_key();
445 let federation_id = self
446 .client_ctx
447 .get_config()
448 .await
449 .global
450 .calculate_federation_id();
451 warn!("Failed to remove gateway {gateway_id} from federation {federation_id}: {e:?}");
452 }
453 }
454
455 async fn remove_from_federation_inner(&self, gateway_keypair: Keypair) -> anyhow::Result<()> {
460 let gateway_id = gateway_keypair.public_key();
461 let challenges = self
462 .module_api
463 .get_remove_gateway_challenge(gateway_id)
464 .await;
465
466 let fed_public_key = self.cfg.threshold_pub_key;
467 let signatures = challenges
468 .into_iter()
469 .filter_map(|(peer_id, challenge)| {
470 let msg = create_gateway_remove_message(fed_public_key, peer_id, challenge?);
471 let signature = gateway_keypair.sign_schnorr(msg);
472 Some((peer_id, signature))
473 })
474 .collect::<BTreeMap<_, _>>();
475
476 let remove_gateway_request = RemoveGatewayRequest {
477 gateway_id,
478 signatures,
479 };
480
481 self.module_api.remove_gateway(remove_gateway_request).await;
482
483 Ok(())
484 }
485
486 pub async fn gateway_handle_intercepted_htlc(&self, htlc: Htlc) -> anyhow::Result<OperationId> {
498 debug!("Handling intercepted HTLC {htlc:?}");
499
500 let operation_id = OperationId(htlc.payment_hash.to_byte_array());
501 let circuit_key = HtlcCircuitKey {
502 operation_id,
503 incoming_chan_id: htlc.incoming_chan_id,
504 htlc_id: htlc.htlc_id,
505 };
506
507 let _circuit_lock_guard = self
510 .intercepted_htlc_lock_pool
511 .async_lock(circuit_key)
512 .await;
513
514 let replay_of_active_circuit = self
517 .client_ctx
518 .get_own_operation_active_states(operation_id)
519 .await
520 .into_iter()
521 .any(|(state, _)| circuit_key.matches_state(&state));
522 if replay_of_active_circuit {
523 debug!(
524 ?operation_id,
525 incoming_chan_id = htlc.incoming_chan_id,
526 htlc_id = htlc.htlc_id,
527 "HTLC circuit already being handled by an active completion state machine, treating as in-flight (likely an LND stream-reconnect replay)"
528 );
529 return Ok(operation_id);
530 }
531 let replay_of_inactive_circuit = self
532 .client_ctx
533 .get_own_operation_inactive_states(operation_id)
534 .await
535 .into_iter()
536 .any(|(state, _)| circuit_key.matches_state(&state));
537 if replay_of_inactive_circuit {
538 debug!(
539 ?operation_id,
540 incoming_chan_id = htlc.incoming_chan_id,
541 htlc_id = htlc.htlc_id,
542 "HTLC circuit was already handled by a completion state machine, treating as idempotent replay"
543 );
544 return Ok(operation_id);
545 }
546
547 let (op_id_from_funding, amount, client_output, client_output_sm, contract_id) = self
548 .create_funding_incoming_contract_output_from_htlc(htlc.clone())
549 .await?;
550 anyhow::ensure!(
553 op_id_from_funding == operation_id,
554 "operation id derivation must match: {op_id_from_funding:?} != {operation_id:?}"
555 );
556
557 let output = ClientOutput {
558 output: LightningOutput::V0(client_output.output),
559 amounts: Amounts::new_bitcoin(amount),
560 };
561
562 let tx = TransactionBuilder::new().with_outputs(self.client_ctx.make_client_outputs(
563 ClientOutputBundle::new(vec![output], vec![client_output_sm]),
564 ));
565 let operation_meta_gen = |_: OutPointRange| GatewayMeta::Receive;
566 self.client_ctx
567 .finalize_and_submit_transaction(operation_id, KIND.as_str(), operation_meta_gen, tx)
568 .await?;
569 debug!(?operation_id, "Submitted transaction for HTLC {htlc:?}");
570 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
571 self.client_ctx
572 .log_event(
573 &mut dbtx,
574 IncomingPaymentStarted {
575 contract_id,
576 payment_hash: htlc.payment_hash,
577 invoice_amount: htlc.outgoing_amount_msat,
578 contract_amount: amount,
579 operation_id,
580 },
581 )
582 .await;
583 dbtx.commit_tx().await;
584 Ok(operation_id)
585 }
586
587 pub async fn gateway_handle_direct_swap(
591 &self,
592 swap_params: SwapParameters,
593 ) -> anyhow::Result<OperationId> {
594 debug!("Handling direct swap {swap_params:?}");
595 let (operation_id, client_output, client_output_sm) = self
596 .create_funding_incoming_contract_output_from_swap(swap_params.clone())
597 .await?;
598
599 let output = ClientOutput {
600 output: LightningOutput::V0(client_output.output),
601 amounts: client_output.amounts,
602 };
603 let tx = TransactionBuilder::new().with_outputs(self.client_ctx.make_client_outputs(
604 ClientOutputBundle::new(vec![output], vec![client_output_sm]),
605 ));
606 let operation_meta_gen = |_: OutPointRange| GatewayMeta::Receive;
607 self.client_ctx
608 .finalize_and_submit_transaction(operation_id, KIND.as_str(), operation_meta_gen, tx)
609 .await?;
610 debug!(
611 ?operation_id,
612 "Submitted transaction for direct swap {swap_params:?}"
613 );
614 Ok(operation_id)
615 }
616
617 pub async fn gateway_subscribe_ln_receive(
620 &self,
621 operation_id: OperationId,
622 ) -> anyhow::Result<UpdateStreamOrOutcome<GatewayExtReceiveStates>> {
623 let operation = self.client_ctx.get_operation(operation_id).await?;
624 let mut stream = self.notifier.subscribe(operation_id).await;
625 let client_ctx = self.client_ctx.clone();
626
627 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
628 stream! {
629
630 yield GatewayExtReceiveStates::Funding;
631
632 let state = loop {
633 debug!("Getting next ln receive state for {}", operation_id.fmt_short());
634 if let Some(GatewayClientStateMachines::Receive(state)) = stream.next().await {
635 match state.state {
636 IncomingSmStates::Preimage(preimage) =>{
637 debug!(?operation_id, "Received preimage");
638 break GatewayExtReceiveStates::Preimage(preimage)
639 },
640 IncomingSmStates::RefundSubmitted { out_points, error } => {
641 debug!(?operation_id, "Refund submitted for {out_points:?} {error}");
642 match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
643 Ok(()) => {
644 debug!(?operation_id, "Refund success");
645 break GatewayExtReceiveStates::RefundSuccess { out_points, error }
646 },
647 Err(e) => {
648 warn!(?operation_id, "Got failure {e:?} while awaiting for refund outputs {out_points:?}");
649 break GatewayExtReceiveStates::RefundError{ error_message: e.to_string(), error }
650 },
651 }
652 },
653 IncomingSmStates::FundingFailed { error } => {
654 warn!(?operation_id, "Funding failed: {error:?}");
655 break GatewayExtReceiveStates::FundingFailed{ error }
656 },
657 other => {
658 debug!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
659 }
660 }
661 }
662 };
663 yield state;
664 }
665 }))
666 }
667
668 pub async fn await_completion(&self, operation_id: OperationId) {
671 let mut stream = self.notifier.subscribe(operation_id).await;
672 loop {
673 match stream.next().await {
674 Some(GatewayClientStateMachines::Complete(state)) => match state.state {
675 GatewayCompleteStates::HtlcFinished => {
676 info!(%state, "LNv1 completion state machine finished");
677 return;
678 }
679 GatewayCompleteStates::Failure => {
680 error!(%state, "LNv1 completion state machine failed");
681 return;
682 }
683 _ => {
684 info!(%state, "Waiting for LNv1 completion state machine");
685 continue;
686 }
687 },
688 Some(GatewayClientStateMachines::Receive(state)) => {
689 info!(%state, "Waiting for LNv1 completion state machine");
690 continue;
691 }
692 Some(state) => {
693 warn!(%state, "Operation is not an LNv1 completion state machine");
694 return;
695 }
696 None => return,
697 }
698 }
699 }
700
701 pub async fn gateway_pay_bolt11_invoice(
703 &self,
704 pay_invoice_payload: PayInvoicePayload,
705 ) -> anyhow::Result<OperationId> {
706 let payload = pay_invoice_payload.clone();
707 self.lightning_manager
708 .verify_pruned_invoice(pay_invoice_payload.payment_data)
709 .await?;
710
711 self.client_ctx.module_db()
712 .autocommit(
713 |dbtx, _| {
714 Box::pin(async {
715 let operation_id = OperationId(payload.contract_id.to_byte_array());
716
717 self.client_ctx.log_event(dbtx, OutgoingPaymentStarted {
718 contract_id: payload.contract_id,
719 invoice_amount: payload.payment_data.amount().expect("LNv1 invoices should have an amount"),
720 operation_id,
721 }).await;
722
723 let state_machines =
724 vec![GatewayClientStateMachines::Pay(GatewayPayStateMachine {
725 common: GatewayPayCommon { operation_id },
726 state: GatewayPayStates::PayInvoice(GatewayPayInvoice {
727 pay_invoice_payload: payload.clone(),
728 }),
729 })];
730
731 let dyn_states = state_machines
732 .into_iter()
733 .map(|s| self.client_ctx.make_dyn(s))
734 .collect();
735
736 match self.client_ctx.add_state_machines_dbtx(dbtx, dyn_states).await {
737 Ok(()) => {
738 self.client_ctx
739 .add_operation_log_entry_dbtx(
740 dbtx,
741 operation_id,
742 KIND.as_str(),
743 GatewayMeta::Pay,
744 )
745 .await;
746 }
747 Err(AddStateMachinesError::StateAlreadyExists) => {
748 info!("State machine for operation {} already exists, will not add a new one", operation_id.fmt_short());
749 }
750 Err(other) => {
751 anyhow::bail!("Failed to add state machines: {other:?}")
752 }
753 }
754 Ok(operation_id)
755 })
756 },
757 Some(100),
758 )
759 .await
760 .map_err(|e| match e {
761 AutocommitError::ClosureError { error, .. } => error,
762 AutocommitError::CommitFailed { last_error, .. } => {
763 anyhow::anyhow!("Commit to DB failed: {last_error}")
764 }
765 })
766 }
767
768 pub async fn gateway_subscribe_ln_pay(
769 &self,
770 operation_id: OperationId,
771 ) -> anyhow::Result<UpdateStreamOrOutcome<GatewayExtPayStates>> {
772 let mut stream = self.notifier.subscribe(operation_id).await;
773 let operation = self.client_ctx.get_operation(operation_id).await?;
774 let client_ctx = self.client_ctx.clone();
775
776 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
777 stream! {
778 yield GatewayExtPayStates::Created;
779
780 loop {
781 debug!("Getting next ln pay state for {}", operation_id.fmt_short());
782 match stream.next().await { Some(GatewayClientStateMachines::Pay(state)) => {
783 match state.state {
784 GatewayPayStates::Preimage(out_points, preimage) => {
785 yield GatewayExtPayStates::Preimage{ preimage: preimage.clone() };
786
787 match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
788 Ok(()) => {
789 debug!(?operation_id, "Success");
790 yield GatewayExtPayStates::Success{ preimage: preimage.clone(), out_points };
791 return;
792
793 }
794 Err(e) => {
795 warn!(?operation_id, "Got failure {e:?} while awaiting for outputs {out_points:?}");
796 }
798 }
799 }
800 GatewayPayStates::Canceled { txid, contract_id, error } => {
801 debug!(?operation_id, "Trying to cancel contract {contract_id:?} due to {error:?}");
802 match client_ctx.transaction_updates(operation_id).await.await_tx_accepted(txid).await {
803 Ok(()) => {
804 debug!(?operation_id, "Canceled contract {contract_id:?} due to {error:?}");
805 yield GatewayExtPayStates::Canceled{ error };
806 return;
807 }
808 Err(e) => {
809 warn!(?operation_id, "Got failure {e:?} while awaiting for transaction {txid} to be accepted for");
810 yield GatewayExtPayStates::Fail { error, error_message: format!("Refund transaction {txid} was not accepted by the federation. OperationId: {} Error: {e:?}", operation_id.fmt_short()) };
811 }
812 }
813 }
814 GatewayPayStates::OfferDoesNotExist(contract_id) => {
815 warn!("Yielding OfferDoesNotExist state for {} and contract {contract_id}", operation_id.fmt_short());
816 yield GatewayExtPayStates::OfferDoesNotExist { contract_id };
817 }
818 GatewayPayStates::Failed{ error, error_message } => {
819 warn!("Yielding Fail state for {} due to {error:?} {error_message:?}", operation_id.fmt_short());
820 yield GatewayExtPayStates::Fail{ error, error_message };
821 },
822 GatewayPayStates::PayInvoice(_) => {
823 debug!("Got initial state PayInvoice while awaiting for output of {}", operation_id.fmt_short());
824 }
825 other => {
826 info!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
827 }
828 }
829 } _ => {
830 warn!("Got None while getting next ln pay state for {}", operation_id.fmt_short());
831 }}
832 }
833 }
834 }))
835 }
836}
837
838#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
839struct HtlcCircuitKey {
840 operation_id: OperationId,
841 incoming_chan_id: u64,
842 htlc_id: u64,
843}
844
845impl HtlcCircuitKey {
846 fn matches_state(self, state: &GatewayClientStateMachines) -> bool {
847 matches!(
848 state,
849 GatewayClientStateMachines::Complete(sm)
850 if sm.common.operation_id == self.operation_id
851 && sm.common.incoming_chan_id == self.incoming_chan_id
852 && sm.common.htlc_id == self.htlc_id
853 )
854 }
855}
856
857#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
858pub enum GatewayClientStateMachines {
859 Pay(GatewayPayStateMachine),
860 Receive(IncomingStateMachine),
861 Complete(GatewayCompleteStateMachine),
862}
863
864impl fmt::Display for GatewayClientStateMachines {
865 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
866 match self {
867 GatewayClientStateMachines::Pay(pay) => {
868 write!(f, "{pay}")
869 }
870 GatewayClientStateMachines::Receive(receive) => {
871 write!(f, "{receive}")
872 }
873 GatewayClientStateMachines::Complete(complete) => {
874 write!(f, "{complete}")
875 }
876 }
877 }
878}
879
880impl IntoDynInstance for GatewayClientStateMachines {
881 type DynType = DynState;
882
883 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
884 DynState::from_typed(instance_id, self)
885 }
886}
887
888impl State for GatewayClientStateMachines {
889 type ModuleContext = GatewayClientContext;
890
891 fn transitions(
892 &self,
893 context: &Self::ModuleContext,
894 global_context: &DynGlobalClientContext,
895 ) -> Vec<StateTransition<Self>> {
896 match self {
897 GatewayClientStateMachines::Pay(pay_state) => {
898 sm_enum_variant_translation!(
899 pay_state.transitions(context, global_context),
900 GatewayClientStateMachines::Pay
901 )
902 }
903 GatewayClientStateMachines::Receive(receive_state) => {
904 sm_enum_variant_translation!(
905 receive_state.transitions(&context.into(), global_context),
906 GatewayClientStateMachines::Receive
907 )
908 }
909 GatewayClientStateMachines::Complete(complete_state) => {
910 sm_enum_variant_translation!(
911 complete_state.transitions(context, global_context),
912 GatewayClientStateMachines::Complete
913 )
914 }
915 }
916 }
917
918 fn operation_id(&self) -> fedimint_core::core::OperationId {
919 match self {
920 GatewayClientStateMachines::Pay(pay_state) => pay_state.operation_id(),
921 GatewayClientStateMachines::Receive(receive_state) => receive_state.operation_id(),
922 GatewayClientStateMachines::Complete(complete_state) => complete_state.operation_id(),
923 }
924 }
925}
926
927#[derive(Debug, Clone, Eq, PartialEq)]
928pub struct Htlc {
929 pub payment_hash: sha256::Hash,
931 pub incoming_amount_msat: Amount,
933 pub outgoing_amount_msat: Amount,
935 pub incoming_expiry: u32,
937 pub short_channel_id: Option<u64>,
939 pub incoming_chan_id: u64,
941 pub htlc_id: u64,
943}
944
945impl TryFrom<InterceptPaymentRequest> for Htlc {
946 type Error = anyhow::Error;
947
948 fn try_from(s: InterceptPaymentRequest) -> Result<Self, Self::Error> {
949 Ok(Self {
950 payment_hash: s.payment_hash,
951 incoming_amount_msat: Amount::from_msats(s.amount_msat),
952 outgoing_amount_msat: Amount::from_msats(s.amount_msat),
953 incoming_expiry: s.expiry,
954 short_channel_id: s.short_channel_id,
955 incoming_chan_id: s.incoming_chan_id,
956 htlc_id: s.htlc_id,
957 })
958 }
959}
960
961#[derive(Debug, Clone)]
962pub struct SwapParameters {
963 pub payment_hash: sha256::Hash,
964 pub amount_msat: Amount,
965}
966
967impl TryFrom<PaymentData> for SwapParameters {
968 type Error = anyhow::Error;
969
970 fn try_from(s: PaymentData) -> Result<Self, Self::Error> {
971 let payment_hash = s.payment_hash();
972 let amount_msat = s
973 .amount()
974 .ok_or_else(|| anyhow::anyhow!("Amountless invoice cannot be used in direct swap"))?;
975 Ok(Self {
976 payment_hash,
977 amount_msat,
978 })
979 }
980}
981
982#[async_trait]
988pub trait IGatewayClientV1: Debug + Send + Sync {
989 async fn verify_preimage_authentication(
995 &self,
996 payment_hash: sha256::Hash,
997 preimage_auth: sha256::Hash,
998 contract: OutgoingContractAccount,
999 ) -> Result<(), OutgoingPaymentError>;
1000
1001 async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()>;
1004
1005 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees>;
1007
1008 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>>;
1011
1012 async fn get_client_for_invoice(
1020 &self,
1021 payment_data: PaymentData,
1022 ) -> Option<Spanned<ClientHandleArc>>;
1023
1024 async fn pay(
1026 &self,
1027 payment_data: PaymentData,
1028 max_delay: u64,
1029 max_fee: Amount,
1030 ) -> Result<PayInvoiceResponse, LightningRpcError>;
1031
1032 async fn complete_htlc(
1034 &self,
1035 htlc_response: InterceptPaymentResponse,
1036 ) -> Result<(), LightningRpcError>;
1037
1038 async fn is_lnv2_direct_swap(
1041 &self,
1042 payment_hash: sha256::Hash,
1043 amount: Amount,
1044 ) -> anyhow::Result<
1045 Option<(
1046 fedimint_lnv2_common::contracts::IncomingContract,
1047 ClientHandleArc,
1048 )>,
1049 >;
1050}