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 invoice.is_expired() {
166 return Err(Cancelled::InvoiceExpired);
167 }
168
169 if max_delay == 0 {
170 return Err(Cancelled::TimeoutTooClose);
171 }
172
173 let Some(max_fee) = contract.amount.checked_sub(min_contract_amount) else {
174 return Err(Cancelled::Underfunded);
175 };
176
177 if let Some(client) = context.gateway.is_lnv1_invoice(&invoice).await {
182 let final_state = context
183 .gateway
184 .relay_lnv1_swap(client.value(), &invoice)
185 .await;
186 return match final_state {
187 Ok(final_receive_state) => match final_receive_state {
188 FinalReceiveState::Rejected => Err(Cancelled::Rejected),
189 FinalReceiveState::Success(preimage) => Ok(PaymentResponse {
190 preimage,
191 target_federation: Some(client.value().federation_id()),
192 }),
193 FinalReceiveState::Refunded => Err(Cancelled::Refunded),
194 FinalReceiveState::Failure => Err(Cancelled::Failure),
195 },
196 Err(e) => Err(Cancelled::FinalizationError(e.to_string())),
197 };
198 }
199
200 match context
201 .gateway
202 .is_direct_swap(&invoice)
203 .await
204 .map_err(|e| Cancelled::RegistrationError(e.to_string()))?
205 {
206 Some((contract, client)) => {
207 match client
208 .get_first_module::<GatewayClientModuleV2>()
209 .expect("Must have client module")
210 .relay_direct_swap(
211 contract,
212 invoice
213 .amount_milli_satoshis()
214 .expect("amountless invoices are not supported"),
215 )
216 .await
217 {
218 Ok(final_receive_state) => match final_receive_state {
219 FinalReceiveState::Rejected => Err(Cancelled::Rejected),
220 FinalReceiveState::Success(preimage) => Ok(PaymentResponse {
221 preimage,
222 target_federation: Some(client.federation_id()),
223 }),
224 FinalReceiveState::Refunded => Err(Cancelled::Refunded),
225 FinalReceiveState::Failure => Err(Cancelled::Failure),
226 },
227 Err(e) => Err(Cancelled::FinalizationError(e.to_string())),
228 }
229 }
230 None => {
231 let preimage = context
232 .gateway
233 .pay(invoice, max_delay, max_fee)
234 .await
235 .map_err(|e| Cancelled::LightningRpcError(e.to_string()))?;
236 Ok(PaymentResponse {
237 preimage,
238 target_federation: None,
239 })
240 }
241 }
242 }
243
244 async fn transition_send_payment(
245 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
246 old_state: SendStateMachine,
247 global_context: DynGlobalClientContext,
248 result: Result<PaymentResponse, Cancelled>,
249 client_ctx: GatewayClientContextV2,
250 ) -> SendStateMachine {
251 match result {
252 Ok(payment_response) => {
253 if !client_ctx
261 .gateway
262 .claim_payment_image(
263 &old_state.common.contract.payment_image,
264 old_state.common.operation_id,
265 )
266 .await
267 {
268 client_ctx
269 .module
270 .client_ctx
271 .log_event(
272 &mut dbtx.module_tx(),
273 OutgoingPaymentFailed {
274 payment_image: old_state.common.contract.payment_image.clone(),
275 error: Cancelled::DuplicatePayment,
276 },
277 )
278 .await;
279
280 return old_state.update(SendSMState::Cancelled(Cancelled::DuplicatePayment));
281 }
282
283 client_ctx
284 .module
285 .client_ctx
286 .log_event(
287 &mut dbtx.module_tx(),
288 OutgoingPaymentSucceeded {
289 payment_image: old_state.common.contract.payment_image.clone(),
290 target_federation: payment_response.target_federation,
291 },
292 )
293 .await;
294 let client_input = ClientInput::<LightningInput> {
295 input: LightningInput::V0(LightningInputV0::Outgoing(
296 old_state.common.outpoint,
297 OutgoingWitness::Claim(payment_response.preimage),
298 )),
299 amounts: Amounts::new_bitcoin(old_state.common.contract.amount),
300 keys: vec![old_state.common.claim_keypair],
301 };
302
303 let outpoints = global_context
304 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
305 .await
306 .expect("Cannot claim input, additional funding needed")
307 .into_iter()
308 .collect();
309
310 old_state.update(SendSMState::Claiming(Claiming {
311 preimage: payment_response.preimage,
312 outpoints,
313 }))
314 }
315 Err(e) => {
316 client_ctx
317 .module
318 .client_ctx
319 .log_event(
320 &mut dbtx.module_tx(),
321 OutgoingPaymentFailed {
322 payment_image: old_state.common.contract.payment_image.clone(),
323 error: e.clone(),
324 },
325 )
326 .await;
327 old_state.update(SendSMState::Cancelled(e))
328 }
329 }
330 }
331}