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 })
165 }
166}
167
168#[derive(Debug, Clone)]
169pub struct GatewayClientContext {
170 redeem_key: Keypair,
171 secp: Secp256k1<All>,
172 pub ln_decoder: Decoder,
173 notifier: ModuleNotifier<GatewayClientStateMachines>,
174 pub client_ctx: ClientContext<GatewayClientModule>,
175 pub lightning_manager: Arc<dyn IGatewayClientV1>,
176 pub connector_registry: ConnectorRegistry,
177}
178
179impl Context for GatewayClientContext {
180 const KIND: Option<ModuleKind> = Some(fedimint_ln_common::KIND);
181}
182
183impl From<&GatewayClientContext> for LightningClientContext {
184 fn from(ctx: &GatewayClientContext) -> Self {
185 let gateway_conn = RealGatewayConnection {
186 api: GatewayApi::new(None, ctx.connector_registry.clone()),
187 };
188 LightningClientContext {
189 ln_decoder: ctx.ln_decoder.clone(),
190 redeem_key: ctx.redeem_key,
191 gateway_conn: Arc::new(gateway_conn),
192 }
193 }
194}
195
196#[derive(Debug)]
201pub struct GatewayClientModule {
202 cfg: LightningClientConfig,
203 pub notifier: ModuleNotifier<GatewayClientStateMachines>,
204 pub redeem_key: Keypair,
205 federation_index: u64,
206 module_api: DynModuleApi,
207 client_ctx: ClientContext<Self>,
208 pub lightning_manager: Arc<dyn IGatewayClientV1>,
209 connector_registry: ConnectorRegistry,
210}
211
212impl ClientModule for GatewayClientModule {
213 type Init = LightningClientInit;
214 type Common = LightningModuleTypes;
215 type Backup = NoModuleBackup;
216 type ModuleStateMachineContext = GatewayClientContext;
217 type States = GatewayClientStateMachines;
218
219 fn context(&self) -> Self::ModuleStateMachineContext {
220 Self::ModuleStateMachineContext {
221 redeem_key: self.redeem_key,
222 secp: Secp256k1::new(),
223 ln_decoder: self.decoder(),
224 notifier: self.notifier.clone(),
225 client_ctx: self.client_ctx.clone(),
226 lightning_manager: self.lightning_manager.clone(),
227 connector_registry: self.connector_registry.clone(),
228 }
229 }
230
231 fn input_fee(
232 &self,
233 _amount: &Amounts,
234 _input: &<Self::Common as fedimint_core::module::ModuleCommon>::Input,
235 ) -> Option<Amounts> {
236 Some(Amounts::new_bitcoin(self.cfg.fee_consensus.contract_input))
237 }
238
239 fn output_fee(
240 &self,
241 _amount: &Amounts,
242 output: &<Self::Common as fedimint_core::module::ModuleCommon>::Output,
243 ) -> Option<Amounts> {
244 match output.maybe_v0_ref()? {
245 LightningOutputV0::Contract(_) => {
246 Some(Amounts::new_bitcoin(self.cfg.fee_consensus.contract_output))
247 }
248 LightningOutputV0::Offer(_) | LightningOutputV0::CancelOutgoing { .. } => {
249 Some(Amounts::ZERO)
250 }
251 }
252 }
253}
254
255impl GatewayClientModule {
256 fn to_gateway_registration_info(
257 &self,
258 route_hints: Vec<RouteHint>,
259 ttl: Duration,
260 fees: RoutingFees,
261 lightning_context: LightningContext,
262 api: SafeUrl,
263 gateway_id: PublicKey,
264 ) -> LightningGatewayAnnouncement {
265 LightningGatewayAnnouncement {
266 info: LightningGateway {
267 federation_index: self.federation_index,
268 gateway_redeem_key: self.redeem_key.public_key(),
269 node_pub_key: lightning_context.lightning_public_key,
270 lightning_alias: lightning_context.lightning_alias,
271 api,
272 route_hints,
273 fees,
274 gateway_id,
275 supports_private_payments: lightning_context.lnrpc.supports_private_payments(),
276 },
277 ttl,
278 vetted: false,
279 }
280 }
281
282 async fn create_funding_incoming_contract_output_from_htlc(
283 &self,
284 htlc: Htlc,
285 ) -> Result<
286 (
287 OperationId,
288 Amount,
289 ClientOutput<LightningOutputV0>,
290 ClientOutputSM<GatewayClientStateMachines>,
291 ContractId,
292 ),
293 IncomingSmError,
294 > {
295 let operation_id = OperationId(htlc.payment_hash.to_byte_array());
296 let (incoming_output, amount, contract_id) = create_incoming_contract_output(
297 &self.module_api,
298 htlc.payment_hash,
299 htlc.outgoing_amount_msat,
300 &self.redeem_key,
301 )
302 .await?;
303
304 let client_output = ClientOutput::<LightningOutputV0> {
305 output: incoming_output,
306 amounts: Amounts::new_bitcoin(amount),
307 };
308 let client_output_sm = ClientOutputSM::<GatewayClientStateMachines> {
309 state_machines: Arc::new(move |out_point_range: OutPointRange| {
310 assert_eq!(out_point_range.count(), 1);
311 vec![
312 GatewayClientStateMachines::Receive(IncomingStateMachine {
313 common: IncomingSmCommon {
314 operation_id,
315 contract_id,
316 payment_hash: htlc.payment_hash,
317 },
318 state: IncomingSmStates::FundingOffer(FundingOfferState {
319 txid: out_point_range.txid(),
320 }),
321 }),
322 GatewayClientStateMachines::Complete(GatewayCompleteStateMachine {
323 common: GatewayCompleteCommon {
324 operation_id,
325 payment_hash: htlc.payment_hash,
326 incoming_chan_id: htlc.incoming_chan_id,
327 htlc_id: htlc.htlc_id,
328 },
329 state: GatewayCompleteStates::WaitForPreimage(WaitForPreimageState),
330 }),
331 ]
332 }),
333 };
334 Ok((
335 operation_id,
336 amount,
337 client_output,
338 client_output_sm,
339 contract_id,
340 ))
341 }
342
343 async fn create_funding_incoming_contract_output_from_swap(
344 &self,
345 swap: SwapParameters,
346 ) -> Result<
347 (
348 OperationId,
349 ClientOutput<LightningOutputV0>,
350 ClientOutputSM<GatewayClientStateMachines>,
351 ),
352 IncomingSmError,
353 > {
354 let payment_hash = swap.payment_hash;
355 let operation_id = OperationId(payment_hash.to_byte_array());
356 let (incoming_output, amount, contract_id) = create_incoming_contract_output(
357 &self.module_api,
358 payment_hash,
359 swap.amount_msat,
360 &self.redeem_key,
361 )
362 .await?;
363
364 let client_output = ClientOutput::<LightningOutputV0> {
365 output: incoming_output,
366 amounts: Amounts::new_bitcoin(amount),
367 };
368 let client_output_sm = ClientOutputSM::<GatewayClientStateMachines> {
369 state_machines: Arc::new(move |out_point_range| {
370 assert_eq!(out_point_range.count(), 1);
371 vec![GatewayClientStateMachines::Receive(IncomingStateMachine {
372 common: IncomingSmCommon {
373 operation_id,
374 contract_id,
375 payment_hash,
376 },
377 state: IncomingSmStates::FundingOffer(FundingOfferState {
378 txid: out_point_range.txid(),
379 }),
380 })]
381 }),
382 };
383 Ok((operation_id, client_output, client_output_sm))
384 }
385
386 pub async fn try_register_with_federation(
388 &self,
389 route_hints: Vec<RouteHint>,
390 time_to_live: Duration,
391 fees: RoutingFees,
392 lightning_context: LightningContext,
393 api: SafeUrl,
394 gateway_id: PublicKey,
395 ) {
396 let registration_info = self.to_gateway_registration_info(
397 route_hints,
398 time_to_live,
399 fees,
400 lightning_context,
401 api,
402 gateway_id,
403 );
404 let gateway_id = registration_info.info.gateway_id;
405
406 let federation_id = self
407 .client_ctx
408 .get_config()
409 .await
410 .global
411 .calculate_federation_id();
412 match self.module_api.register_gateway(®istration_info).await {
413 Err(e) => {
414 warn!(
415 e = %e.fmt_compact(),
416 "Failed to register gateway {gateway_id} with federation {federation_id}"
417 );
418 }
419 _ => {
420 info!(
421 "Successfully registered gateway {gateway_id} with federation {federation_id}"
422 );
423 }
424 }
425 }
426
427 pub async fn remove_from_federation(&self, gateway_keypair: Keypair) {
432 if let Err(e) = self.remove_from_federation_inner(gateway_keypair).await {
435 let gateway_id = gateway_keypair.public_key();
436 let federation_id = self
437 .client_ctx
438 .get_config()
439 .await
440 .global
441 .calculate_federation_id();
442 warn!("Failed to remove gateway {gateway_id} from federation {federation_id}: {e:?}");
443 }
444 }
445
446 async fn remove_from_federation_inner(&self, gateway_keypair: Keypair) -> anyhow::Result<()> {
451 let gateway_id = gateway_keypair.public_key();
452 let challenges = self
453 .module_api
454 .get_remove_gateway_challenge(gateway_id)
455 .await;
456
457 let fed_public_key = self.cfg.threshold_pub_key;
458 let signatures = challenges
459 .into_iter()
460 .filter_map(|(peer_id, challenge)| {
461 let msg = create_gateway_remove_message(fed_public_key, peer_id, challenge?);
462 let signature = gateway_keypair.sign_schnorr(msg);
463 Some((peer_id, signature))
464 })
465 .collect::<BTreeMap<_, _>>();
466
467 let remove_gateway_request = RemoveGatewayRequest {
468 gateway_id,
469 signatures,
470 };
471
472 self.module_api.remove_gateway(remove_gateway_request).await;
473
474 Ok(())
475 }
476
477 pub async fn gateway_handle_intercepted_htlc(&self, htlc: Htlc) -> anyhow::Result<OperationId> {
479 debug!("Handling intercepted HTLC {htlc:?}");
480 let (operation_id, amount, client_output, client_output_sm, contract_id) = self
481 .create_funding_incoming_contract_output_from_htlc(htlc.clone())
482 .await?;
483
484 let output = ClientOutput {
485 output: LightningOutput::V0(client_output.output),
486 amounts: Amounts::new_bitcoin(amount),
487 };
488
489 let tx = TransactionBuilder::new().with_outputs(self.client_ctx.make_client_outputs(
490 ClientOutputBundle::new(vec![output], vec![client_output_sm]),
491 ));
492 let operation_meta_gen = |_: OutPointRange| GatewayMeta::Receive;
493 self.client_ctx
494 .finalize_and_submit_transaction(operation_id, KIND.as_str(), operation_meta_gen, tx)
495 .await?;
496 debug!(?operation_id, "Submitted transaction for HTLC {htlc:?}");
497 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
498 self.client_ctx
499 .log_event(
500 &mut dbtx,
501 IncomingPaymentStarted {
502 contract_id,
503 payment_hash: htlc.payment_hash,
504 invoice_amount: htlc.outgoing_amount_msat,
505 contract_amount: amount,
506 operation_id,
507 },
508 )
509 .await;
510 dbtx.commit_tx().await;
511 Ok(operation_id)
512 }
513
514 pub async fn gateway_handle_direct_swap(
518 &self,
519 swap_params: SwapParameters,
520 ) -> anyhow::Result<OperationId> {
521 debug!("Handling direct swap {swap_params:?}");
522 let (operation_id, client_output, client_output_sm) = self
523 .create_funding_incoming_contract_output_from_swap(swap_params.clone())
524 .await?;
525
526 let output = ClientOutput {
527 output: LightningOutput::V0(client_output.output),
528 amounts: client_output.amounts,
529 };
530 let tx = TransactionBuilder::new().with_outputs(self.client_ctx.make_client_outputs(
531 ClientOutputBundle::new(vec![output], vec![client_output_sm]),
532 ));
533 let operation_meta_gen = |_: OutPointRange| GatewayMeta::Receive;
534 self.client_ctx
535 .finalize_and_submit_transaction(operation_id, KIND.as_str(), operation_meta_gen, tx)
536 .await?;
537 debug!(
538 ?operation_id,
539 "Submitted transaction for direct swap {swap_params:?}"
540 );
541 Ok(operation_id)
542 }
543
544 pub async fn gateway_subscribe_ln_receive(
547 &self,
548 operation_id: OperationId,
549 ) -> anyhow::Result<UpdateStreamOrOutcome<GatewayExtReceiveStates>> {
550 let operation = self.client_ctx.get_operation(operation_id).await?;
551 let mut stream = self.notifier.subscribe(operation_id).await;
552 let client_ctx = self.client_ctx.clone();
553
554 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
555 stream! {
556
557 yield GatewayExtReceiveStates::Funding;
558
559 let state = loop {
560 debug!("Getting next ln receive state for {}", operation_id.fmt_short());
561 if let Some(GatewayClientStateMachines::Receive(state)) = stream.next().await {
562 match state.state {
563 IncomingSmStates::Preimage(preimage) =>{
564 debug!(?operation_id, "Received preimage");
565 break GatewayExtReceiveStates::Preimage(preimage)
566 },
567 IncomingSmStates::RefundSubmitted { out_points, error } => {
568 debug!(?operation_id, "Refund submitted for {out_points:?} {error}");
569 match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
570 Ok(()) => {
571 debug!(?operation_id, "Refund success");
572 break GatewayExtReceiveStates::RefundSuccess { out_points, error }
573 },
574 Err(e) => {
575 warn!(?operation_id, "Got failure {e:?} while awaiting for refund outputs {out_points:?}");
576 break GatewayExtReceiveStates::RefundError{ error_message: e.to_string(), error }
577 },
578 }
579 },
580 IncomingSmStates::FundingFailed { error } => {
581 warn!(?operation_id, "Funding failed: {error:?}");
582 break GatewayExtReceiveStates::FundingFailed{ error }
583 },
584 other => {
585 debug!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
586 }
587 }
588 }
589 };
590 yield state;
591 }
592 }))
593 }
594
595 pub async fn await_completion(&self, operation_id: OperationId) {
598 let mut stream = self.notifier.subscribe(operation_id).await;
599 loop {
600 match stream.next().await {
601 Some(GatewayClientStateMachines::Complete(state)) => match state.state {
602 GatewayCompleteStates::HtlcFinished => {
603 info!(%state, "LNv1 completion state machine finished");
604 return;
605 }
606 GatewayCompleteStates::Failure => {
607 error!(%state, "LNv1 completion state machine failed");
608 return;
609 }
610 _ => {
611 info!(%state, "Waiting for LNv1 completion state machine");
612 continue;
613 }
614 },
615 Some(GatewayClientStateMachines::Receive(state)) => {
616 info!(%state, "Waiting for LNv1 completion state machine");
617 continue;
618 }
619 Some(state) => {
620 warn!(%state, "Operation is not an LNv1 completion state machine");
621 return;
622 }
623 None => return,
624 }
625 }
626 }
627
628 pub async fn gateway_pay_bolt11_invoice(
630 &self,
631 pay_invoice_payload: PayInvoicePayload,
632 ) -> anyhow::Result<OperationId> {
633 let payload = pay_invoice_payload.clone();
634 self.lightning_manager
635 .verify_pruned_invoice(pay_invoice_payload.payment_data)
636 .await?;
637
638 self.client_ctx.module_db()
639 .autocommit(
640 |dbtx, _| {
641 Box::pin(async {
642 let operation_id = OperationId(payload.contract_id.to_byte_array());
643
644 self.client_ctx.log_event(dbtx, OutgoingPaymentStarted {
645 contract_id: payload.contract_id,
646 invoice_amount: payload.payment_data.amount().expect("LNv1 invoices should have an amount"),
647 operation_id,
648 }).await;
649
650 let state_machines =
651 vec![GatewayClientStateMachines::Pay(GatewayPayStateMachine {
652 common: GatewayPayCommon { operation_id },
653 state: GatewayPayStates::PayInvoice(GatewayPayInvoice {
654 pay_invoice_payload: payload.clone(),
655 }),
656 })];
657
658 let dyn_states = state_machines
659 .into_iter()
660 .map(|s| self.client_ctx.make_dyn(s))
661 .collect();
662
663 match self.client_ctx.add_state_machines_dbtx(dbtx, dyn_states).await {
664 Ok(()) => {
665 self.client_ctx
666 .add_operation_log_entry_dbtx(
667 dbtx,
668 operation_id,
669 KIND.as_str(),
670 GatewayMeta::Pay,
671 )
672 .await;
673 }
674 Err(AddStateMachinesError::StateAlreadyExists) => {
675 info!("State machine for operation {} already exists, will not add a new one", operation_id.fmt_short());
676 }
677 Err(other) => {
678 anyhow::bail!("Failed to add state machines: {other:?}")
679 }
680 }
681 Ok(operation_id)
682 })
683 },
684 Some(100),
685 )
686 .await
687 .map_err(|e| match e {
688 AutocommitError::ClosureError { error, .. } => error,
689 AutocommitError::CommitFailed { last_error, .. } => {
690 anyhow::anyhow!("Commit to DB failed: {last_error}")
691 }
692 })
693 }
694
695 pub async fn gateway_subscribe_ln_pay(
696 &self,
697 operation_id: OperationId,
698 ) -> anyhow::Result<UpdateStreamOrOutcome<GatewayExtPayStates>> {
699 let mut stream = self.notifier.subscribe(operation_id).await;
700 let operation = self.client_ctx.get_operation(operation_id).await?;
701 let client_ctx = self.client_ctx.clone();
702
703 Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
704 stream! {
705 yield GatewayExtPayStates::Created;
706
707 loop {
708 debug!("Getting next ln pay state for {}", operation_id.fmt_short());
709 match stream.next().await { Some(GatewayClientStateMachines::Pay(state)) => {
710 match state.state {
711 GatewayPayStates::Preimage(out_points, preimage) => {
712 yield GatewayExtPayStates::Preimage{ preimage: preimage.clone() };
713
714 match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
715 Ok(()) => {
716 debug!(?operation_id, "Success");
717 yield GatewayExtPayStates::Success{ preimage: preimage.clone(), out_points };
718 return;
719
720 }
721 Err(e) => {
722 warn!(?operation_id, "Got failure {e:?} while awaiting for outputs {out_points:?}");
723 }
725 }
726 }
727 GatewayPayStates::Canceled { txid, contract_id, error } => {
728 debug!(?operation_id, "Trying to cancel contract {contract_id:?} due to {error:?}");
729 match client_ctx.transaction_updates(operation_id).await.await_tx_accepted(txid).await {
730 Ok(()) => {
731 debug!(?operation_id, "Canceled contract {contract_id:?} due to {error:?}");
732 yield GatewayExtPayStates::Canceled{ error };
733 return;
734 }
735 Err(e) => {
736 warn!(?operation_id, "Got failure {e:?} while awaiting for transaction {txid} to be accepted for");
737 yield GatewayExtPayStates::Fail { error, error_message: format!("Refund transaction {txid} was not accepted by the federation. OperationId: {} Error: {e:?}", operation_id.fmt_short()) };
738 }
739 }
740 }
741 GatewayPayStates::OfferDoesNotExist(contract_id) => {
742 warn!("Yielding OfferDoesNotExist state for {} and contract {contract_id}", operation_id.fmt_short());
743 yield GatewayExtPayStates::OfferDoesNotExist { contract_id };
744 }
745 GatewayPayStates::Failed{ error, error_message } => {
746 warn!("Yielding Fail state for {} due to {error:?} {error_message:?}", operation_id.fmt_short());
747 yield GatewayExtPayStates::Fail{ error, error_message };
748 },
749 GatewayPayStates::PayInvoice(_) => {
750 debug!("Got initial state PayInvoice while awaiting for output of {}", operation_id.fmt_short());
751 }
752 other => {
753 info!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
754 }
755 }
756 } _ => {
757 warn!("Got None while getting next ln pay state for {}", operation_id.fmt_short());
758 }}
759 }
760 }
761 }))
762 }
763}
764
765#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
766pub enum GatewayClientStateMachines {
767 Pay(GatewayPayStateMachine),
768 Receive(IncomingStateMachine),
769 Complete(GatewayCompleteStateMachine),
770}
771
772impl fmt::Display for GatewayClientStateMachines {
773 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774 match self {
775 GatewayClientStateMachines::Pay(pay) => {
776 write!(f, "{pay}")
777 }
778 GatewayClientStateMachines::Receive(receive) => {
779 write!(f, "{receive}")
780 }
781 GatewayClientStateMachines::Complete(complete) => {
782 write!(f, "{complete}")
783 }
784 }
785 }
786}
787
788impl IntoDynInstance for GatewayClientStateMachines {
789 type DynType = DynState;
790
791 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
792 DynState::from_typed(instance_id, self)
793 }
794}
795
796impl State for GatewayClientStateMachines {
797 type ModuleContext = GatewayClientContext;
798
799 fn transitions(
800 &self,
801 context: &Self::ModuleContext,
802 global_context: &DynGlobalClientContext,
803 ) -> Vec<StateTransition<Self>> {
804 match self {
805 GatewayClientStateMachines::Pay(pay_state) => {
806 sm_enum_variant_translation!(
807 pay_state.transitions(context, global_context),
808 GatewayClientStateMachines::Pay
809 )
810 }
811 GatewayClientStateMachines::Receive(receive_state) => {
812 sm_enum_variant_translation!(
813 receive_state.transitions(&context.into(), global_context),
814 GatewayClientStateMachines::Receive
815 )
816 }
817 GatewayClientStateMachines::Complete(complete_state) => {
818 sm_enum_variant_translation!(
819 complete_state.transitions(context, global_context),
820 GatewayClientStateMachines::Complete
821 )
822 }
823 }
824 }
825
826 fn operation_id(&self) -> fedimint_core::core::OperationId {
827 match self {
828 GatewayClientStateMachines::Pay(pay_state) => pay_state.operation_id(),
829 GatewayClientStateMachines::Receive(receive_state) => receive_state.operation_id(),
830 GatewayClientStateMachines::Complete(complete_state) => complete_state.operation_id(),
831 }
832 }
833}
834
835#[derive(Debug, Clone, Eq, PartialEq)]
836pub struct Htlc {
837 pub payment_hash: sha256::Hash,
839 pub incoming_amount_msat: Amount,
841 pub outgoing_amount_msat: Amount,
843 pub incoming_expiry: u32,
845 pub short_channel_id: Option<u64>,
847 pub incoming_chan_id: u64,
849 pub htlc_id: u64,
851}
852
853impl TryFrom<InterceptPaymentRequest> for Htlc {
854 type Error = anyhow::Error;
855
856 fn try_from(s: InterceptPaymentRequest) -> Result<Self, Self::Error> {
857 Ok(Self {
858 payment_hash: s.payment_hash,
859 incoming_amount_msat: Amount::from_msats(s.amount_msat),
860 outgoing_amount_msat: Amount::from_msats(s.amount_msat),
861 incoming_expiry: s.expiry,
862 short_channel_id: s.short_channel_id,
863 incoming_chan_id: s.incoming_chan_id,
864 htlc_id: s.htlc_id,
865 })
866 }
867}
868
869#[derive(Debug, Clone)]
870pub struct SwapParameters {
871 pub payment_hash: sha256::Hash,
872 pub amount_msat: Amount,
873}
874
875impl TryFrom<PaymentData> for SwapParameters {
876 type Error = anyhow::Error;
877
878 fn try_from(s: PaymentData) -> Result<Self, Self::Error> {
879 let payment_hash = s.payment_hash();
880 let amount_msat = s
881 .amount()
882 .ok_or_else(|| anyhow::anyhow!("Amountless invoice cannot be used in direct swap"))?;
883 Ok(Self {
884 payment_hash,
885 amount_msat,
886 })
887 }
888}
889
890#[async_trait]
896pub trait IGatewayClientV1: Debug + Send + Sync {
897 async fn verify_preimage_authentication(
903 &self,
904 payment_hash: sha256::Hash,
905 preimage_auth: sha256::Hash,
906 contract: OutgoingContractAccount,
907 ) -> Result<(), OutgoingPaymentError>;
908
909 async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()>;
912
913 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees>;
915
916 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>>;
919
920 async fn get_client_for_invoice(
928 &self,
929 payment_data: PaymentData,
930 ) -> Option<Spanned<ClientHandleArc>>;
931
932 async fn pay(
934 &self,
935 payment_data: PaymentData,
936 max_delay: u64,
937 max_fee: Amount,
938 ) -> Result<PayInvoiceResponse, LightningRpcError>;
939
940 async fn complete_htlc(
942 &self,
943 htlc_response: InterceptPaymentResponse,
944 ) -> Result<(), LightningRpcError>;
945
946 async fn is_lnv2_direct_swap(
949 &self,
950 payment_hash: sha256::Hash,
951 amount: Amount,
952 ) -> anyhow::Result<
953 Option<(
954 fedimint_lnv2_common::contracts::IncomingContract,
955 ClientHandleArc,
956 )>,
957 >;
958}