use std::{collections::BTreeMap, sync::Mutex};
use anyswap_core::types::{Chain, SwapType};
use async_trait::async_trait;
use bitcoin::secp256k1::{PublicKey, SecretKey};
use serde::{Deserialize, Serialize};
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;
#[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,
pub send_amount: u64,
pub receive_amount: u64,
pub started_at: Timestamp,
pub status: Status,
pub committed_at: Option<Timestamp>,
pub preimage: Option<[u8; 32]>,
pub sent: Sent,
}
#[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 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> {
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,
})
}
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,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "status")]
pub enum Status {
Funding {
expires_at: Timestamp,
cancel_sent: bool,
},
Confirming {
txid: String,
confs: u32,
needed: u32,
matures_at: Option<Height>,
},
CoopKey {
expires_at: Timestamp,
matures_at: Height,
},
Spending {
txid: String,
confs: u32,
},
Refund {
expires_at: Timestamp,
refundable: u64,
matures_at: Option<Height>,
refunded: Vec<(String, bool)>,
},
Completed {
opening: String,
payout: Option<String>,
},
Lost {
opening: String,
},
Canceled {
opening: Option<String>,
},
}
impl Status {
pub fn is_terminal(&self) -> bool {
matches!(
self,
Self::Completed { .. } | Self::Lost { .. } | Self::Canceled { .. }
)
}
pub fn is_quiet(&self, now: Timestamp) -> bool {
match self {
Self::Refund {
expires_at,
refundable,
refunded,
..
} => {
*refundable == 0
&& refunded.iter().all(|(_, confirmed)| *confirmed)
&& now >= *expires_at
}
_ => self.is_terminal(),
}
}
}
#[derive(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<SwapRecord>, 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<SwapRecord>, 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<SwapRecord>, StoreError> {
Ok(self.0.lock().unwrap().values().cloned().collect())
}
}