1mod api;
2mod complete_sm;
3pub mod events;
4mod receive_sm;
5mod send_sm;
6
7use std::collections::BTreeMap;
8use std::fmt;
9use std::fmt::Debug;
10use std::sync::Arc;
11
12use anyhow::{anyhow, ensure};
13use async_trait::async_trait;
14use bitcoin::hashes::sha256;
15use bitcoin::secp256k1::Message;
16use events::{IncomingPaymentStarted, OutgoingPaymentStarted};
17use fedimint_api_client::api::DynModuleApi;
18use fedimint_client::ClientHandleArc;
19use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
20use fedimint_client_module::module::recovery::NoModuleBackup;
21use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
22use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
23use fedimint_client_module::transaction::{
24 ClientOutput, ClientOutputBundle, ClientOutputSM, TransactionBuilder,
25};
26use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
27use fedimint_core::config::FederationId;
28use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
29use fedimint_core::db::DatabaseTransaction;
30use fedimint_core::encoding::{Decodable, Encodable};
31use fedimint_core::module::{
32 ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
33};
34use fedimint_core::secp256k1::Keypair;
35use fedimint_core::time::now;
36use fedimint_core::{Amount, PeerId, apply, async_trait_maybe_send, secp256k1};
37use fedimint_lightning::{InterceptPaymentResponse, LightningRpcError};
38use fedimint_lnv2_common::config::LightningClientConfig;
39use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage};
40use fedimint_lnv2_common::gateway_api::SendPaymentPayload;
41use fedimint_lnv2_common::{
42 LightningCommonInit, LightningInvoice, LightningModuleTypes, LightningOutput, LightningOutputV0,
43};
44use futures::StreamExt;
45use lightning_invoice::Bolt11Invoice;
46use receive_sm::{ReceiveSMState, ReceiveStateMachine};
47use secp256k1::schnorr::Signature;
48use send_sm::{SendSMState, SendStateMachine};
49use serde::{Deserialize, Serialize};
50use tpe::{AggregatePublicKey, PublicKeyShare};
51use tracing::{info, warn};
52
53use crate::api::GatewayFederationApi;
54use crate::complete_sm::{CompleteSMCommon, CompleteSMState, CompleteStateMachine};
55use crate::receive_sm::ReceiveSMCommon;
56use crate::send_sm::SendSMCommon;
57
58pub const EXPIRATION_DELTA_MINIMUM_V2: u64 = 144;
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct GatewayOperationMetaV2;
63
64#[derive(Debug, Clone)]
65pub struct GatewayClientInitV2 {
66 pub gateway: Arc<dyn IGatewayClientV2>,
67}
68
69impl ModuleInit for GatewayClientInitV2 {
70 type Common = LightningCommonInit;
71
72 async fn dump_database(
73 &self,
74 _dbtx: &mut DatabaseTransaction<'_>,
75 _prefix_names: Vec<String>,
76 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
77 Box::new(vec![].into_iter())
78 }
79}
80
81#[apply(async_trait_maybe_send!)]
82impl ClientModuleInit for GatewayClientInitV2 {
83 type Module = GatewayClientModuleV2;
84
85 fn supported_api_versions(&self) -> MultiApiVersion {
86 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
87 .expect("no version conflicts")
88 }
89
90 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
91 Ok(GatewayClientModuleV2 {
92 federation_id: *args.federation_id(),
93 cfg: args.cfg().clone(),
94 notifier: args.notifier().clone(),
95 client_ctx: args.context(),
96 module_api: args.module_api().clone(),
97 keypair: args
98 .module_root_secret()
99 .clone()
100 .to_secp_key(fedimint_core::secp256k1::SECP256K1),
101 gateway: self.gateway.clone(),
102 })
103 }
104}
105
106#[derive(Debug, Clone)]
107pub struct GatewayClientModuleV2 {
108 pub federation_id: FederationId,
109 pub cfg: LightningClientConfig,
110 pub notifier: ModuleNotifier<GatewayClientStateMachinesV2>,
111 pub client_ctx: ClientContext<Self>,
112 pub module_api: DynModuleApi,
113 pub keypair: Keypair,
114 pub gateway: Arc<dyn IGatewayClientV2>,
115}
116
117#[derive(Debug, Clone)]
118pub struct GatewayClientContextV2 {
119 pub module: GatewayClientModuleV2,
120 pub decoder: Decoder,
121 pub tpe_agg_pk: AggregatePublicKey,
122 pub tpe_pks: BTreeMap<PeerId, PublicKeyShare>,
123 pub gateway: Arc<dyn IGatewayClientV2>,
124}
125
126impl Context for GatewayClientContextV2 {
127 const KIND: Option<ModuleKind> = Some(fedimint_lnv2_common::KIND);
128}
129
130impl ClientModule for GatewayClientModuleV2 {
131 type Init = GatewayClientInitV2;
132 type Common = LightningModuleTypes;
133 type Backup = NoModuleBackup;
134 type ModuleStateMachineContext = GatewayClientContextV2;
135 type States = GatewayClientStateMachinesV2;
136
137 fn context(&self) -> Self::ModuleStateMachineContext {
138 GatewayClientContextV2 {
139 module: self.clone(),
140 decoder: self.decoder(),
141 tpe_agg_pk: self.cfg.tpe_agg_pk,
142 tpe_pks: self.cfg.tpe_pks.clone(),
143 gateway: self.gateway.clone(),
144 }
145 }
146 fn input_fee(
147 &self,
148 amount: Amount,
149 _input: &<Self::Common as ModuleCommon>::Input,
150 ) -> Option<Amount> {
151 Some(self.cfg.fee_consensus.fee(amount))
152 }
153
154 fn output_fee(
155 &self,
156 _amount: Amount,
157 output: &<Self::Common as ModuleCommon>::Output,
158 ) -> Option<Amount> {
159 let amount = match output.ensure_v0_ref().ok()? {
160 LightningOutputV0::Outgoing(contract) => contract.amount,
161 LightningOutputV0::Incoming(contract) => contract.commitment.amount,
162 };
163
164 Some(self.cfg.fee_consensus.fee(amount))
165 }
166}
167
168#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
169pub enum GatewayClientStateMachinesV2 {
170 Send(SendStateMachine),
171 Receive(ReceiveStateMachine),
172 Complete(CompleteStateMachine),
173}
174
175impl fmt::Display for GatewayClientStateMachinesV2 {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 match self {
178 GatewayClientStateMachinesV2::Send(send) => {
179 write!(f, "{send}")
180 }
181 GatewayClientStateMachinesV2::Receive(receive) => {
182 write!(f, "{receive}")
183 }
184 GatewayClientStateMachinesV2::Complete(complete) => {
185 write!(f, "{complete}")
186 }
187 }
188 }
189}
190
191impl IntoDynInstance for GatewayClientStateMachinesV2 {
192 type DynType = DynState;
193
194 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
195 DynState::from_typed(instance_id, self)
196 }
197}
198
199impl State for GatewayClientStateMachinesV2 {
200 type ModuleContext = GatewayClientContextV2;
201
202 fn transitions(
203 &self,
204 context: &Self::ModuleContext,
205 global_context: &DynGlobalClientContext,
206 ) -> Vec<StateTransition<Self>> {
207 match self {
208 GatewayClientStateMachinesV2::Send(state) => {
209 sm_enum_variant_translation!(
210 state.transitions(context, global_context),
211 GatewayClientStateMachinesV2::Send
212 )
213 }
214 GatewayClientStateMachinesV2::Receive(state) => {
215 sm_enum_variant_translation!(
216 state.transitions(context, global_context),
217 GatewayClientStateMachinesV2::Receive
218 )
219 }
220 GatewayClientStateMachinesV2::Complete(state) => {
221 sm_enum_variant_translation!(
222 state.transitions(context, global_context),
223 GatewayClientStateMachinesV2::Complete
224 )
225 }
226 }
227 }
228
229 fn operation_id(&self) -> OperationId {
230 match self {
231 GatewayClientStateMachinesV2::Send(state) => state.operation_id(),
232 GatewayClientStateMachinesV2::Receive(state) => state.operation_id(),
233 GatewayClientStateMachinesV2::Complete(state) => state.operation_id(),
234 }
235 }
236}
237
238#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)]
239pub enum FinalReceiveState {
240 Rejected,
241 Success([u8; 32]),
242 Refunded,
243 Failure,
244}
245
246impl GatewayClientModuleV2 {
247 pub async fn send_payment(
248 &self,
249 payload: SendPaymentPayload,
250 ) -> anyhow::Result<Result<[u8; 32], Signature>> {
251 let operation_start = now();
252
253 let operation_id = OperationId::from_encodable(&payload.contract.clone());
259
260 if self.client_ctx.operation_exists(operation_id).await {
261 return Ok(self.subscribe_send(operation_id).await);
262 }
263
264 ensure!(
268 payload.contract.claim_pk == self.keypair.public_key(),
269 "The outgoing contract is keyed to another gateway"
270 );
271
272 ensure!(
274 secp256k1::SECP256K1
275 .verify_schnorr(
276 &payload.auth,
277 &Message::from_digest(
278 *payload.invoice.consensus_hash::<sha256::Hash>().as_ref()
279 ),
280 &payload.contract.refund_pk.x_only_public_key().0,
281 )
282 .is_ok(),
283 "Invalid auth signature for the invoice data"
284 );
285
286 let (contract_id, expiration) = self
289 .module_api
290 .outgoing_contract_expiration(payload.outpoint)
291 .await
292 .map_err(|_| anyhow!("The gateway can not reach the federation"))?
293 .ok_or(anyhow!("The outgoing contract has not yet been confirmed"))?;
294
295 ensure!(
296 contract_id == payload.contract.contract_id(),
297 "Contract Id returned by the federation does not match contract in request"
298 );
299
300 let (payment_hash, amount) = match &payload.invoice {
301 LightningInvoice::Bolt11(invoice) => (
302 invoice.payment_hash(),
303 invoice
304 .amount_milli_satoshis()
305 .ok_or(anyhow!("Invoice is missing amount"))?,
306 ),
307 };
308
309 ensure!(
310 PaymentImage::Hash(*payment_hash) == payload.contract.payment_image,
311 "The invoices payment hash does not match the contracts payment hash"
312 );
313
314 let min_contract_amount = self
315 .gateway
316 .min_contract_amount(&payload.federation_id, amount)
317 .await?;
318
319 let send_sm = GatewayClientStateMachinesV2::Send(SendStateMachine {
320 common: SendSMCommon {
321 operation_id,
322 outpoint: payload.outpoint,
323 contract: payload.contract.clone(),
324 max_delay: expiration.saturating_sub(EXPIRATION_DELTA_MINIMUM_V2),
325 min_contract_amount,
326 invoice: payload.invoice,
327 claim_keypair: self.keypair,
328 },
329 state: SendSMState::Sending,
330 });
331
332 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
333 self.client_ctx
334 .manual_operation_start_dbtx(
335 &mut dbtx.to_ref_nc(),
336 operation_id,
337 LightningCommonInit::KIND.as_str(),
338 GatewayOperationMetaV2,
339 vec![self.client_ctx.make_dyn_state(send_sm)],
340 )
341 .await
342 .ok();
343
344 self.client_ctx
345 .log_event(
346 &mut dbtx,
347 OutgoingPaymentStarted {
348 operation_start,
349 outgoing_contract: payload.contract.clone(),
350 min_contract_amount,
351 invoice_amount: Amount::from_msats(amount),
352 max_delay: expiration.saturating_sub(EXPIRATION_DELTA_MINIMUM_V2),
353 },
354 )
355 .await;
356 dbtx.commit_tx().await;
357
358 Ok(self.subscribe_send(operation_id).await)
359 }
360
361 pub async fn subscribe_send(&self, operation_id: OperationId) -> Result<[u8; 32], Signature> {
362 let mut stream = self.notifier.subscribe(operation_id).await;
363
364 loop {
365 if let Some(GatewayClientStateMachinesV2::Send(state)) = stream.next().await {
366 match state.state {
367 SendSMState::Sending => {}
368 SendSMState::Claiming(claiming) => {
369 assert!(
373 self.client_ctx
374 .await_primary_module_outputs(operation_id, claiming.outpoints)
375 .await
376 .is_ok(),
377 "Gateway Module V2 failed to claim outgoing contract with preimage"
378 );
379
380 return Ok(claiming.preimage);
381 }
382 SendSMState::Cancelled(cancelled) => {
383 warn!("Outgoing lightning payment is cancelled {:?}", cancelled);
384
385 let signature = self
386 .keypair
387 .sign_schnorr(state.common.contract.forfeit_message());
388
389 assert!(state.common.contract.verify_forfeit_signature(&signature));
390
391 return Err(signature);
392 }
393 }
394 }
395 }
396 }
397
398 pub async fn relay_incoming_htlc(
399 &self,
400 payment_hash: sha256::Hash,
401 incoming_chan_id: u64,
402 htlc_id: u64,
403 contract: IncomingContract,
404 amount_msat: u64,
405 ) -> anyhow::Result<()> {
406 let operation_start = now();
407
408 let operation_id = OperationId::from_encodable(&contract);
409
410 if self.client_ctx.operation_exists(operation_id).await {
411 return Ok(());
412 }
413
414 let refund_keypair = self.keypair;
415
416 let client_output = ClientOutput::<LightningOutput> {
417 output: LightningOutput::V0(LightningOutputV0::Incoming(contract.clone())),
418 amount: contract.commitment.amount,
419 };
420 let commitment = contract.commitment.clone();
421 let client_output_sm = ClientOutputSM::<GatewayClientStateMachinesV2> {
422 state_machines: Arc::new(move |range: OutPointRange| {
423 assert_eq!(range.count(), 1);
424
425 vec![
426 GatewayClientStateMachinesV2::Receive(ReceiveStateMachine {
427 common: ReceiveSMCommon {
428 operation_id,
429 contract: contract.clone(),
430 outpoint: range.into_iter().next().unwrap(),
431 refund_keypair,
432 },
433 state: ReceiveSMState::Funding,
434 }),
435 GatewayClientStateMachinesV2::Complete(CompleteStateMachine {
436 common: CompleteSMCommon {
437 operation_id,
438 payment_hash,
439 incoming_chan_id,
440 htlc_id,
441 },
442 state: CompleteSMState::Pending,
443 }),
444 ]
445 }),
446 };
447
448 let client_output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
449 vec![client_output],
450 vec![client_output_sm],
451 ));
452 let transaction = TransactionBuilder::new().with_outputs(client_output);
453
454 self.client_ctx
455 .finalize_and_submit_transaction(
456 operation_id,
457 LightningCommonInit::KIND.as_str(),
458 |_| GatewayOperationMetaV2,
459 transaction,
460 )
461 .await?;
462
463 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
464 self.client_ctx
465 .log_event(
466 &mut dbtx,
467 IncomingPaymentStarted {
468 operation_start,
469 incoming_contract_commitment: commitment,
470 invoice_amount: Amount::from_msats(amount_msat),
471 },
472 )
473 .await;
474 dbtx.commit_tx().await;
475
476 Ok(())
477 }
478
479 pub async fn relay_direct_swap(
480 &self,
481 contract: IncomingContract,
482 amount_msat: u64,
483 ) -> anyhow::Result<FinalReceiveState> {
484 let operation_start = now();
485
486 let operation_id = OperationId::from_encodable(&contract);
487
488 if self.client_ctx.operation_exists(operation_id).await {
489 return Ok(self.await_receive(operation_id).await);
490 }
491
492 let refund_keypair = self.keypair;
493
494 let client_output = ClientOutput::<LightningOutput> {
495 output: LightningOutput::V0(LightningOutputV0::Incoming(contract.clone())),
496 amount: contract.commitment.amount,
497 };
498 let commitment = contract.commitment.clone();
499 let client_output_sm = ClientOutputSM::<GatewayClientStateMachinesV2> {
500 state_machines: Arc::new(move |range| {
501 assert_eq!(range.count(), 1);
502
503 vec![GatewayClientStateMachinesV2::Receive(ReceiveStateMachine {
504 common: ReceiveSMCommon {
505 operation_id,
506 contract: contract.clone(),
507 outpoint: range.into_iter().next().unwrap(),
508 refund_keypair,
509 },
510 state: ReceiveSMState::Funding,
511 })]
512 }),
513 };
514
515 let client_output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
516 vec![client_output],
517 vec![client_output_sm],
518 ));
519
520 let transaction = TransactionBuilder::new().with_outputs(client_output);
521
522 self.client_ctx
523 .finalize_and_submit_transaction(
524 operation_id,
525 LightningCommonInit::KIND.as_str(),
526 |_| GatewayOperationMetaV2,
527 transaction,
528 )
529 .await?;
530
531 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
532 self.client_ctx
533 .log_event(
534 &mut dbtx,
535 IncomingPaymentStarted {
536 operation_start,
537 incoming_contract_commitment: commitment,
538 invoice_amount: Amount::from_msats(amount_msat),
539 },
540 )
541 .await;
542 dbtx.commit_tx().await;
543
544 Ok(self.await_receive(operation_id).await)
545 }
546
547 async fn await_receive(&self, operation_id: OperationId) -> FinalReceiveState {
548 let mut stream = self.notifier.subscribe(operation_id).await;
549
550 loop {
551 if let Some(GatewayClientStateMachinesV2::Receive(state)) = stream.next().await {
552 match state.state {
553 ReceiveSMState::Funding => {}
554 ReceiveSMState::Rejected(..) => return FinalReceiveState::Rejected,
555 ReceiveSMState::Success(preimage) => {
556 return FinalReceiveState::Success(preimage);
557 }
558 ReceiveSMState::Refunding(out_points) => {
559 if self
560 .client_ctx
561 .await_primary_module_outputs(operation_id, out_points)
562 .await
563 .is_err()
564 {
565 return FinalReceiveState::Failure;
566 }
567
568 return FinalReceiveState::Refunded;
569 }
570 ReceiveSMState::Failure => return FinalReceiveState::Failure,
571 }
572 }
573 }
574 }
575
576 pub async fn await_completion(&self, operation_id: OperationId) {
579 let mut stream = self.notifier.subscribe(operation_id).await;
580
581 loop {
582 match stream.next().await {
583 Some(GatewayClientStateMachinesV2::Complete(state)) => {
584 if state.state == CompleteSMState::Completed {
585 info!(%state, "LNv2 completion state machine finished");
586 return;
587 }
588
589 info!(%state, "Waiting for LNv2 completion state machine");
590 }
591 Some(GatewayClientStateMachinesV2::Receive(state)) => {
592 info!(%state, "Waiting for LNv2 completion state machine");
593 continue;
594 }
595 Some(state) => {
596 warn!(%state, "Operation is not an LNv2 completion state machine");
597 return;
598 }
599 None => return,
600 }
601 }
602 }
603}
604
605#[async_trait]
611pub trait IGatewayClientV2: Debug + Send + Sync {
612 async fn complete_htlc(&self, htlc_response: InterceptPaymentResponse);
614
615 async fn is_direct_swap(
624 &self,
625 invoice: &Bolt11Invoice,
626 ) -> anyhow::Result<Option<(IncomingContract, ClientHandleArc)>>;
627
628 async fn pay(
630 &self,
631 invoice: Bolt11Invoice,
632 max_delay: u64,
633 max_fee: Amount,
634 ) -> Result<[u8; 32], LightningRpcError>;
635
636 async fn min_contract_amount(
644 &self,
645 federation_id: &FederationId,
646 amount: u64,
647 ) -> anyhow::Result<Amount>;
648}