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 match context
176 .gateway
177 .is_direct_swap(&invoice)
178 .await
179 .map_err(|e| Cancelled::RegistrationError(e.to_string()))?
180 {
181 Some((contract, client)) => {
182 match client
183 .get_first_module::<GatewayClientModuleV2>()
184 .expect("Must have client module")
185 .relay_direct_swap(
186 contract,
187 invoice
188 .amount_milli_satoshis()
189 .expect("amountless invoices are not supported"),
190 )
191 .await
192 {
193 Ok(final_receive_state) => match final_receive_state {
194 FinalReceiveState::Rejected => Err(Cancelled::Rejected),
195 FinalReceiveState::Success(preimage) => Ok(PaymentResponse {
196 preimage,
197 target_federation: Some(client.federation_id()),
198 }),
199 FinalReceiveState::Refunded => Err(Cancelled::Refunded),
200 FinalReceiveState::Failure => Err(Cancelled::Failure),
201 },
202 Err(e) => Err(Cancelled::FinalizationError(e.to_string())),
203 }
204 }
205 None => {
206 let preimage = context
207 .gateway
208 .pay(invoice, max_delay, max_fee)
209 .await
210 .map_err(|e| Cancelled::LightningRpcError(e.to_string()))?;
211 Ok(PaymentResponse {
212 preimage,
213 target_federation: None,
214 })
215 }
216 }
217 }
218
219 async fn transition_send_payment(
220 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
221 old_state: SendStateMachine,
222 global_context: DynGlobalClientContext,
223 result: Result<PaymentResponse, Cancelled>,
224 client_ctx: GatewayClientContextV2,
225 ) -> SendStateMachine {
226 match result {
227 Ok(payment_response) => {
228 client_ctx
229 .module
230 .client_ctx
231 .log_event(
232 &mut dbtx.module_tx(),
233 OutgoingPaymentSucceeded {
234 payment_image: old_state.common.contract.payment_image.clone(),
235 target_federation: payment_response.target_federation,
236 },
237 )
238 .await;
239 let client_input = ClientInput::<LightningInput> {
240 input: LightningInput::V0(LightningInputV0::Outgoing(
241 old_state.common.outpoint,
242 OutgoingWitness::Claim(payment_response.preimage),
243 )),
244 amount: old_state.common.contract.amount,
245 keys: vec![old_state.common.claim_keypair],
246 };
247
248 let outpoints = global_context
249 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
250 .await
251 .expect("Cannot claim input, additional funding needed")
252 .into_iter()
253 .collect();
254
255 old_state.update(SendSMState::Claiming(Claiming {
256 preimage: payment_response.preimage,
257 outpoints,
258 }))
259 }
260 Err(e) => {
261 client_ctx
262 .module
263 .client_ctx
264 .log_event(
265 &mut dbtx.module_tx(),
266 OutgoingPaymentFailed {
267 payment_image: old_state.common.contract.payment_image.clone(),
268 error: e.clone(),
269 },
270 )
271 .await;
272 old_state.update(SendSMState::Cancelled(e))
273 }
274 }
275 }
276}