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
100#[cfg_attr(doc, aquamarine::aquamarine)]
101impl State for SendStateMachine {
111 type ModuleContext = GatewayClientContextV2;
112
113 fn transitions(
114 &self,
115 context: &Self::ModuleContext,
116 global_context: &DynGlobalClientContext,
117 ) -> Vec<StateTransition<Self>> {
118 let gc = global_context.clone();
119 let gateway_context = context.clone();
120
121 match &self.state {
122 SendSMState::Sending => {
123 vec![StateTransition::new(
124 Self::send_payment(context.clone(), self.common.clone()),
125 move |dbtx, result, old_state| {
126 Box::pin(Self::transition_send_payment(
127 dbtx,
128 old_state,
129 gc.clone(),
130 result,
131 gateway_context.clone(),
132 ))
133 },
134 )]
135 }
136 SendSMState::Claiming(..) | SendSMState::Cancelled(..) => {
137 vec![]
138 }
139 }
140 }
141
142 fn operation_id(&self) -> OperationId {
143 self.common.operation_id
144 }
145}
146
147impl SendStateMachine {
148 async fn send_payment(
149 context: GatewayClientContextV2,
150 common: SendSMCommon,
151 ) -> Result<PaymentResponse, Cancelled> {
152 let SendSMCommon {
153 operation_id: _,
154 outpoint,
155 contract,
156 max_delay,
157 min_contract_amount,
158 invoice,
159 claim_keypair: _,
160 } = common;
161 let LightningInvoice::Bolt11(invoice) = invoice;
162
163 if max_delay == 0 {
169 return Err(Cancelled::TimeoutTooClose);
170 }
171
172 let Some(max_fee) = contract.amount.checked_sub(min_contract_amount) else {
173 return Err(Cancelled::Underfunded);
174 };
175
176 context.gateway.await_lightning_connected().await;
186 let fresh_max_delay = context
187 .module
188 .await_outgoing_contract_max_delay(outpoint)
189 .await;
190
191 let fresh_dispatch_refusal = fresh_dispatch_refusal(invoice.is_expired(), fresh_max_delay);
200
201 if let Some(client) = context.gateway.is_lnv1_invoice(&invoice).await {
206 let final_state = context
207 .gateway
208 .relay_lnv1_swap(client.value(), &invoice, fresh_dispatch_refusal.is_none())
209 .await;
210 return match final_state {
211 Ok(Some(final_receive_state)) => match final_receive_state {
212 FinalReceiveState::Rejected => Err(Cancelled::Rejected),
213 FinalReceiveState::Success(preimage) => Ok(PaymentResponse {
214 preimage,
215 target_federation: Some(client.value().federation_id()),
216 }),
217 FinalReceiveState::Refunded => Err(Cancelled::Refunded),
218 FinalReceiveState::Failure => Err(Cancelled::Failure),
219 },
220 Ok(None) => Err(fresh_dispatch_refusal
221 .expect("the relay only refuses a fresh dispatch when one was denied")),
222 Err(e) => Err(Cancelled::FinalizationError(e.fmt_compact().to_string())),
223 };
224 }
225
226 match context
227 .gateway
228 .is_direct_swap(&invoice)
229 .await
230 .map_err(|e| Cancelled::RegistrationError(e.fmt_compact().to_string()))?
231 {
232 Some((contract, client)) => {
233 match client
234 .get_first_module::<GatewayClientModuleV2>()
235 .expect("Must have client module")
236 .relay_direct_swap(
237 contract,
238 invoice
239 .amount_milli_satoshis()
240 .expect("amountless invoices are not supported"),
241 fresh_dispatch_refusal.is_none(),
242 )
243 .await
244 {
245 Ok(Some(final_receive_state)) => match final_receive_state {
246 FinalReceiveState::Rejected => Err(Cancelled::Rejected),
247 FinalReceiveState::Success(preimage) => Ok(PaymentResponse {
248 preimage,
249 target_federation: Some(client.federation_id()),
250 }),
251 FinalReceiveState::Refunded => Err(Cancelled::Refunded),
252 FinalReceiveState::Failure => Err(Cancelled::Failure),
253 },
254 Ok(None) => Err(fresh_dispatch_refusal
255 .expect("the relay only refuses a fresh dispatch when one was denied")),
256 Err(e) => Err(Cancelled::FinalizationError(e.fmt_compact().to_string())),
257 }
258 }
259 None => {
260 if let Some(refusal) = fresh_dispatch_refusal
264 && !context
265 .gateway
266 .outbound_payment_exists(*invoice.payment_hash())
267 .await
268 {
269 return Err(refusal);
270 }
271
272 let preimage = context
273 .gateway
274 .pay(invoice, fresh_max_delay, max_fee)
275 .await
276 .map_err(|e| Cancelled::LightningRpcError(e.to_string()))?;
277 Ok(PaymentResponse {
278 preimage,
279 target_federation: None,
280 })
281 }
282 }
283 }
284
285 async fn transition_send_payment(
286 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
287 old_state: SendStateMachine,
288 global_context: DynGlobalClientContext,
289 result: Result<PaymentResponse, Cancelled>,
290 client_ctx: GatewayClientContextV2,
291 ) -> SendStateMachine {
292 match result {
293 Ok(payment_response) => {
294 client_ctx
295 .module
296 .client_ctx
297 .log_event(
298 &mut dbtx.module_tx(),
299 OutgoingPaymentSucceeded {
300 payment_image: old_state.common.contract.payment_image.clone(),
301 target_federation: payment_response.target_federation,
302 },
303 )
304 .await;
305 let client_input = ClientInput::<LightningInput> {
306 input: LightningInput::V0(LightningInputV0::Outgoing(
307 old_state.common.outpoint,
308 OutgoingWitness::Claim(payment_response.preimage),
309 )),
310 amounts: Amounts::new_bitcoin(old_state.common.contract.amount),
311 keys: vec![old_state.common.claim_keypair],
312 };
313
314 let outpoints = global_context
315 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
316 .await
317 .expect("Cannot claim input, additional funding needed")
318 .into_iter()
319 .collect();
320
321 old_state.update(SendSMState::Claiming(Claiming {
322 preimage: payment_response.preimage,
323 outpoints,
324 }))
325 }
326 Err(e) => {
327 client_ctx
328 .module
329 .client_ctx
330 .log_event(
331 &mut dbtx.module_tx(),
332 OutgoingPaymentFailed {
333 payment_image: old_state.common.contract.payment_image.clone(),
334 error: e.clone(),
335 },
336 )
337 .await;
338 old_state.update(SendSMState::Cancelled(e))
339 }
340 }
341 }
342}
343
344pub(crate) fn fresh_dispatch_refusal(
348 invoice_expired: bool,
349 fresh_max_delay: u64,
350) -> Option<Cancelled> {
351 if invoice_expired {
352 Some(Cancelled::InvoiceExpired)
353 } else if fresh_max_delay == 0 {
354 Some(Cancelled::TimeoutTooClose)
355 } else {
356 None
357 }
358}