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::{Amount, OutPoint};
12use fedimint_lnv2_common::contracts::OutgoingContract;
13use fedimint_lnv2_common::{LightningInput, LightningInputV0, LightningInvoice, OutgoingWitness};
14use serde::{Deserialize, Serialize};
15
16use super::FinalReceiveState;
17use super::events::{OutgoingPaymentFailed, OutgoingPaymentSucceeded};
18use crate::{GatewayClientContextV2, GatewayClientModuleV2};
19
20#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
21pub struct SendStateMachine {
22 pub common: SendSMCommon,
23 pub state: SendSMState,
24}
25
26impl SendStateMachine {
27 pub fn update(&self, state: SendSMState) -> Self {
28 Self {
29 common: self.common.clone(),
30 state,
31 }
32 }
33}
34
35impl fmt::Display for SendStateMachine {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 write!(
38 f,
39 "Send State Machine Operation ID: {:?} State: {}",
40 self.common.operation_id, self.state
41 )
42 }
43}
44
45#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
46pub struct SendSMCommon {
47 pub operation_id: OperationId,
48 pub outpoint: OutPoint,
49 pub contract: OutgoingContract,
50 pub max_delay: u64,
51 pub min_contract_amount: Amount,
52 pub invoice: LightningInvoice,
53 pub claim_keypair: Keypair,
54}
55
56#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
57pub enum SendSMState {
58 Sending,
59 Claiming(Claiming),
60 Cancelled(Cancelled),
61}
62
63#[derive(Debug, Serialize, Deserialize)]
64pub struct PaymentResponse {
65 preimage: [u8; 32],
66 target_federation: Option<FederationId>,
67}
68
69impl fmt::Display for SendSMState {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 match self {
72 SendSMState::Sending => write!(f, "Sending"),
73 SendSMState::Claiming(_) => write!(f, "Claiming"),
74 SendSMState::Cancelled(_) => write!(f, "Cancelled"),
75 }
76 }
77}
78
79#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
80pub struct Claiming {
81 pub preimage: [u8; 32],
82 pub outpoints: Vec<OutPoint>,
83}
84
85#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
86pub enum Cancelled {
87 InvoiceExpired,
88 TimeoutTooClose,
89 Underfunded,
90 RegistrationError(String),
91 FinalizationError(String),
92 Rejected,
93 Refunded,
94 Failure,
95 LightningRpcError(String),
96 DuplicatePayment,
97}
98
99#[cfg_attr(doc, aquamarine::aquamarine)]
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(
124 context.clone(),
125 self.common.max_delay,
126 self.common.min_contract_amount,
127 self.common.invoice.clone(),
128 self.common.contract.clone(),
129 ),
130 move |dbtx, result, old_state| {
131 Box::pin(Self::transition_send_payment(
132 dbtx,
133 old_state,
134 gc.clone(),
135 result,
136 gateway_context.clone(),
137 ))
138 },
139 )]
140 }
141 SendSMState::Claiming(..) | SendSMState::Cancelled(..) => {
142 vec![]
143 }
144 }
145 }
146
147 fn operation_id(&self) -> OperationId {
148 self.common.operation_id
149 }
150}
151
152impl SendStateMachine {
153 async fn send_payment(
154 context: GatewayClientContextV2,
155 max_delay: u64,
156 min_contract_amount: Amount,
157 invoice: LightningInvoice,
158 contract: OutgoingContract,
159 ) -> Result<PaymentResponse, Cancelled> {
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 let allow_fresh_dispatch = !invoice.is_expired();
184
185 if let Some(client) = context.gateway.is_lnv1_invoice(&invoice).await {
190 let final_state = context
191 .gateway
192 .relay_lnv1_swap(client.value(), &invoice, allow_fresh_dispatch)
193 .await;
194 return match final_state {
195 Ok(Some(final_receive_state)) => match final_receive_state {
196 FinalReceiveState::Rejected => Err(Cancelled::Rejected),
197 FinalReceiveState::Success(preimage) => Ok(PaymentResponse {
198 preimage,
199 target_federation: Some(client.value().federation_id()),
200 }),
201 FinalReceiveState::Refunded => Err(Cancelled::Refunded),
202 FinalReceiveState::Failure => Err(Cancelled::Failure),
203 },
204 Ok(None) => Err(Cancelled::InvoiceExpired),
205 Err(e) => Err(Cancelled::FinalizationError(e.to_string())),
206 };
207 }
208
209 match context
210 .gateway
211 .is_direct_swap(&invoice)
212 .await
213 .map_err(|e| Cancelled::RegistrationError(e.to_string()))?
214 {
215 Some((contract, client)) => {
216 match client
217 .get_first_module::<GatewayClientModuleV2>()
218 .expect("Must have client module")
219 .relay_direct_swap(
220 contract,
221 invoice
222 .amount_milli_satoshis()
223 .expect("amountless invoices are not supported"),
224 allow_fresh_dispatch,
225 )
226 .await
227 {
228 Ok(Some(final_receive_state)) => match final_receive_state {
229 FinalReceiveState::Rejected => Err(Cancelled::Rejected),
230 FinalReceiveState::Success(preimage) => Ok(PaymentResponse {
231 preimage,
232 target_federation: Some(client.federation_id()),
233 }),
234 FinalReceiveState::Refunded => Err(Cancelled::Refunded),
235 FinalReceiveState::Failure => Err(Cancelled::Failure),
236 },
237 Ok(None) => Err(Cancelled::InvoiceExpired),
238 Err(e) => Err(Cancelled::FinalizationError(e.to_string())),
239 }
240 }
241 None => {
242 if !allow_fresh_dispatch
246 && !context
247 .gateway
248 .outbound_payment_exists(*invoice.payment_hash())
249 .await
250 {
251 return Err(Cancelled::InvoiceExpired);
252 }
253
254 let preimage = context
255 .gateway
256 .pay(invoice, max_delay, max_fee)
257 .await
258 .map_err(|e| Cancelled::LightningRpcError(e.to_string()))?;
259 Ok(PaymentResponse {
260 preimage,
261 target_federation: None,
262 })
263 }
264 }
265 }
266
267 async fn transition_send_payment(
268 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
269 old_state: SendStateMachine,
270 global_context: DynGlobalClientContext,
271 result: Result<PaymentResponse, Cancelled>,
272 client_ctx: GatewayClientContextV2,
273 ) -> SendStateMachine {
274 match result {
275 Ok(payment_response) => {
276 if !client_ctx
284 .gateway
285 .claim_payment_image(
286 &old_state.common.contract.payment_image,
287 old_state.common.operation_id,
288 )
289 .await
290 {
291 client_ctx
292 .module
293 .client_ctx
294 .log_event(
295 &mut dbtx.module_tx(),
296 OutgoingPaymentFailed {
297 payment_image: old_state.common.contract.payment_image.clone(),
298 error: Cancelled::DuplicatePayment,
299 },
300 )
301 .await;
302
303 return old_state.update(SendSMState::Cancelled(Cancelled::DuplicatePayment));
304 }
305
306 client_ctx
307 .module
308 .client_ctx
309 .log_event(
310 &mut dbtx.module_tx(),
311 OutgoingPaymentSucceeded {
312 payment_image: old_state.common.contract.payment_image.clone(),
313 target_federation: payment_response.target_federation,
314 },
315 )
316 .await;
317 let client_input = ClientInput::<LightningInput> {
318 input: LightningInput::V0(LightningInputV0::Outgoing(
319 old_state.common.outpoint,
320 OutgoingWitness::Claim(payment_response.preimage),
321 )),
322 amounts: Amounts::new_bitcoin(old_state.common.contract.amount),
323 keys: vec![old_state.common.claim_keypair],
324 };
325
326 let outpoints = global_context
327 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
328 .await
329 .expect("Cannot claim input, additional funding needed")
330 .into_iter()
331 .collect();
332
333 old_state.update(SendSMState::Claiming(Claiming {
334 preimage: payment_response.preimage,
335 outpoints,
336 }))
337 }
338 Err(e) => {
339 client_ctx
340 .module
341 .client_ctx
342 .log_event(
343 &mut dbtx.module_tx(),
344 OutgoingPaymentFailed {
345 payment_image: old_state.common.contract.payment_image.clone(),
346 error: e.clone(),
347 },
348 )
349 .await;
350 old_state.update(SendSMState::Cancelled(e))
351 }
352 }
353 }
354}