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