1use std::{collections::BTreeMap, sync::Mutex};
2
3use anyswap_core::types::{Chain, Outpoint, SwapInStatus, SwapOutStatus, SwapType};
4use async_trait::async_trait;
5use bitcoin::secp256k1::{PublicKey, SecretKey};
6use serde::{Deserialize, Serialize};
7use serde_with::{DisplayFromStr, serde_as};
8use thiserror::Error;
9use uuid::Uuid;
10
11use super::{
12 error::DriverError,
13 rt::{MaybeSend, MaybeSync},
14};
15use crate::swap_script::{Side, Terms};
16
17pub type Timestamp = u64;
19
20pub type Height = u32;
22
23#[serde_as]
27#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
28pub struct SwapRecord {
29 pub version: u32,
30 pub swap_id: Uuid,
31 pub payment_hash: [u8; 32],
32 pub source: Endpoint,
33 pub dest: Endpoint,
34 #[serde_as(as = "DisplayFromStr")]
35 pub send_amount: u64,
36 #[serde_as(as = "DisplayFromStr")]
37 pub receive_amount: u64,
38 #[serde_as(as = "DisplayFromStr")]
41 pub base_fee: u64,
42 #[serde_as(as = "DisplayFromStr")]
43 pub service_fee: u64,
44 pub started_at: Timestamp,
46 pub status: Status,
47 #[serde(default)]
49 pub funds: Vec<Fund>,
50 #[serde(default)]
53 pub ended_at: Option<Timestamp>,
54 pub preimage: Option<[u8; 32]>,
58 pub sent: Sent,
59 #[serde(default)]
62 pub server_status: Option<String>,
63 #[serde(default)]
66 pub server_cancel: Option<ServerCancel>,
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)]
71pub enum ServerStatus {
72 #[strum(default)]
73 In(SwapInStatus),
74 #[strum(default)]
75 Out(SwapOutStatus),
76}
77
78impl ServerStatus {
79 pub fn is_terminal(&self) -> bool {
80 match self {
81 Self::In(status) => matches!(status, SwapInStatus::Claimed | SwapInStatus::Canceled),
82 Self::Out(status) => matches!(
83 status,
84 SwapOutStatus::Claimed | SwapOutStatus::Canceled | SwapOutStatus::Refunded
85 ),
86 }
87 }
88}
89
90#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
94pub struct Endpoint {
95 pub chain: Option<Chain>,
96 pub asset: Option<String>,
97 pub csv: Option<u32>,
98 pub server_pubkey: Option<PublicKey>,
99 pub server_blinding_pubkey: Option<PublicKey>,
100 pub destination: Option<String>,
103 pub invoice: Option<String>,
105 pub server_privkey: Option<SecretKey>,
107}
108
109impl Endpoint {
110 pub fn lightning(invoice: String) -> Self {
111 Self {
112 invoice: Some(invoice),
113 ..Self::default()
114 }
115 }
116
117 pub fn on_chain(terms: Terms, destination: Option<String>) -> Self {
118 Self {
119 chain: Some(terms.chain),
120 asset: Some(terms.asset),
121 csv: Some(terms.csv),
122 server_pubkey: Some(terms.server_pubkey),
123 server_blinding_pubkey: terms.server_blinding_pubkey,
124 destination,
125 invoice: None,
126 server_privkey: None,
127 }
128 }
129}
130
131impl SwapRecord {
132 pub const VERSION: u32 = 1;
134
135 pub fn is_quiet(&self, now: Timestamp, window: u64, hook: bool) -> bool {
142 let destination = hook || self.source.destination.is_some();
143 let busy = self.funds.iter().any(|fund| match fund.state {
144 FundState::Refunding { .. } | FundState::Locked { .. } => true,
145 FundState::Refundable => destination,
146 FundState::Dust | FundState::Refunded { .. } => false,
147 });
148 let watched = self
149 .ended_at
150 .is_none_or(|ended| now < ended.saturating_add(window));
151 let swap_in = matches!(self.swap_type(), Ok(SwapType::SwapIn));
152 self.status.is_terminal() && !busy && !(swap_in && watched)
153 }
154
155 pub(crate) fn refund_keypath(&self) -> Option<SecretKey> {
159 self.source.server_privkey.filter(|_| !self.sent.refund_key)
160 }
161
162 pub(crate) fn locked(&self, csv: u32, confs: u32) -> bool {
166 confs == 0 || (self.refund_keypath().is_none() && confs < csv)
167 }
168
169 pub fn server_status(&self) -> Option<ServerStatus> {
170 let label = self.server_status.as_deref()?;
171 match self.swap_type().ok()? {
172 SwapType::SwapIn => label.parse().ok().map(ServerStatus::In),
173 SwapType::SwapOut => label.parse().ok().map(ServerStatus::Out),
174 SwapType::SwapChain => None,
175 }
176 }
177
178 pub fn swap_type(&self) -> Result<SwapType, DriverError> {
181 match (self.source.chain, self.dest.chain) {
182 (Some(_), None) => Ok(SwapType::SwapIn),
183 (None, Some(_)) => Ok(SwapType::SwapOut),
184 (Some(_), Some(_)) => Ok(SwapType::SwapChain),
185 (None, None) => Err(self.missing("chain")),
186 }
187 }
188
189 pub fn endpoint(&self, side: Side) -> &Endpoint {
190 match side {
191 Side::Source => &self.source,
192 Side::Dest => &self.dest,
193 }
194 }
195
196 pub fn endpoint_mut(&mut self, side: Side) -> &mut Endpoint {
197 match side {
198 Side::Source => &mut self.source,
199 Side::Dest => &mut self.dest,
200 }
201 }
202
203 pub fn terms(&self, side: Side) -> Result<Terms, DriverError> {
206 if self.version != Self::VERSION {
207 return Err(DriverError::RecordVersion(self.swap_id, self.version));
208 }
209 let end = self.endpoint(side);
210 let missing = |name: &str| self.missing(&format!("{side} {name}"));
211 Ok(Terms {
212 swap_id: self.swap_id,
213 side,
214 chain: end.chain.ok_or_else(|| missing("chain"))?,
215 asset: end.asset.clone().ok_or_else(|| missing("asset"))?,
216 csv: end.csv.ok_or_else(|| missing("csv"))?,
217 payment_hash: self.payment_hash,
218 server_pubkey: end.server_pubkey.ok_or_else(|| missing("server pubkey"))?,
219 server_blinding_pubkey: end.server_blinding_pubkey,
220 })
221 }
222
223 pub(crate) fn missing(&self, what: &str) -> DriverError {
224 DriverError::MissingField(self.swap_id, what.to_owned())
225 }
226}
227
228#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
230pub struct Sent {
231 pub cancel: bool,
232 pub refund_key: bool,
233 pub preimage: bool,
234}
235
236#[serde_as]
239#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
240#[serde(tag = "status")]
241pub enum Status {
242 Restoring,
244 Funding {
245 expires_at: Timestamp,
246 cancel_sent: bool,
247 },
248 Confirming {
252 txid: String,
253 confs: u32,
254 needed: u32,
255 #[serde(default)]
256 max_fee_rate: f64,
257 #[serde(default, skip_serializing_if = "Option::is_none")]
258 fee_rate_spike: Option<f64>,
259 },
260 Claiming {
263 opening: String,
264 claim: Option<String>,
265 confs: u32,
266 #[serde(default)]
267 #[serde_as(as = "Option<DisplayFromStr>")]
268 fee: Option<u64>,
269 },
270 Completed {
273 opening: String,
274 payout: Option<String>,
275 #[serde(default)]
276 #[serde_as(as = "Option<DisplayFromStr>")]
277 fee: Option<u64>,
278 },
279 Canceled {
285 opening: Option<String>,
286 reason: CancelReason,
287 },
288}
289
290impl Status {
291 pub fn is_terminal(&self) -> bool {
292 matches!(self, Self::Completed { .. } | Self::Canceled { .. })
293 }
294}
295
296impl Eq for Status {}
299
300#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
302#[serde(tag = "kind")]
303pub enum CancelReason {
304 ServerCanceled(ServerCancel),
307 CsvMatured,
310 ServerRefunded,
312 ClaimWindowClosed { at: Height },
316}
317
318#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
321pub struct ServerCancel {
322 pub error_type: Option<String>,
323 pub cancel_message: Option<String>,
324}
325
326#[serde_as]
330#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
331pub struct Fund {
332 pub outpoint: Outpoint,
333 #[serde_as(as = "DisplayFromStr")]
334 pub amount: u64,
335 pub state: FundState,
336}
337
338#[serde_as]
339#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
340#[serde(tag = "state")]
341pub enum FundState {
342 Locked { at: Option<Height> },
344 Refundable,
346 Dust,
350 Refunding {
353 txid: String,
354 #[serde(default)]
355 #[serde_as(as = "Option<DisplayFromStr>")]
356 fee: Option<u64>,
357 },
358 Refunded {
360 txid: String,
361 #[serde(default)]
362 #[serde_as(as = "Option<DisplayFromStr>")]
363 fee: Option<u64>,
364 },
365}
366
367#[derive(Clone, Debug, Error)]
368#[error("store: {0}")]
369pub struct StoreError(pub String);
370
371#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
373#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
374pub trait SwapStore: MaybeSend + MaybeSync {
375 async fn save(&self, record: &SwapRecord) -> Result<(), StoreError>;
377 async fn load(&self, swap_id: &Uuid) -> Result<Option<SwapRecord>, StoreError>;
378 async fn list(&self) -> Result<Vec<Uuid>, StoreError>;
381}
382
383#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
384#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
385impl<S: SwapStore + ?Sized> SwapStore for std::sync::Arc<S> {
386 async fn save(&self, record: &SwapRecord) -> Result<(), StoreError> {
387 (**self).save(record).await
388 }
389
390 async fn load(&self, swap_id: &Uuid) -> Result<Option<SwapRecord>, StoreError> {
391 (**self).load(swap_id).await
392 }
393
394 async fn list(&self) -> Result<Vec<Uuid>, StoreError> {
395 (**self).list().await
396 }
397}
398
399#[derive(Default)]
401pub struct MemoryStore(Mutex<BTreeMap<Uuid, SwapRecord>>);
402
403#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
404#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
405impl SwapStore for MemoryStore {
406 async fn save(&self, record: &SwapRecord) -> Result<(), StoreError> {
407 self.0
408 .lock()
409 .unwrap()
410 .insert(record.swap_id, record.clone());
411 Ok(())
412 }
413
414 async fn load(&self, swap_id: &Uuid) -> Result<Option<SwapRecord>, StoreError> {
415 Ok(self.0.lock().unwrap().get(swap_id).cloned())
416 }
417
418 async fn list(&self) -> Result<Vec<Uuid>, StoreError> {
419 Ok(self.0.lock().unwrap().keys().copied().collect())
420 }
421}