diff --git a/daemon/src/projection.rs b/daemon/src/projection.rs index 9b827e2..1bf5533 100644 --- a/daemon/src/projection.rs +++ b/daemon/src/projection.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; -use crate::model::cfd::OrderId; +use crate::model::cfd::{OrderId, Role, SettlementKind, UpdateCfdProposal}; use crate::model::{Leverage, Position, Timestamp, TradingPair}; -use crate::{bitmex_price_feed, model, Cfd, Order, UpdateCfdProposals}; +use crate::{bitmex_price_feed, model, tx, Cfd, Order, UpdateCfdProposals}; +use bdk::bitcoin::{Amount, Network}; use itertools::Itertools; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -259,3 +260,129 @@ impl From for Identity { Self(id.to_string()) } } + +#[derive(Debug, Clone, Serialize)] +pub enum CfdState { + OutgoingOrderRequest, + IncomingOrderRequest, + Accepted, + Rejected, + ContractSetup, + PendingOpen, + Open, + PendingCommit, + PendingCet, + PendingClose, + OpenCommitted, + IncomingSettlementProposal, + OutgoingSettlementProposal, + IncomingRollOverProposal, + OutgoingRollOverProposal, + Closed, + PendingRefund, + Refunded, + SetupFailed, +} + +pub fn to_cfd_state( + cfd_state: &model::cfd::CfdState, + proposal_status: Option<&UpdateCfdProposal>, +) -> CfdState { + match proposal_status { + Some(UpdateCfdProposal::Settlement { + direction: SettlementKind::Outgoing, + .. + }) => CfdState::OutgoingSettlementProposal, + Some(UpdateCfdProposal::Settlement { + direction: SettlementKind::Incoming, + .. + }) => CfdState::IncomingSettlementProposal, + Some(UpdateCfdProposal::RollOverProposal { + direction: SettlementKind::Outgoing, + .. + }) => CfdState::OutgoingRollOverProposal, + Some(UpdateCfdProposal::RollOverProposal { + direction: SettlementKind::Incoming, + .. + }) => CfdState::IncomingRollOverProposal, + None => match cfd_state { + // Filled in collaborative close in Open means that we're awaiting + // a collaborative closure + model::cfd::CfdState::Open { + collaborative_close: Some(_), + .. + } => CfdState::PendingClose, + model::cfd::CfdState::OutgoingOrderRequest { .. } => CfdState::OutgoingOrderRequest, + model::cfd::CfdState::IncomingOrderRequest { .. } => CfdState::IncomingOrderRequest, + model::cfd::CfdState::Accepted { .. } => CfdState::Accepted, + model::cfd::CfdState::Rejected { .. } => CfdState::Rejected, + model::cfd::CfdState::ContractSetup { .. } => CfdState::ContractSetup, + model::cfd::CfdState::PendingOpen { .. } => CfdState::PendingOpen, + model::cfd::CfdState::Open { .. } => CfdState::Open, + model::cfd::CfdState::OpenCommitted { .. } => CfdState::OpenCommitted, + model::cfd::CfdState::PendingRefund { .. } => CfdState::PendingRefund, + model::cfd::CfdState::Refunded { .. } => CfdState::Refunded, + model::cfd::CfdState::SetupFailed { .. } => CfdState::SetupFailed, + model::cfd::CfdState::PendingCommit { .. } => CfdState::PendingCommit, + model::cfd::CfdState::PendingCet { .. } => CfdState::PendingCet, + model::cfd::CfdState::Closed { .. } => CfdState::Closed, + }, + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct CfdDetails { + tx_url_list: Vec, + #[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc::opt")] + payout: Option, +} + +// TODO: don't expose +pub fn to_cfd_details(cfd: &model::cfd::Cfd, network: Network) -> CfdDetails { + CfdDetails { + tx_url_list: tx::to_tx_url_list(cfd.state.clone(), network), + payout: cfd.payout(), + } +} + +#[derive(Debug, derive_more::Display, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum CfdAction { + AcceptOrder, + RejectOrder, + Commit, + Settle, + AcceptSettlement, + RejectSettlement, + RollOver, + AcceptRollOver, + RejectRollOver, +} + +pub fn available_actions(state: CfdState, role: Role) -> Vec { + match (state, role) { + (CfdState::IncomingOrderRequest { .. }, Role::Maker) => { + vec![CfdAction::AcceptOrder, CfdAction::RejectOrder] + } + (CfdState::IncomingSettlementProposal { .. }, Role::Maker) => { + vec![CfdAction::AcceptSettlement, CfdAction::RejectSettlement] + } + (CfdState::IncomingRollOverProposal { .. }, Role::Maker) => { + vec![CfdAction::AcceptRollOver, CfdAction::RejectRollOver] + } + // If there is an outgoing settlement proposal already, user can't + // initiate new one + (CfdState::OutgoingSettlementProposal { .. }, Role::Maker) => { + vec![CfdAction::Commit] + } + // User is awaiting collaborative close, commit is left as a safeguard + (CfdState::PendingClose { .. }, _) => { + vec![CfdAction::Commit] + } + (CfdState::Open { .. }, Role::Taker) => { + vec![CfdAction::RollOver, CfdAction::Commit, CfdAction::Settle] + } + (CfdState::Open { .. }, Role::Maker) => vec![CfdAction::Commit], + _ => vec![], + } +} diff --git a/daemon/src/routes_maker.rs b/daemon/src/routes_maker.rs index 3e06b9a..588a411 100644 --- a/daemon/src/routes_maker.rs +++ b/daemon/src/routes_maker.rs @@ -3,9 +3,9 @@ use bdk::bitcoin::Network; use daemon::auth::Authenticated; use daemon::model::cfd::{OrderId, Role}; use daemon::model::{Price, Usd, WalletInfo}; -use daemon::projection::Feeds; +use daemon::projection::{CfdAction, Feeds}; use daemon::routes::EmbeddedFileExt; -use daemon::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent}; +use daemon::to_sse_event::{CfdsWithAuxData, ToSseEvent}; use daemon::{maker_cfd, maker_inc_connections, monitor, oracle, wallet}; use http_api_problem::{HttpApiProblem, StatusCode}; use rocket::http::{ContentType, Header, Status}; diff --git a/daemon/src/routes_taker.rs b/daemon/src/routes_taker.rs index 7e3e511..0fe9513 100644 --- a/daemon/src/routes_taker.rs +++ b/daemon/src/routes_taker.rs @@ -2,9 +2,9 @@ use bdk::bitcoin::{Amount, Network}; use daemon::connection::ConnectionStatus; use daemon::model::cfd::{calculate_long_margin, OrderId, Role}; use daemon::model::{Leverage, Price, Usd, WalletInfo}; -use daemon::projection::Feeds; +use daemon::projection::{CfdAction, Feeds}; use daemon::routes::EmbeddedFileExt; -use daemon::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent}; +use daemon::to_sse_event::{CfdsWithAuxData, ToSseEvent}; use daemon::{bitmex_price_feed, monitor, oracle, taker_cfd, wallet}; use http_api_problem::{HttpApiProblem, StatusCode}; use rocket::http::{ContentType, Status}; diff --git a/daemon/src/to_sse_event.rs b/daemon/src/to_sse_event.rs index 444a841..526fdbb 100644 --- a/daemon/src/to_sse_event.rs +++ b/daemon/src/to_sse_event.rs @@ -1,14 +1,13 @@ use crate::connection::ConnectionStatus; -use crate::model::cfd::{OrderId, Role, SettlementKind, UpdateCfdProposal, UpdateCfdProposals}; +use crate::model::cfd::{OrderId, Role, UpdateCfdProposals}; use crate::model::{Leverage, Position, Timestamp, TradingPair}; -use crate::projection::{CfdOrder, Identity, Price, Quote, Usd}; -use crate::tx::{self, TxUrl}; +use crate::projection::{self, CfdAction, CfdOrder, CfdState, Identity, Price, Quote, Usd}; use crate::{bitmex_price_feed, model}; use bdk::bitcoin::{Amount, Network, SignedAmount}; use rocket::request::FromParam; use rocket::response::stream::Event; use rust_decimal::Decimal; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use time::OffsetDateTime; use tokio::sync::watch; @@ -37,33 +36,12 @@ pub struct Cfd { pub actions: Vec, pub state_transition_timestamp: i64, - pub details: CfdDetails, + pub details: projection::CfdDetails, #[serde(with = "::time::serde::timestamp")] pub expiry_timestamp: OffsetDateTime, } -#[derive(Debug, Clone, Serialize)] -pub struct CfdDetails { - tx_url_list: Vec, - #[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc::opt")] - payout: Option, -} - -#[derive(Debug, derive_more::Display, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum CfdAction { - AcceptOrder, - RejectOrder, - Commit, - Settle, - AcceptSettlement, - RejectSettlement, - RollOver, - AcceptRollOver, - RejectRollOver, -} - impl<'v> FromParam<'v> for CfdAction { type Error = serde_plain::Error; @@ -73,29 +51,6 @@ impl<'v> FromParam<'v> for CfdAction { } } -#[derive(Debug, Clone, Serialize)] -pub enum CfdState { - OutgoingOrderRequest, - IncomingOrderRequest, - Accepted, - Rejected, - ContractSetup, - PendingOpen, - Open, - PendingCommit, - PendingCet, - PendingClose, - OpenCommitted, - IncomingSettlementProposal, - OutgoingSettlementProposal, - IncomingRollOverProposal, - OutgoingRollOverProposal, - Closed, - PendingRefund, - Refunded, - SetupFailed, -} - pub trait ToSseEvent { fn to_sse_event(&self) -> Event; } @@ -155,12 +110,7 @@ impl ToSseEvent for CfdsWithAuxData { }); let pending_proposal = self.pending_proposals.get(&cfd.order.id); - let state = to_cfd_state(&cfd.state, pending_proposal); - - let details = CfdDetails { - tx_url_list: tx::to_tx_url_list(cfd.state.clone(), network), - payout: cfd.payout(), - }; + let state = projection::to_cfd_state(&cfd.state, pending_proposal); Cfd { order_id: cfd.order.id, @@ -173,14 +123,14 @@ impl ToSseEvent for CfdsWithAuxData { profit_btc, profit_in_percent: profit_in_percent.round_dp(1).to_string(), state: state.clone(), - actions: available_actions(state, cfd.role()), + actions: projection::available_actions(state, cfd.role()), state_transition_timestamp: cfd.state.get_transition_timestamp().seconds(), // TODO: Depending on the state the margin might be set (i.e. in Open we save it // in the DB internally) and does not have to be calculated margin: cfd.margin().expect("margin to be available"), margin_counterparty: cfd.counterparty_margin().expect("margin to be available"), - details, + details: projection::to_cfd_details(cfd, network), expiry_timestamp: match cfd.expiry_timestamp() { None => cfd.order.oracle_event_id.timestamp(), Some(timestamp) => timestamp, @@ -236,86 +186,12 @@ impl ToSseEvent for ConnectionStatus { } } -fn to_cfd_state( - cfd_state: &model::cfd::CfdState, - proposal_status: Option<&UpdateCfdProposal>, -) -> CfdState { - match proposal_status { - Some(UpdateCfdProposal::Settlement { - direction: SettlementKind::Outgoing, - .. - }) => CfdState::OutgoingSettlementProposal, - Some(UpdateCfdProposal::Settlement { - direction: SettlementKind::Incoming, - .. - }) => CfdState::IncomingSettlementProposal, - Some(UpdateCfdProposal::RollOverProposal { - direction: SettlementKind::Outgoing, - .. - }) => CfdState::OutgoingRollOverProposal, - Some(UpdateCfdProposal::RollOverProposal { - direction: SettlementKind::Incoming, - .. - }) => CfdState::IncomingRollOverProposal, - None => match cfd_state { - // Filled in collaborative close in Open means that we're awaiting - // a collaborative closure - model::cfd::CfdState::Open { - collaborative_close: Some(_), - .. - } => CfdState::PendingClose, - model::cfd::CfdState::OutgoingOrderRequest { .. } => CfdState::OutgoingOrderRequest, - model::cfd::CfdState::IncomingOrderRequest { .. } => CfdState::IncomingOrderRequest, - model::cfd::CfdState::Accepted { .. } => CfdState::Accepted, - model::cfd::CfdState::Rejected { .. } => CfdState::Rejected, - model::cfd::CfdState::ContractSetup { .. } => CfdState::ContractSetup, - model::cfd::CfdState::PendingOpen { .. } => CfdState::PendingOpen, - model::cfd::CfdState::Open { .. } => CfdState::Open, - model::cfd::CfdState::OpenCommitted { .. } => CfdState::OpenCommitted, - model::cfd::CfdState::PendingRefund { .. } => CfdState::PendingRefund, - model::cfd::CfdState::Refunded { .. } => CfdState::Refunded, - model::cfd::CfdState::SetupFailed { .. } => CfdState::SetupFailed, - model::cfd::CfdState::PendingCommit { .. } => CfdState::PendingCommit, - model::cfd::CfdState::PendingCet { .. } => CfdState::PendingCet, - model::cfd::CfdState::Closed { .. } => CfdState::Closed, - }, - } -} - impl ToSseEvent for Quote { fn to_sse_event(&self) -> Event { Event::json(self).event("quote") } } -fn available_actions(state: CfdState, role: Role) -> Vec { - match (state, role) { - (CfdState::IncomingOrderRequest { .. }, Role::Maker) => { - vec![CfdAction::AcceptOrder, CfdAction::RejectOrder] - } - (CfdState::IncomingSettlementProposal { .. }, Role::Maker) => { - vec![CfdAction::AcceptSettlement, CfdAction::RejectSettlement] - } - (CfdState::IncomingRollOverProposal { .. }, Role::Maker) => { - vec![CfdAction::AcceptRollOver, CfdAction::RejectRollOver] - } - // If there is an outgoing settlement proposal already, user can't - // initiate new one - (CfdState::OutgoingSettlementProposal { .. }, Role::Maker) => { - vec![CfdAction::Commit] - } - // User is awaiting collaborative close, commit is left as a safeguard - (CfdState::PendingClose { .. }, _) => { - vec![CfdAction::Commit] - } - (CfdState::Open { .. }, Role::Taker) => { - vec![CfdAction::RollOver, CfdAction::Commit, CfdAction::Settle] - } - (CfdState::Open { .. }, Role::Maker) => vec![CfdAction::Commit], - _ => vec![], - } -} - #[cfg(test)] mod tests { use super::*;