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}
97
98#[cfg_attr(doc, aquamarine::aquamarine)]
99impl State for SendStateMachine {
109 type ModuleContext = GatewayClientContextV2;
110
111 fn transitions(
112 &self,
113 context: &Self::ModuleContext,
114 global_context: &DynGlobalClientContext,
115 ) -> Vec<StateTransition<Self>> {
116 let gc = global_context.clone();
117 let gateway_context = context.clone();
118
119 match &self.state {
120 SendSMState::Sending => {
121 vec![StateTransition::new(
122 Self::send_payment(
123 context.clone(),
124 self.common.max_delay,
125 self.common.min_contract_amount,
126 self.common.invoice.clone(),
127 self.common.contract.clone(),
128 ),
129 move |dbtx, result, old_state| {
130 Box::pin(Self::transition_send_payment(
131 dbtx,
132 old_state,
133 gc.clone(),
134 result,
135 gateway_context.clone(),
136 ))
137 },
138 )]
139 }
140 SendSMState::Claiming(..) | SendSMState::Cancelled(..) => {
141 vec![]
142 }
143 }
144 }
145
146 fn operation_id(&self) -> OperationId {
147 self.common.operation_id
148 }
149}
150
151impl SendStateMachine {
152 async fn send_payment(
153 context: GatewayClientContextV2,
154 max_delay: u64,
155 min_contract_amount: Amount,
156 invoice: LightningInvoice,
157 contract: OutgoingContract,
158 ) -> Result<PaymentResponse, Cancelled> {
159 let LightningInvoice::Bolt11(invoice) = invoice;
160
161 if invoice.is_expired() {
165 return Err(Cancelled::InvoiceExpired);
166 }
167
168 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 if let Some(client) = context.gateway.is_lnv1_invoice(&invoice).await {
181 let final_state = context
182 .gateway
183 .relay_lnv1_swap(client.value(), &invoice)
184 .await;
185 return match final_state {
186 Ok(final_receive_state) => match final_receive_state {
187 FinalReceiveState::Rejected => Err(Cancelled::Rejected),
188 FinalReceiveState::Success(preimage) => Ok(PaymentResponse {
189 preimage,
190 target_federation: Some(client.value().federation_id()),
191 }),
192 FinalReceiveState::Refunded => Err(Cancelled::Refunded),
193 FinalReceiveState::Failure => Err(Cancelled::Failure),
194 },
195 Err(e) => Err(Cancelled::FinalizationError(e.to_string())),
196 };
197 }
198
199 match context
200 .gateway
201 .is_direct_swap(&invoice)
202 .await
203 .map_err(|e| Cancelled::RegistrationError(e.to_string()))?
204 {
205 Some((contract, client)) => {
206 match client
207 .get_first_module::<GatewayClientModuleV2>()
208 .expect("Must have client module")
209 .relay_direct_swap(
210 contract,
211 invoice
212 .amount_milli_satoshis()
213 .expect("amountless invoices are not supported"),
214 )
215 .await
216 {
217 Ok(final_receive_state) => match final_receive_state {
218 FinalReceiveState::Rejected => Err(Cancelled::Rejected),
219 FinalReceiveState::Success(preimage) => Ok(PaymentResponse {
220 preimage,
221 target_federation: Some(client.federation_id()),
222 }),
223 FinalReceiveState::Refunded => Err(Cancelled::Refunded),
224 FinalReceiveState::Failure => Err(Cancelled::Failure),
225 },
226 Err(e) => Err(Cancelled::FinalizationError(e.to_string())),
227 }
228 }
229 None => {
230 let preimage = context
231 .gateway
232 .pay(invoice, max_delay, max_fee)
233 .await
234 .map_err(|e| Cancelled::LightningRpcError(e.to_string()))?;
235 Ok(PaymentResponse {
236 preimage,
237 target_federation: None,
238 })
239 }
240 }
241 }
242
243 async fn transition_send_payment(
244 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
245 old_state: SendStateMachine,
246 global_context: DynGlobalClientContext,
247 result: Result<PaymentResponse, Cancelled>,
248 client_ctx: GatewayClientContextV2,
249 ) -> SendStateMachine {
250 match result {
251 Ok(payment_response) => {
252 client_ctx
253 .module
254 .client_ctx
255 .log_event(
256 &mut dbtx.module_tx(),
257 OutgoingPaymentSucceeded {
258 payment_image: old_state.common.contract.payment_image.clone(),
259 target_federation: payment_response.target_federation,
260 },
261 )
262 .await;
263 let client_input = ClientInput::<LightningInput> {
264 input: LightningInput::V0(LightningInputV0::Outgoing(
265 old_state.common.outpoint,
266 OutgoingWitness::Claim(payment_response.preimage),
267 )),
268 amounts: Amounts::new_bitcoin(old_state.common.contract.amount),
269 keys: vec![old_state.common.claim_keypair],
270 };
271
272 let outpoints = global_context
273 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
274 .await
275 .expect("Cannot claim input, additional funding needed")
276 .into_iter()
277 .collect();
278
279 old_state.update(SendSMState::Claiming(Claiming {
280 preimage: payment_response.preimage,
281 outpoints,
282 }))
283 }
284 Err(e) => {
285 client_ctx
286 .module
287 .client_ctx
288 .log_event(
289 &mut dbtx.module_tx(),
290 OutgoingPaymentFailed {
291 payment_image: old_state.common.contract.payment_image.clone(),
292 error: e.clone(),
293 },
294 )
295 .await;
296 old_state.update(SendSMState::Cancelled(e))
297 }
298 }
299 }
300}