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::secp256k1::Keypair;
10use fedimint_core::{Amount, OutPoint};
11use fedimint_lnv2_common::contracts::OutgoingContract;
12use fedimint_lnv2_common::{LightningInput, LightningInputV0, LightningInvoice, OutgoingWitness};
13use serde::{Deserialize, Serialize};
14
15use super::FinalReceiveState;
16use super::events::{OutgoingPaymentFailed, OutgoingPaymentSucceeded};
17use crate::{GatewayClientContextV2, GatewayClientModuleV2};
18
19#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
20pub struct SendStateMachine {
21 pub common: SendSMCommon,
22 pub state: SendSMState,
23}
24
25impl SendStateMachine {
26 pub fn update(&self, state: SendSMState) -> Self {
27 Self {
28 common: self.common.clone(),
29 state,
30 }
31 }
32}
33
34impl fmt::Display for SendStateMachine {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 write!(
37 f,
38 "Send State Machine Operation ID: {:?} State: {}",
39 self.common.operation_id, self.state
40 )
41 }
42}
43
44#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
45pub struct SendSMCommon {
46 pub operation_id: OperationId,
47 pub outpoint: OutPoint,
48 pub contract: OutgoingContract,
49 pub max_delay: u64,
50 pub min_contract_amount: Amount,
51 pub invoice: LightningInvoice,
52 pub claim_keypair: Keypair,
53}
54
55#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
56pub enum SendSMState {
57 Sending,
58 Claiming(Claiming),
59 Cancelled(Cancelled),
60}
61
62#[derive(Debug, Serialize, Deserialize)]
63pub struct PaymentResponse {
64 preimage: [u8; 32],
65 target_federation: Option<FederationId>,
66}
67
68impl fmt::Display for SendSMState {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 match self {
71 SendSMState::Sending => write!(f, "Sending"),
72 SendSMState::Claiming(_) => write!(f, "Claiming"),
73 SendSMState::Cancelled(_) => write!(f, "Cancelled"),
74 }
75 }
76}
77
78#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
79pub struct Claiming {
80 pub preimage: [u8; 32],
81 pub outpoints: Vec<OutPoint>,
82}
83
84#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
85pub enum Cancelled {
86 InvoiceExpired,
87 TimeoutTooClose,
88 Underfunded,
89 RegistrationError(String),
90 FinalizationError(String),
91 Rejected,
92 Refunded,
93 Failure,
94 LightningRpcError(String),
95}
96
97#[cfg_attr(doc, aquamarine::aquamarine)]
98impl State for SendStateMachine {
108 type ModuleContext = GatewayClientContextV2;
109
110 fn transitions(
111 &self,
112 context: &Self::ModuleContext,
113 global_context: &DynGlobalClientContext,
114 ) -> Vec<StateTransition<Self>> {
115 let gc = global_context.clone();
116 let gateway_context = context.clone();
117
118 match &self.state {
119 SendSMState::Sending => {
120 vec![StateTransition::new(
121 Self::send_payment(
122 context.clone(),
123 self.common.max_delay,
124 self.common.min_contract_amount,
125 self.common.invoice.clone(),
126 self.common.contract.clone(),
127 ),
128 move |dbtx, result, old_state| {
129 Box::pin(Self::transition_send_payment(
130 dbtx,
131 old_state,
132 gc.clone(),
133 result,
134 gateway_context.clone(),
135 ))
136 },
137 )]
138 }
139 SendSMState::Claiming(..) | SendSMState::Cancelled(..) => {
140 vec![]
141 }
142 }
143 }
144
145 fn operation_id(&self) -> OperationId {
146 self.common.operation_id
147 }
148}
149
150impl SendStateMachine {
151 async fn send_payment(
152 context: GatewayClientContextV2,
153 max_delay: u64,
154 min_contract_amount: Amount,
155 invoice: LightningInvoice,
156 contract: OutgoingContract,
157 ) -> Result<PaymentResponse, Cancelled> {
158 let LightningInvoice::Bolt11(invoice) = invoice;
159
160 if invoice.is_expired() {
164 return Err(Cancelled::InvoiceExpired);
165 }
166
167 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 if let Some(client) = context.gateway.is_lnv1_invoice(&invoice).await {
180 let final_state = context
181 .gateway
182 .relay_lnv1_swap(client.value(), &invoice)
183 .await;
184 return match final_state {
185 Ok(final_receive_state) => match final_receive_state {
186 FinalReceiveState::Rejected => Err(Cancelled::Rejected),
187 FinalReceiveState::Success(preimage) => Ok(PaymentResponse {
188 preimage,
189 target_federation: Some(client.value().federation_id()),
190 }),
191 FinalReceiveState::Refunded => Err(Cancelled::Refunded),
192 FinalReceiveState::Failure => Err(Cancelled::Failure),
193 },
194 Err(e) => Err(Cancelled::FinalizationError(e.to_string())),
195 };
196 }
197
198 match context
199 .gateway
200 .is_direct_swap(&invoice)
201 .await
202 .map_err(|e| Cancelled::RegistrationError(e.to_string()))?
203 {
204 Some((contract, client)) => {
205 match client
206 .get_first_module::<GatewayClientModuleV2>()
207 .expect("Must have client module")
208 .relay_direct_swap(
209 contract,
210 invoice
211 .amount_milli_satoshis()
212 .expect("amountless invoices are not supported"),
213 )
214 .await
215 {
216 Ok(final_receive_state) => match final_receive_state {
217 FinalReceiveState::Rejected => Err(Cancelled::Rejected),
218 FinalReceiveState::Success(preimage) => Ok(PaymentResponse {
219 preimage,
220 target_federation: Some(client.federation_id()),
221 }),
222 FinalReceiveState::Refunded => Err(Cancelled::Refunded),
223 FinalReceiveState::Failure => Err(Cancelled::Failure),
224 },
225 Err(e) => Err(Cancelled::FinalizationError(e.to_string())),
226 }
227 }
228 None => {
229 let preimage = context
230 .gateway
231 .pay(invoice, max_delay, max_fee)
232 .await
233 .map_err(|e| Cancelled::LightningRpcError(e.to_string()))?;
234 Ok(PaymentResponse {
235 preimage,
236 target_federation: None,
237 })
238 }
239 }
240 }
241
242 async fn transition_send_payment(
243 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
244 old_state: SendStateMachine,
245 global_context: DynGlobalClientContext,
246 result: Result<PaymentResponse, Cancelled>,
247 client_ctx: GatewayClientContextV2,
248 ) -> SendStateMachine {
249 match result {
250 Ok(payment_response) => {
251 client_ctx
252 .module
253 .client_ctx
254 .log_event(
255 &mut dbtx.module_tx(),
256 OutgoingPaymentSucceeded {
257 payment_image: old_state.common.contract.payment_image.clone(),
258 target_federation: payment_response.target_federation,
259 },
260 )
261 .await;
262 let client_input = ClientInput::<LightningInput> {
263 input: LightningInput::V0(LightningInputV0::Outgoing(
264 old_state.common.outpoint,
265 OutgoingWitness::Claim(payment_response.preimage),
266 )),
267 amount: old_state.common.contract.amount,
268 keys: vec![old_state.common.claim_keypair],
269 };
270
271 let outpoints = global_context
272 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
273 .await
274 .expect("Cannot claim input, additional funding needed")
275 .into_iter()
276 .collect();
277
278 old_state.update(SendSMState::Claiming(Claiming {
279 preimage: payment_response.preimage,
280 outpoints,
281 }))
282 }
283 Err(e) => {
284 client_ctx
285 .module
286 .client_ctx
287 .log_event(
288 &mut dbtx.module_tx(),
289 OutgoingPaymentFailed {
290 payment_image: old_state.common.contract.payment_image.clone(),
291 error: e.clone(),
292 },
293 )
294 .await;
295 old_state.update(SendSMState::Cancelled(e))
296 }
297 }
298 }
299}