1mod complete;
2pub mod pay;
3
4use std::collections::BTreeMap;
5use std::fmt;
6use std::sync::Arc;
7use std::time::Duration;
8
9use anyhow::ensure;
10use async_stream::stream;
11use bitcoin::hashes::{sha256, Hash};
12use bitcoin::key::Secp256k1;
13use bitcoin::secp256k1::All;
14use fedimint_api_client::api::DynModuleApi;
15use fedimint_client::derivable_secret::ChildId;
16use fedimint_client::module::init::{ClientModuleInit, ClientModuleInitArgs};
17use fedimint_client::module::recovery::NoModuleBackup;
18use fedimint_client::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
19use fedimint_client::oplog::UpdateStreamOrOutcome;
20use fedimint_client::sm::util::MapStateTransitions;
21use fedimint_client::sm::{Context, DynState, ModuleNotifier, State};
22use fedimint_client::transaction::{
23 ClientOutput, ClientOutputBundle, ClientOutputSM, TransactionBuilder,
24};
25use fedimint_client::{sm_enum_variant_translation, AddStateMachinesError, DynGlobalClientContext};
26use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
27use fedimint_core::db::{AutocommitError, DatabaseTransaction};
28use fedimint_core::encoding::{Decodable, Encodable};
29use fedimint_core::module::{ApiVersion, ModuleInit, MultiApiVersion};
30use fedimint_core::{apply, async_trait_maybe_send, secp256k1, Amount, OutPoint, TransactionId};
31use fedimint_ln_client::api::LnFederationApi;
32use fedimint_ln_client::incoming::{
33 FundingOfferState, IncomingSmCommon, IncomingSmError, IncomingSmStates, IncomingStateMachine,
34};
35use fedimint_ln_client::pay::{PayInvoicePayload, PaymentData};
36use fedimint_ln_client::{
37 create_incoming_contract_output, LightningClientContext, LightningClientInit,
38 RealGatewayConnection,
39};
40use fedimint_ln_common::config::LightningClientConfig;
41use fedimint_ln_common::contracts::{ContractId, Preimage};
42use fedimint_ln_common::route_hints::RouteHint;
43use fedimint_ln_common::{
44 create_gateway_remove_message, LightningCommonInit, LightningGateway,
45 LightningGatewayAnnouncement, LightningModuleTypes, LightningOutput, LightningOutputV0,
46 RemoveGatewayRequest, KIND,
47};
48use futures::StreamExt;
49use lightning_invoice::RoutingFees;
50use secp256k1::Keypair;
51use serde::{Deserialize, Serialize};
52use tracing::{debug, error, info, warn};
53
54use self::complete::GatewayCompleteStateMachine;
55use self::pay::{
56 GatewayPayCommon, GatewayPayInvoice, GatewayPayStateMachine, GatewayPayStates,
57 OutgoingPaymentError,
58};
59use crate::lightning::{InterceptPaymentRequest, LightningContext};
60use crate::state_machine::complete::{
61 GatewayCompleteCommon, GatewayCompleteStates, WaitForPreimageState,
62};
63use crate::Gateway;
64
65#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
68pub enum GatewayExtPayStates {
69 Created,
70 Preimage {
71 preimage: Preimage,
72 },
73 Success {
74 preimage: Preimage,
75 out_points: Vec<OutPoint>,
76 },
77 Canceled {
78 error: OutgoingPaymentError,
79 },
80 Fail {
81 error: OutgoingPaymentError,
82 error_message: String,
83 },
84 OfferDoesNotExist {
85 contract_id: ContractId,
86 },
87}
88
89#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
92pub enum GatewayExtReceiveStates {
93 Funding,
94 Preimage(Preimage),
95 RefundSuccess {
96 out_points: Vec<OutPoint>,
97 error: IncomingSmError,
98 },
99 RefundError {
100 error_message: String,
101 error: IncomingSmError,
102 },
103 FundingFailed {
104 error: IncomingSmError,
105 },
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub enum GatewayMeta {
110 Pay,
111 Receive,
112}
113
114#[derive(Debug, Clone)]
115pub struct GatewayClientInit {
116 pub timelock_delta: u64,
117 pub federation_index: u64,
118 pub gateway: Arc<Gateway>,
119}
120
121impl ModuleInit for GatewayClientInit {
122 type Common = LightningCommonInit;
123
124 async fn dump_database(
125 &self,
126 _dbtx: &mut DatabaseTransaction<'_>,
127 _prefix_names: Vec<String>,
128 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
129 Box::new(vec![].into_iter())
130 }
131}
132
133#[apply(async_trait_maybe_send!)]
134impl ClientModuleInit for GatewayClientInit {
135 type Module = GatewayClientModule;
136
137 fn supported_api_versions(&self) -> MultiApiVersion {
138 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
139 .expect("no version conflicts")
140 }
141
142 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
143 Ok(GatewayClientModule {
144 cfg: args.cfg().clone(),
145 notifier: args.notifier().clone(),
146 redeem_key: args
147 .module_root_secret()
148 .child_key(ChildId(0))
149 .to_secp_key(&fedimint_core::secp256k1::Secp256k1::new()),
150 module_api: args.module_api().clone(),
151 timelock_delta: self.timelock_delta,
152 federation_index: self.federation_index,
153 client_ctx: args.context(),
154 gateway: self.gateway.clone(),
155 })
156 }
157}
158
159#[derive(Debug, Clone)]
160pub struct GatewayClientContext {
161 redeem_key: Keypair,
162 timelock_delta: u64,
163 secp: Secp256k1<All>,
164 pub ln_decoder: Decoder,
165 notifier: ModuleNotifier<GatewayClientStateMachines>,
166 gateway: Arc<Gateway>,
167}
168
169impl Context for GatewayClientContext {
170 const KIND: Option<ModuleKind> = Some(fedimint_ln_common::KIND);
171}
172
173impl From<&GatewayClientContext> for LightningClientContext {
174 fn from(ctx: &GatewayClientContext) -> Self {
175 LightningClientContext {
176 ln_decoder: ctx.ln_decoder.clone(),
177 redeem_key: ctx.redeem_key,
178 gateway_conn: Arc::new(RealGatewayConnection::default()),
179 }
180 }
181}
182
183#[derive(Debug)]
188pub struct GatewayClientModule {
189 cfg: LightningClientConfig,
190 pub notifier: ModuleNotifier<GatewayClientStateMachines>,
191 pub redeem_key: Keypair,
192 timelock_delta: u64,
193 federation_index: u64,
194 module_api: DynModuleApi,
195 client_ctx: ClientContext<Self>,
196 gateway: Arc<Gateway>,
197}
198
199impl ClientModule for GatewayClientModule {
200 type Init = LightningClientInit;
201 type Common = LightningModuleTypes;
202 type Backup = NoModuleBackup;
203 type ModuleStateMachineContext = GatewayClientContext;
204 type States = GatewayClientStateMachines;
205
206 fn context(&self) -> Self::ModuleStateMachineContext {
207 Self::ModuleStateMachineContext {
208 redeem_key: self.redeem_key,
209 timelock_delta: self.timelock_delta,
210 secp: Secp256k1::new(),
211 ln_decoder: self.decoder(),
212 notifier: self.notifier.clone(),
213 gateway: self.gateway.clone(),
214 }
215 }
216
217 fn input_fee(
218 &self,
219 _amount: Amount,
220 _input: &<Self::Common as fedimint_core::module::ModuleCommon>::Input,
221 ) -> Option<Amount> {
222 Some(self.cfg.fee_consensus.contract_input)
223 }
224
225 fn output_fee(
226 &self,
227 _amount: Amount,
228 output: &<Self::Common as fedimint_core::module::ModuleCommon>::Output,
229 ) -> Option<Amount> {
230 match output.maybe_v0_ref()? {
231 LightningOutputV0::Contract(_) => Some(self.cfg.fee_consensus.contract_output),
232 LightningOutputV0::Offer(_) | LightningOutputV0::CancelOutgoing { .. } => {
233 Some(Amount::ZERO)
234 }
235 }
236 }
237}
238
239impl GatewayClientModule {
240 fn to_gateway_registration_info(
241 &self,
242 route_hints: Vec<RouteHint>,
243 ttl: Duration,
244 fees: RoutingFees,
245 lightning_context: LightningContext,
246 ) -> LightningGatewayAnnouncement {
247 LightningGatewayAnnouncement {
248 info: LightningGateway {
249 federation_index: self.federation_index,
250 gateway_redeem_key: self.redeem_key.public_key(),
251 node_pub_key: lightning_context.lightning_public_key,
252 lightning_alias: lightning_context.lightning_alias,
253 api: self.gateway.versioned_api.clone(),
254 route_hints,
255 fees,
256 gateway_id: self.gateway.gateway_id,
257 supports_private_payments: lightning_context.lnrpc.supports_private_payments(),
258 },
259 ttl,
260 vetted: false,
261 }
262 }
263
264 async fn create_funding_incoming_contract_output_from_htlc(
265 &self,
266 htlc: Htlc,
267 ) -> Result<
268 (
269 OperationId,
270 Amount,
271 ClientOutput<LightningOutputV0>,
272 ClientOutputSM<GatewayClientStateMachines>,
273 ),
274 IncomingSmError,
275 > {
276 let operation_id = OperationId(htlc.payment_hash.to_byte_array());
277 let (incoming_output, amount, contract_id) = create_incoming_contract_output(
278 &self.module_api,
279 htlc.payment_hash,
280 htlc.outgoing_amount_msat,
281 &self.redeem_key,
282 )
283 .await?;
284
285 let client_output = ClientOutput::<LightningOutputV0> {
286 output: incoming_output,
287 amount,
288 };
289 let client_output_sm = ClientOutputSM::<GatewayClientStateMachines> {
290 state_machines: Arc::new(move |out_point_range: OutPointRange| {
291 assert_eq!(out_point_range.count(), 1);
292 vec![
293 GatewayClientStateMachines::Receive(IncomingStateMachine {
294 common: IncomingSmCommon {
295 operation_id,
296 contract_id,
297 payment_hash: htlc.payment_hash,
298 },
299 state: IncomingSmStates::FundingOffer(FundingOfferState {
300 txid: out_point_range.txid(),
301 }),
302 }),
303 GatewayClientStateMachines::Complete(GatewayCompleteStateMachine {
304 common: GatewayCompleteCommon {
305 operation_id,
306 payment_hash: htlc.payment_hash,
307 incoming_chan_id: htlc.incoming_chan_id,
308 htlc_id: htlc.htlc_id,
309 },
310 state: GatewayCompleteStates::WaitForPreimage(WaitForPreimageState),
311 }),
312 ]
313 }),
314 };
315 Ok((operation_id, amount, client_output, client_output_sm))
316 }
317
318 async fn create_funding_incoming_contract_output_from_swap(
319 &self,
320 swap: SwapParameters,
321 ) -> Result<
322 (
323 OperationId,
324 ClientOutput<LightningOutputV0>,
325 ClientOutputSM<GatewayClientStateMachines>,
326 ),
327 IncomingSmError,
328 > {
329 let payment_hash = swap.payment_hash;
330 let operation_id = OperationId(payment_hash.to_byte_array());
331 let (incoming_output, amount, contract_id) = create_incoming_contract_output(
332 &self.module_api,
333 payment_hash,
334 swap.amount_msat,
335 &self.redeem_key,
336 )
337 .await?;
338
339 let client_output = ClientOutput::<LightningOutputV0> {
340 output: incoming_output,
341 amount,
342 };
343 let client_output_sm = ClientOutputSM::<GatewayClientStateMachines> {
344 state_machines: Arc::new(move |out_point_range| {
345 assert_eq!(out_point_range.count(), 1);
346 vec![GatewayClientStateMachines::Receive(IncomingStateMachine {
347 common: IncomingSmCommon {
348 operation_id,
349 contract_id,
350 payment_hash,
351 },
352 state: IncomingSmStates::FundingOffer(FundingOfferState {
353 txid: out_point_range.txid(),
354 }),
355 })]
356 }),
357 };
358 Ok((operation_id, client_output, client_output_sm))
359 }
360
361 pub async fn try_register_with_federation(
363 &self,
364 route_hints: Vec<RouteHint>,
365 time_to_live: Duration,
366 fees: RoutingFees,
367 lightning_context: LightningContext,
368 ) {
369 let registration_info =
370 self.to_gateway_registration_info(route_hints, time_to_live, fees, lightning_context);
371 let gateway_id = registration_info.info.gateway_id;
372
373 let federation_id = self
374 .client_ctx
375 .get_config()
376 .await
377 .global
378 .calculate_federation_id();
379 if let Err(e) = self.module_api.register_gateway(®istration_info).await {
380 warn!(
381 ?e,
382 "Failed to register gateway {gateway_id} with federation {federation_id}"
383 );
384 } else {
385 info!("Successfully registered gateway {gateway_id} with federation {federation_id}");
386 }
387 }
388
389 pub async fn remove_from_federation(&self, gateway_keypair: Keypair) {
394 if let Err(e) = self.remove_from_federation_inner(gateway_keypair).await {
397 let gateway_id = gateway_keypair.public_key();
398 let federation_id = self
399 .client_ctx
400 .get_config()
401 .await
402 .global
403 .calculate_federation_id();
404 warn!("Failed to remove gateway {gateway_id} from federation {federation_id}: {e:?}");
405 }
406 }
407
408 async fn remove_from_federation_inner(&self, gateway_keypair: Keypair) -> anyhow::Result<()> {
413 let gateway_id = gateway_keypair.public_key();
414 let challenges = self
415 .module_api
416 .get_remove_gateway_challenge(gateway_id)
417 .await;
418
419 let fed_public_key = self.cfg.threshold_pub_key;
420 let signatures = challenges
421 .into_iter()
422 .filter_map(|(peer_id, challenge)| {
423 let msg = create_gateway_remove_message(fed_public_key, peer_id, challenge?);
424 let signature = gateway_keypair.sign_schnorr(msg);
425 Some((peer_id, signature))
426 })
427 .collect::<BTreeMap<_, _>>();
428
429 let remove_gateway_request = RemoveGatewayRequest {
430 gateway_id,
431 signatures,
432 };
433
434 self.module_api.remove_gateway(remove_gateway_request).await;
435
436 Ok(())
437 }
438
439 pub async fn gateway_handle_intercepted_htlc(&self, htlc: Htlc) -> anyhow::Result<OperationId> {
441 debug!("Handling intercepted HTLC {htlc:?}");
442 let (operation_id, amount, client_output, client_output_sm) = self
443 .create_funding_incoming_contract_output_from_htlc(htlc.clone())
444 .await?;
445
446 let output = ClientOutput {
447 output: LightningOutput::V0(client_output.output),
448 amount,
449 };
450
451 let tx = TransactionBuilder::new().with_outputs(self.client_ctx.make_client_outputs(
452 ClientOutputBundle::new(vec![output], vec![client_output_sm]),
453 ));
454 let operation_meta_gen = |_: TransactionId, _: Vec<OutPoint>| GatewayMeta::Receive;
455 self.client_ctx
456 .finalize_and_submit_transaction(operation_id, KIND.as_str(), operation_meta_gen, tx)
457 .await?;
458 debug!(?operation_id, "Submitted transaction for HTLC {htlc:?}");
459 Ok(operation_id)
460 }
461
462 async fn gateway_handle_direct_swap(
466 &self,
467 swap_params: SwapParameters,
468 ) -> anyhow::Result<OperationId> {
469 debug!("Handling direct swap {swap_params:?}");
470 let (operation_id, client_output, client_output_sm) = self
471 .create_funding_incoming_contract_output_from_swap(swap_params.clone())
472 .await?;
473
474 let output = ClientOutput {
475 output: LightningOutput::V0(client_output.output),
476 amount: client_output.amount,
477 };
478 let tx = TransactionBuilder::new().with_outputs(self.client_ctx.make_client_outputs(
479 ClientOutputBundle::new(vec![output], vec![client_output_sm]),
480 ));
481 let operation_meta_gen = |_: TransactionId, _: Vec<OutPoint>| GatewayMeta::Receive;
482 self.client_ctx
483 .finalize_and_submit_transaction(operation_id, KIND.as_str(), operation_meta_gen, tx)
484 .await?;
485 debug!(
486 ?operation_id,
487 "Submitted transaction for direct swap {swap_params:?}"
488 );
489 Ok(operation_id)
490 }
491
492 pub async fn gateway_subscribe_ln_receive(
495 &self,
496 operation_id: OperationId,
497 ) -> anyhow::Result<UpdateStreamOrOutcome<GatewayExtReceiveStates>> {
498 let operation = self.client_ctx.get_operation(operation_id).await?;
499 let mut stream = self.notifier.subscribe(operation_id).await;
500 let client_ctx = self.client_ctx.clone();
501
502 Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, || {
503 stream! {
504
505 yield GatewayExtReceiveStates::Funding;
506
507 let state = loop {
508 debug!("Getting next ln receive state for {}", operation_id.fmt_short());
509 if let Some(GatewayClientStateMachines::Receive(state)) = stream.next().await {
510 match state.state {
511 IncomingSmStates::Preimage(preimage) =>{
512 debug!(?operation_id, "Received preimage");
513 break GatewayExtReceiveStates::Preimage(preimage)
514 },
515 IncomingSmStates::RefundSubmitted { out_points, error } => {
516 debug!(?operation_id, "Refund submitted for {out_points:?} {error}");
517 match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
518 Ok(()) => {
519 debug!(?operation_id, "Refund success");
520 break GatewayExtReceiveStates::RefundSuccess { out_points, error }
521 },
522 Err(e) => {
523 warn!(?operation_id, "Got failure {e:?} while awaiting for refund outputs {out_points:?}");
524 break GatewayExtReceiveStates::RefundError{ error_message: e.to_string(), error }
525 },
526 }
527 },
528 IncomingSmStates::FundingFailed { error } => {
529 warn!(?operation_id, "Funding failed: {error:?}");
530 break GatewayExtReceiveStates::FundingFailed{ error }
531 },
532 other => {
533 debug!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
534 }
535 }
536 }
537 };
538 yield state;
539 }
540 }))
541 }
542
543 pub async fn await_completion(&self, operation_id: OperationId) {
546 let mut stream = self.notifier.subscribe(operation_id).await;
547 loop {
548 match stream.next().await {
549 Some(GatewayClientStateMachines::Complete(state)) => match state.state {
550 GatewayCompleteStates::HtlcFinished => {
551 info!(%state, "LNv1 completion state machine finished");
552 return;
553 }
554 GatewayCompleteStates::Failure => {
555 error!(%state, "LNv1 completion state machine failed");
556 return;
557 }
558 _ => {
559 info!(%state, "Waiting for LNv1 completion state machine");
560 continue;
561 }
562 },
563 Some(GatewayClientStateMachines::Receive(state)) => {
564 info!(%state, "Waiting for LNv1 completion state machine");
565 continue;
566 }
567 Some(state) => {
568 warn!(%state, "Operation is not an LNv1 completion state machine");
569 return;
570 }
571 None => return,
572 }
573 }
574 }
575
576 pub async fn gateway_pay_bolt11_invoice(
578 &self,
579 pay_invoice_payload: PayInvoicePayload,
580 ) -> anyhow::Result<OperationId> {
581 let payload = pay_invoice_payload.clone();
582 let lightning_context = self.gateway.get_lightning_context().await?;
583
584 if matches!(
585 pay_invoice_payload.payment_data,
586 PaymentData::PrunedInvoice { .. }
587 ) {
588 ensure!(
589 lightning_context.lnrpc.supports_private_payments(),
590 "Private payments are not supported by the lightning node"
591 );
592 }
593
594 self.client_ctx.module_db()
595 .autocommit(
596 |dbtx, _| {
597 Box::pin(async {
598 let operation_id = OperationId(payload.contract_id.to_byte_array());
599
600 let state_machines =
601 vec![GatewayClientStateMachines::Pay(GatewayPayStateMachine {
602 common: GatewayPayCommon { operation_id },
603 state: GatewayPayStates::PayInvoice(GatewayPayInvoice {
604 pay_invoice_payload: payload.clone(),
605 }),
606 })];
607
608 let dyn_states = state_machines
609 .into_iter()
610 .map(|s| self.client_ctx.make_dyn(s))
611 .collect();
612
613 match self.client_ctx.add_state_machines_dbtx(dbtx, dyn_states).await {
614 Ok(()) => {
615 self.client_ctx
616 .add_operation_log_entry_dbtx(
617 dbtx,
618 operation_id,
619 KIND.as_str(),
620 GatewayMeta::Pay,
621 )
622 .await;
623 }
624 Err(AddStateMachinesError::StateAlreadyExists) => {
625 info!("State machine for operation {} already exists, will not add a new one", operation_id.fmt_short());
626 }
627 Err(other) => {
628 anyhow::bail!("Failed to add state machines: {other:?}")
629 }
630 }
631 Ok(operation_id)
632 })
633 },
634 Some(100),
635 )
636 .await
637 .map_err(|e| match e {
638 AutocommitError::ClosureError { error, .. } => error,
639 AutocommitError::CommitFailed { last_error, .. } => {
640 anyhow::anyhow!("Commit to DB failed: {last_error}")
641 }
642 })
643 }
644
645 pub async fn gateway_subscribe_ln_pay(
646 &self,
647 operation_id: OperationId,
648 ) -> anyhow::Result<UpdateStreamOrOutcome<GatewayExtPayStates>> {
649 let mut stream = self.notifier.subscribe(operation_id).await;
650 let operation = self.client_ctx.get_operation(operation_id).await?;
651 let client_ctx = self.client_ctx.clone();
652
653 Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, || {
654 stream! {
655 yield GatewayExtPayStates::Created;
656
657 loop {
658 debug!("Getting next ln pay state for {}", operation_id.fmt_short());
659 if let Some(GatewayClientStateMachines::Pay(state)) = stream.next().await {
660 match state.state {
661 GatewayPayStates::Preimage(out_points, preimage) => {
662 yield GatewayExtPayStates::Preimage{ preimage: preimage.clone() };
663
664 match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
665 Ok(()) => {
666 debug!(?operation_id, "Success");
667 yield GatewayExtPayStates::Success{ preimage: preimage.clone(), out_points };
668 return;
669
670 }
671 Err(e) => {
672 warn!(?operation_id, "Got failure {e:?} while awaiting for outputs {out_points:?}");
673 }
675 }
676 }
677 GatewayPayStates::Canceled { txid, contract_id, error } => {
678 debug!(?operation_id, "Trying to cancel contract {contract_id:?} due to {error:?}");
679 match client_ctx.transaction_updates(operation_id).await.await_tx_accepted(txid).await {
680 Ok(()) => {
681 debug!(?operation_id, "Canceled contract {contract_id:?} due to {error:?}");
682 yield GatewayExtPayStates::Canceled{ error };
683 return;
684 }
685 Err(e) => {
686 warn!(?operation_id, "Got failure {e:?} while awaiting for transaction {txid} to be accepted for");
687 yield GatewayExtPayStates::Fail { error, error_message: format!("Refund transaction {txid} was not accepted by the federation. OperationId: {} Error: {e:?}", operation_id.fmt_short()) };
688 }
689 }
690 }
691 GatewayPayStates::OfferDoesNotExist(contract_id) => {
692 warn!("Yielding OfferDoesNotExist state for {} and contract {contract_id}", operation_id.fmt_short());
693 yield GatewayExtPayStates::OfferDoesNotExist { contract_id };
694 }
695 GatewayPayStates::Failed{ error, error_message } => {
696 warn!("Yielding Fail state for {} due to {error:?} {error_message:?}", operation_id.fmt_short());
697 yield GatewayExtPayStates::Fail{ error, error_message };
698 },
699 GatewayPayStates::PayInvoice(_) => {
700 debug!("Got initial state PayInvoice while awaiting for output of {}", operation_id.fmt_short());
701 }
702 other => {
703 info!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
704 }
705 }
706 } else {
707 warn!("Got None while getting next ln pay state for {}", operation_id.fmt_short());
708 }
709 }
710 }
711 }))
712 }
713}
714
715#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
716pub enum GatewayClientStateMachines {
717 Pay(GatewayPayStateMachine),
718 Receive(IncomingStateMachine),
719 Complete(GatewayCompleteStateMachine),
720}
721
722impl fmt::Display for GatewayClientStateMachines {
723 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
724 match self {
725 GatewayClientStateMachines::Pay(pay) => {
726 write!(f, "{pay}")
727 }
728 GatewayClientStateMachines::Receive(receive) => {
729 write!(f, "{receive}")
730 }
731 GatewayClientStateMachines::Complete(complete) => {
732 write!(f, "{complete}")
733 }
734 }
735 }
736}
737
738impl IntoDynInstance for GatewayClientStateMachines {
739 type DynType = DynState;
740
741 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
742 DynState::from_typed(instance_id, self)
743 }
744}
745
746impl State for GatewayClientStateMachines {
747 type ModuleContext = GatewayClientContext;
748
749 fn transitions(
750 &self,
751 context: &Self::ModuleContext,
752 global_context: &DynGlobalClientContext,
753 ) -> Vec<fedimint_client::sm::StateTransition<Self>> {
754 match self {
755 GatewayClientStateMachines::Pay(pay_state) => {
756 sm_enum_variant_translation!(
757 pay_state.transitions(context, global_context),
758 GatewayClientStateMachines::Pay
759 )
760 }
761 GatewayClientStateMachines::Receive(receive_state) => {
762 sm_enum_variant_translation!(
763 receive_state.transitions(&context.into(), global_context),
764 GatewayClientStateMachines::Receive
765 )
766 }
767 GatewayClientStateMachines::Complete(complete_state) => {
768 sm_enum_variant_translation!(
769 complete_state.transitions(context, global_context),
770 GatewayClientStateMachines::Complete
771 )
772 }
773 }
774 }
775
776 fn operation_id(&self) -> fedimint_core::core::OperationId {
777 match self {
778 GatewayClientStateMachines::Pay(pay_state) => pay_state.operation_id(),
779 GatewayClientStateMachines::Receive(receive_state) => receive_state.operation_id(),
780 GatewayClientStateMachines::Complete(complete_state) => complete_state.operation_id(),
781 }
782 }
783}
784
785#[derive(Debug, Clone, Eq, PartialEq)]
786pub struct Htlc {
787 pub payment_hash: sha256::Hash,
789 pub incoming_amount_msat: Amount,
791 pub outgoing_amount_msat: Amount,
793 pub incoming_expiry: u32,
795 pub short_channel_id: Option<u64>,
797 pub incoming_chan_id: u64,
799 pub htlc_id: u64,
801}
802
803impl TryFrom<InterceptPaymentRequest> for Htlc {
804 type Error = anyhow::Error;
805
806 fn try_from(s: InterceptPaymentRequest) -> Result<Self, Self::Error> {
807 Ok(Self {
808 payment_hash: s.payment_hash,
809 incoming_amount_msat: Amount::from_msats(s.amount_msat),
810 outgoing_amount_msat: Amount::from_msats(s.amount_msat),
811 incoming_expiry: s.expiry,
812 short_channel_id: s.short_channel_id,
813 incoming_chan_id: s.incoming_chan_id,
814 htlc_id: s.htlc_id,
815 })
816 }
817}
818
819#[derive(Debug, Clone)]
820struct SwapParameters {
821 payment_hash: sha256::Hash,
822 amount_msat: Amount,
823}
824
825impl TryFrom<PaymentData> for SwapParameters {
826 type Error = anyhow::Error;
827
828 fn try_from(s: PaymentData) -> Result<Self, Self::Error> {
829 let payment_hash = s.payment_hash();
830 let amount_msat = s
831 .amount()
832 .ok_or_else(|| anyhow::anyhow!("Amountless invoice cannot be used in direct swap"))?;
833 Ok(Self {
834 payment_hash,
835 amount_msat,
836 })
837 }
838}