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