use std::{collections::BTreeMap, sync::Mutex};
use anyswap_core::types::{Chain, Outpoint, SwapInStatus, SwapOutStatus, SwapType};
use async_trait::async_trait;
use bitcoin::secp256k1::{PublicKey, SecretKey};
use serde::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, serde_as};
use thiserror::Error;
use uuid::Uuid;
use super::{
error::DriverError,
rt::{MaybeSend, MaybeSync},
};
use crate::swap_script::{Side, Terms};
pub type Timestamp = u64;
pub type Height = u32;
#[serde_as]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SwapRecord {
pub version: u32,
pub swap_id: Uuid,
pub payment_hash: [u8; 32],
pub source: Endpoint,
pub dest: Endpoint,
#[serde_as(as = "DisplayFromStr")]
pub send_amount: u64,
#[serde_as(as = "DisplayFromStr")]
pub receive_amount: u64,
#[serde_as(as = "DisplayFromStr")]
pub base_fee: u64,
#[serde_as(as = "DisplayFromStr")]
pub service_fee: u64,
pub started_at: Timestamp,
pub status: Status,
#[serde(default)]
pub funds: Vec<Fund>,
#[serde(default)]
pub ended_at: Option<Timestamp>,
pub preimage: Option<[u8; 32]>,
pub sent: Sent,
#[serde(default)]
pub server_status: Option<String>,
#[serde(default)]
pub server_cancel: Option<ServerCancel>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)]
pub enum ServerStatus {
#[strum(default)]
In(SwapInStatus),
#[strum(default)]
Out(SwapOutStatus),
}
impl ServerStatus {
pub fn is_terminal(&self) -> bool {
match self {
Self::In(status) => matches!(status, SwapInStatus::Claimed | SwapInStatus::Canceled),
Self::Out(status) => matches!(
status,
SwapOutStatus::Claimed | SwapOutStatus::Canceled | SwapOutStatus::Refunded
),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Endpoint {
pub chain: Option<Chain>,
pub asset: Option<String>,
pub csv: Option<u32>,
pub server_pubkey: Option<PublicKey>,
pub server_blinding_pubkey: Option<PublicKey>,
pub destination: Option<String>,
pub invoice: Option<String>,
pub server_privkey: Option<SecretKey>,
}
impl Endpoint {
pub fn lightning(invoice: String) -> Self {
Self {
invoice: Some(invoice),
..Self::default()
}
}
pub fn on_chain(terms: Terms, destination: Option<String>) -> Self {
Self {
chain: Some(terms.chain),
asset: Some(terms.asset),
csv: Some(terms.csv),
server_pubkey: Some(terms.server_pubkey),
server_blinding_pubkey: terms.server_blinding_pubkey,
destination,
invoice: None,
server_privkey: None,
}
}
}
impl SwapRecord {
pub const VERSION: u32 = 1;
pub fn is_quiet(&self, now: Timestamp, window: u64, hook: bool) -> bool {
let destination = hook || self.source.destination.is_some();
let busy = self.funds.iter().any(|fund| match fund.state {
FundState::Refunding { .. } => true,
FundState::Locked { .. } | FundState::Refundable => destination,
FundState::Dust | FundState::Refunded { .. } => false,
});
let watched = self
.ended_at
.is_none_or(|ended| now < ended.saturating_add(window));
let swap_in = matches!(self.swap_type(), Ok(SwapType::SwapIn));
self.status.is_terminal() && !busy && !(swap_in && watched)
}
pub(crate) fn refund_keypath(&self) -> Option<SecretKey> {
self.source.server_privkey.filter(|_| !self.sent.refund_key)
}
pub(crate) fn locked(&self, csv: u32, confs: u32) -> bool {
confs == 0 || (self.refund_keypath().is_none() && confs < csv)
}
pub fn server_status(&self) -> Option<ServerStatus> {
let label = self.server_status.as_deref()?;
match self.swap_type().ok()? {
SwapType::SwapIn => label.parse().ok().map(ServerStatus::In),
SwapType::SwapOut => label.parse().ok().map(ServerStatus::Out),
SwapType::SwapChain => None,
}
}
pub fn swap_type(&self) -> Result<SwapType, DriverError> {
match (self.source.chain, self.dest.chain) {
(Some(_), None) => Ok(SwapType::SwapIn),
(None, Some(_)) => Ok(SwapType::SwapOut),
(Some(_), Some(_)) => Ok(SwapType::SwapChain),
(None, None) => Err(self.missing("chain")),
}
}
pub fn endpoint(&self, side: Side) -> &Endpoint {
match side {
Side::Source => &self.source,
Side::Dest => &self.dest,
}
}
pub fn endpoint_mut(&mut self, side: Side) -> &mut Endpoint {
match side {
Side::Source => &mut self.source,
Side::Dest => &mut self.dest,
}
}
pub fn terms(&self, side: Side) -> Result<Terms, DriverError> {
if self.version != Self::VERSION {
return Err(DriverError::RecordVersion(self.swap_id, self.version));
}
let end = self.endpoint(side);
let missing = |name: &str| self.missing(&format!("{side} {name}"));
Ok(Terms {
swap_id: self.swap_id,
side,
chain: end.chain.ok_or_else(|| missing("chain"))?,
asset: end.asset.clone().ok_or_else(|| missing("asset"))?,
csv: end.csv.ok_or_else(|| missing("csv"))?,
payment_hash: self.payment_hash,
server_pubkey: end.server_pubkey.ok_or_else(|| missing("server pubkey"))?,
server_blinding_pubkey: end.server_blinding_pubkey,
})
}
pub(crate) fn missing(&self, what: &str) -> DriverError {
DriverError::MissingField(self.swap_id, what.to_owned())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Sent {
pub cancel: bool,
pub refund_key: bool,
pub preimage: bool,
}
#[serde_as]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "status")]
pub enum Status {
Restoring,
Funding {
expires_at: Timestamp,
cancel_sent: bool,
},
Confirming {
txid: String,
confs: u32,
needed: u32,
#[serde(default)]
max_fee_rate: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
fee_rate_spike: Option<f64>,
},
Claiming {
opening: String,
claim: Option<String>,
confs: u32,
#[serde(default)]
#[serde_as(as = "Option<DisplayFromStr>")]
fee: Option<u64>,
},
Completed {
opening: String,
payout: Option<String>,
#[serde(default)]
#[serde_as(as = "Option<DisplayFromStr>")]
fee: Option<u64>,
},
Canceled {
opening: Option<String>,
reason: CancelReason,
},
}
impl Status {
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Completed { .. } | Self::Canceled { .. })
}
}
impl Eq for Status {}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum CancelReason {
ServerCanceled(ServerCancel),
CsvMatured,
ServerRefunded,
ClaimWindowClosed { at: Height },
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerCancel {
pub error_type: Option<String>,
pub cancel_message: Option<String>,
}
#[serde_as]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Fund {
pub outpoint: Outpoint,
#[serde_as(as = "DisplayFromStr")]
pub amount: u64,
pub state: FundState,
}
#[serde_as]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state")]
pub enum FundState {
Locked { at: Option<Height> },
Refundable,
Dust,
Refunding {
txid: String,
#[serde(default)]
#[serde_as(as = "Option<DisplayFromStr>")]
fee: Option<u64>,
},
Refunded {
txid: String,
#[serde(default)]
#[serde_as(as = "Option<DisplayFromStr>")]
fee: Option<u64>,
},
}
#[derive(Clone, Debug, Error)]
#[error("store: {0}")]
pub struct StoreError(pub String);
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait SwapStore: MaybeSend + MaybeSync {
async fn save(&self, record: &SwapRecord) -> Result<(), StoreError>;
async fn load(&self, swap_id: &Uuid) -> Result<Option<SwapRecord>, StoreError>;
async fn list(&self) -> Result<Vec<Uuid>, StoreError>;
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<S: SwapStore + ?Sized> SwapStore for std::sync::Arc<S> {
async fn save(&self, record: &SwapRecord) -> Result<(), StoreError> {
(**self).save(record).await
}
async fn load(&self, swap_id: &Uuid) -> Result<Option<SwapRecord>, StoreError> {
(**self).load(swap_id).await
}
async fn list(&self) -> Result<Vec<Uuid>, StoreError> {
(**self).list().await
}
}
#[derive(Default)]
pub struct MemoryStore(Mutex<BTreeMap<Uuid, SwapRecord>>);
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl SwapStore for MemoryStore {
async fn save(&self, record: &SwapRecord) -> Result<(), StoreError> {
self.0
.lock()
.unwrap()
.insert(record.swap_id, record.clone());
Ok(())
}
async fn load(&self, swap_id: &Uuid) -> Result<Option<SwapRecord>, StoreError> {
Ok(self.0.lock().unwrap().get(swap_id).cloned())
}
async fn list(&self) -> Result<Vec<Uuid>, StoreError> {
Ok(self.0.lock().unwrap().keys().copied().collect())
}
}