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