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