1use std::fmt;
2
3use fedimint_client_module::DynGlobalClientContext;
4use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
5use fedimint_client_module::transaction::{ClientInput, ClientInputBundle};
6use fedimint_core::config::FederationId;
7use fedimint_core::core::OperationId;
8use fedimint_core::encoding::{Decodable, Encodable};
9use fedimint_core::module::Amounts;
10use fedimint_core::secp256k1::Keypair;
11use fedimint_core::util::FmtCompact as _;
12use fedimint_core::{Amount, OutPoint};
13use fedimint_lnv2_common::contracts::OutgoingContract;
14use fedimint_lnv2_common::{LightningInput, LightningInputV0, LightningInvoice, OutgoingWitness};
15use serde::{Deserialize, Serialize};
16
17use super::FinalReceiveState;
18use super::events::{OutgoingPaymentFailed, OutgoingPaymentSucceeded};
19use crate::{GatewayClientContextV2, GatewayClientModuleV2};
20
21#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
22pub struct SendStateMachine {
23 pub common: SendSMCommon,
24 pub state: SendSMState,
25}
26
27impl SendStateMachine {
28 pub fn update(&self, state: SendSMState) -> Self {
29 Self {
30 common: self.common.clone(),
31 state,
32 }
33 }
34}
35
36impl fmt::Display for SendStateMachine {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 write!(
39 f,
40 "Send State Machine Operation ID: {:?} State: {}",
41 self.common.operation_id, self.state
42 )
43 }
44}
45
46#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
47pub struct SendSMCommon {
48 pub operation_id: OperationId,
49 pub outpoint: OutPoint,
50 pub contract: OutgoingContract,
51 pub max_delay: u64,
52 pub min_contract_amount: Amount,
53 pub invoice: LightningInvoice,
54 pub claim_keypair: Keypair,
55}
56
57#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
58pub enum SendSMState {
59 Sending,
60 Claiming(Claiming),
61 Cancelled(Cancelled),
62}
63
64#[derive(Debug, Serialize, Deserialize)]
65pub struct PaymentResponse {
66 preimage: [u8; 32],
67 target_federation: Option<FederationId>,
68}
69
70impl fmt::Display for SendSMState {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 match self {
73 SendSMState::Sending => write!(f, "Sending"),
74 SendSMState::Claiming(_) => write!(f, "Claiming"),
75 SendSMState::Cancelled(_) => write!(f, "Cancelled"),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
81pub struct Claiming {
82 pub preimage: [u8; 32],
83 pub outpoints: Vec<OutPoint>,
84}
85
86#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
87pub enum Cancelled {
88 InvoiceExpired,
89 TimeoutTooClose,
90 Underfunded,
91 RegistrationError(String),
92 FinalizationError(String),
93 Rejected,
94 Refunded,
95 Failure,
96 LightningRpcError(String),
97 DuplicatePayment,
98}
99
100impl State for SendStateMachine {
110 type ModuleContext = GatewayClientContextV2;
111
112 fn transitions(
113 &self,
114 context: &Self::ModuleContext,
115 global_context: &DynGlobalClientContext,
116 ) -> Vec<StateTransition<Self>> {
117 let gc = global_context.clone();
118 let gateway_context = context.clone();
119
120 match &self.state {
121 SendSMState::Sending => {
122 vec![StateTransition::new(
123 Self::send_payment(context.clone(), self.common.clone()),
124 move |dbtx, result, old_state| {
125 Box::pin(Self::transition_send_payment(
126 dbtx,
127 old_state,
128 gc.clone(),
129 result,
130 gateway_context.clone(),
131 ))
132 },
133 )]
134 }
135 SendSMState::Claiming(..) | SendSMState::Cancelled(..) => {
136 vec![]
137 }
138 }
139 }
140
141 fn operation_id(&self) -> OperationId {
142 self.common.operation_id
143 }
144}
145
146impl SendStateMachine {
147 async fn send_payment(
148 context: GatewayClientContextV2,
149 common: SendSMCommon,
150 ) -> Result<PaymentResponse, Cancelled> {
151 let SendSMCommon {
152 operation_id: _,
153 outpoint,
154 contract,
155 max_delay,
156 min_contract_amount,
157 invoice,
158 claim_keypair: _,
159 } = common;
160 let LightningInvoice::Bolt11(invoice) = invoice;
161
162 if max_delay == 0 {
168 return Err(Cancelled::TimeoutTooClose);
169 }
170
171 let Some(max_fee) = contract.amount.checked_sub(min_contract_amount) else {
172 return Err(Cancelled::Underfunded);
173 };
174
175 context.gateway.await_lightning_connected().await;
185 let fresh_max_delay = context
186 .module
187 .await_outgoing_contract_max_delay(outpoint)
188 .await;
189
190 let fresh_dispatch_refusal = fresh_dispatch_refusal(invoice.is_expired(), fresh_max_delay);
199
200 if let Some(client) = context.gateway.is_lnv1_invoice(&invoice).await {
205 let final_state = context
206 .gateway
207 .relay_lnv1_swap(client.value(), &invoice, fresh_dispatch_refusal.is_none())
208 .await;
209 return match final_state {
210 Ok(Some(final_receive_state)) => match final_receive_state {
211 FinalReceiveState::Rejected => Err(Cancelled::Rejected),
212 FinalReceiveState::Success(preimage) => Ok(PaymentResponse {
213 preimage,
214 target_federation: Some(client.value().federation_id()),
215 }),
216 FinalReceiveState::Refunded => Err(Cancelled::Refunded),
217 FinalReceiveState::Failure => Err(Cancelled::Failure),
218 },
219 Ok(None) => Err(fresh_dispatch_refusal
220 .expect("the relay only refuses a fresh dispatch when one was denied")),
221 Err(e) => Err(Cancelled::FinalizationError(e.fmt_compact().to_string())),
222 };
223 }
224
225 match context
226 .gateway
227 .is_direct_swap(&invoice)
228 .await
229 .map_err(|e| Cancelled::RegistrationError(e.fmt_compact().to_string()))?
230 {
231 Some((contract, client)) => {
232 match client
233 .get_first_module::<GatewayClientModuleV2>()
234 .expect("Must have client module")
235 .relay_direct_swap(
236 contract,
237 invoice
238 .amount_milli_satoshis()
239 .expect("amountless invoices are not supported"),
240 fresh_dispatch_refusal.is_none(),
241 )
242 .await
243 {
244 Ok(Some(final_receive_state)) => match final_receive_state {
245 FinalReceiveState::Rejected => Err(Cancelled::Rejected),
246 FinalReceiveState::Success(preimage) => Ok(PaymentResponse {
247 preimage,
248 target_federation: Some(client.federation_id()),
249 }),
250 FinalReceiveState::Refunded => Err(Cancelled::Refunded),
251 FinalReceiveState::Failure => Err(Cancelled::Failure),
252 },
253 Ok(None) => Err(fresh_dispatch_refusal
254 .expect("the relay only refuses a fresh dispatch when one was denied")),
255 Err(e) => Err(Cancelled::FinalizationError(e.fmt_compact().to_string())),
256 }
257 }
258 None => {
259 if let Some(refusal) = fresh_dispatch_refusal
263 && !context
264 .gateway
265 .outbound_payment_exists(*invoice.payment_hash())
266 .await
267 {
268 return Err(refusal);
269 }
270
271 let preimage = context
272 .gateway
273 .pay(invoice, fresh_max_delay, max_fee)
274 .await
275 .map_err(|e| Cancelled::LightningRpcError(e.to_string()))?;
276 Ok(PaymentResponse {
277 preimage,
278 target_federation: None,
279 })
280 }
281 }
282 }
283
284 async fn transition_send_payment(
285 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
286 old_state: SendStateMachine,
287 global_context: DynGlobalClientContext,
288 result: Result<PaymentResponse, Cancelled>,
289 client_ctx: GatewayClientContextV2,
290 ) -> SendStateMachine {
291 match result {
292 Ok(payment_response) => {
293 client_ctx
294 .module
295 .client_ctx
296 .log_event(
297 &mut dbtx.module_tx(),
298 OutgoingPaymentSucceeded {
299 payment_image: old_state.common.contract.payment_image.clone(),
300 target_federation: payment_response.target_federation,
301 },
302 )
303 .await;
304 let client_input = ClientInput::<LightningInput> {
305 input: LightningInput::V0(LightningInputV0::Outgoing(
306 old_state.common.outpoint,
307 OutgoingWitness::Claim(payment_response.preimage),
308 )),
309 amounts: Amounts::new_bitcoin(old_state.common.contract.amount),
310 keys: vec![old_state.common.claim_keypair],
311 };
312
313 let outpoints = global_context
314 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
315 .await
316 .expect("Cannot claim input, additional funding needed")
317 .into_iter()
318 .collect();
319
320 old_state.update(SendSMState::Claiming(Claiming {
321 preimage: payment_response.preimage,
322 outpoints,
323 }))
324 }
325 Err(e) => {
326 client_ctx
327 .module
328 .client_ctx
329 .log_event(
330 &mut dbtx.module_tx(),
331 OutgoingPaymentFailed {
332 payment_image: old_state.common.contract.payment_image.clone(),
333 error: e.clone(),
334 },
335 )
336 .await;
337 old_state.update(SendSMState::Cancelled(e))
338 }
339 }
340 }
341}
342
343pub(crate) fn fresh_dispatch_refusal(
347 invoice_expired: bool,
348 fresh_max_delay: u64,
349) -> Option<Cancelled> {
350 if invoice_expired {
351 Some(Cancelled::InvoiceExpired)
352 } else if fresh_max_delay == 0 {
353 Some(Cancelled::TimeoutTooClose)
354 } else {
355 None
356 }
357}