diff --git a/daemon/src/maker.rs b/daemon/src/maker.rs index e95b189..d39afc6 100644 --- a/daemon/src/maker.rs +++ b/daemon/src/maker.rs @@ -1,5 +1,6 @@ use crate::auth::MAKER_USERNAME; use crate::db::load_all_cfds; +use crate::model::cfd::SettlementProposals; use crate::seed::Seed; use crate::wallet::Wallet; use anyhow::{Context, Result}; @@ -10,6 +11,7 @@ use model::cfd::Order; use model::WalletInfo; use rocket::fairing::AdHoc; use rocket_db_pools::Database; +use std::collections::HashMap; use std::path::PathBuf; use std::task::Poll; use std::time::Duration; @@ -111,6 +113,8 @@ async fn main() -> Result<()> { let (order_feed_sender, order_feed_receiver) = watch::channel::>(None); let (wallet_feed_sender, wallet_feed_receiver) = watch::channel::(wallet_info); + let (settlement_feed_sender, settlement_feed_receiver) = + watch::channel::(SettlementProposals::Incoming(HashMap::new())); let figment = rocket::Config::figment() .merge(("databases.maker.url", data_dir.join("maker.sqlite"))) @@ -127,6 +131,7 @@ async fn main() -> Result<()> { rocket::custom(figment) .manage(order_feed_receiver) .manage(wallet_feed_receiver) + .manage(settlement_feed_receiver) .manage(auth_password) .manage(quote_updates) .attach(Db::init()) @@ -175,6 +180,7 @@ async fn main() -> Result<()> { schnorrsig::PublicKey::from_keypair(SECP256K1, &oracle), cfd_feed_sender, order_feed_sender, + settlement_feed_sender, maker_inc_connections_address.clone(), monitor_actor_address.clone(), oracle_actor_address, diff --git a/daemon/src/maker_cfd.rs b/daemon/src/maker_cfd.rs index 7250814..7c1f131 100644 --- a/daemon/src/maker_cfd.rs +++ b/daemon/src/maker_cfd.rs @@ -6,7 +6,7 @@ use crate::db::{ use crate::maker_inc_connections::TakerCommand; use crate::model::cfd::{ Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, Dlc, Order, OrderId, Role, - SettlementProposal, + SettlementProposal, SettlementProposals, }; use crate::model::{TakerId, Usd}; use crate::monitor::MonitorParams; @@ -17,6 +17,7 @@ use async_trait::async_trait; use bdk::bitcoin::secp256k1::schnorrsig; use futures::channel::mpsc; use futures::{future, SinkExt}; +use std::collections::HashMap; use std::time::SystemTime; use tokio::sync::watch; use xtra::prelude::*; @@ -34,6 +35,14 @@ pub struct Commit { pub order_id: OrderId, } +pub struct AcceptSettlement { + pub order_id: OrderId, +} + +pub struct RejectSettlement { + pub order_id: OrderId, +} + pub struct NewOrder(pub Order); pub struct NewTakerOnline { @@ -56,12 +65,14 @@ pub struct Actor { oracle_pk: schnorrsig::PublicKey, cfd_feed_actor_inbox: watch::Sender>, order_feed_sender: watch::Sender>, + settlements_feed_sender: watch::Sender, takers: Address, current_order_id: Option, monitor_actor: Address>, setup_state: SetupState, latest_announcement: Option, _oracle_actor: Address>>, + current_settlement_proposals: HashMap, } enum SetupState { @@ -80,6 +91,7 @@ impl Actor { oracle_pk: schnorrsig::PublicKey, cfd_feed_actor_inbox: watch::Sender>, order_feed_sender: watch::Sender>, + settlements_feed_sender: watch::Sender, takers: Address, monitor_actor: Address>, oracle_actor: Address>>, @@ -90,15 +102,25 @@ impl Actor { oracle_pk, cfd_feed_actor_inbox, order_feed_sender, + settlements_feed_sender, takers, current_order_id: None, monitor_actor, setup_state: SetupState::None, latest_announcement: None, _oracle_actor: oracle_actor, + current_settlement_proposals: HashMap::new(), } } + fn send_current_settlement_proposals(&self) -> Result<()> { + Ok(self + .settlements_feed_sender + .send(SettlementProposals::Incoming( + self.current_settlement_proposals.clone(), + ))?) + } + async fn handle_new_order(&mut self, order: Order) -> Result<()> { // 1. Save to DB let mut conn = self.db.acquire().await?; @@ -142,7 +164,10 @@ impl Actor { "Received settlement proposal from the taker: {:?}", proposal ); - // TODO: Handle the proposal + self.current_settlement_proposals + .insert(proposal.order_id, proposal); + self.send_current_settlement_proposals()?; + Ok(()) } @@ -427,7 +452,29 @@ impl Actor { self.cfd_feed_actor_inbox .send(load_all_cfds(&mut conn).await?)?; + Ok(()) + } + + async fn handle_accept_settlement(&mut self, order_id: OrderId) -> Result<()> { + tracing::debug!(%order_id, "Maker accepts a settlement proposal" ); + // TODO: Initiate the settlement + + self.current_settlement_proposals + .remove(&order_id) + .context("Could not find proposal for given order id")?; + self.send_current_settlement_proposals()?; + Ok(()) + } + async fn handle_reject_settlement(&mut self, order_id: OrderId) -> Result<()> { + tracing::debug!(%order_id, "Maker rejects a settlement proposal" ); + // TODO: Handle rejection offer: + // - notify the taker that the settlement was rejected + + self.current_settlement_proposals + .remove(&order_id) + .context("Could not find proposal for given order id")?; + self.send_current_settlement_proposals()?; Ok(()) } @@ -494,6 +541,20 @@ impl Handler for Actor { } } +#[async_trait] +impl Handler for Actor { + async fn handle(&mut self, msg: AcceptSettlement, _ctx: &mut Context) { + log_error!(self.handle_accept_settlement(msg.order_id)) + } +} + +#[async_trait] +impl Handler for Actor { + async fn handle(&mut self, msg: RejectSettlement, _ctx: &mut Context) { + log_error!(self.handle_reject_settlement(msg.order_id)) + } +} + #[async_trait] impl Handler for Actor { async fn handle(&mut self, msg: Commit, _ctx: &mut Context) { @@ -612,6 +673,14 @@ impl Message for Commit { type Result = (); } +impl Message for AcceptSettlement { + type Result = (); +} + +impl Message for RejectSettlement { + type Result = (); +} + // this signature is a bit different because we use `Address::attach_stream` impl Message for TakerStreamMessage { type Result = KeepRunning; diff --git a/daemon/src/model/cfd.rs b/daemon/src/model/cfd.rs index 2ab4b32..3f60c84 100644 --- a/daemon/src/model/cfd.rs +++ b/daemon/src/model/cfd.rs @@ -11,6 +11,7 @@ use rust_decimal::prelude::{FromPrimitive, ToPrimitive}; use rust_decimal::Decimal; use rust_decimal_macros::dec; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::fmt; use std::ops::{Neg, RangeInclusive}; use std::time::{Duration, SystemTime}; @@ -49,7 +50,7 @@ pub enum Origin { } /// Role in the Cfd -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum Role { Maker, Taker, @@ -350,6 +351,13 @@ pub struct SettlementProposal { pub maker: Amount, } +#[derive(Debug, Clone)] +#[allow(dead_code)] // Variants used by different binaries +pub enum SettlementProposals { + Incoming(HashMap), + Outgoing(HashMap), +} + /// Represents a cfd (including state) #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Cfd { diff --git a/daemon/src/routes_maker.rs b/daemon/src/routes_maker.rs index 018f807..c283231 100644 --- a/daemon/src/routes_maker.rs +++ b/daemon/src/routes_maker.rs @@ -1,8 +1,8 @@ use crate::auth::Authenticated; -use crate::model::cfd::{Cfd, Order, OrderId, Origin}; +use crate::model::cfd::{Cfd, Order, OrderId, Origin, Role, SettlementProposals}; use crate::model::{Usd, WalletInfo}; use crate::routes::EmbeddedFileExt; -use crate::to_sse_event::{CfdAction, CfdsWithCurrentPrice, ToSseEvent}; +use crate::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent}; use crate::{bitmex_price_feed, maker_cfd}; use anyhow::Result; use rocket::http::{ContentType, Header, Status}; @@ -24,12 +24,14 @@ pub async fn maker_feed( rx_order: &State>>, rx_wallet: &State>, rx_quote: &State>, + rx_settlements: &State>, _auth: Authenticated, ) -> EventStream![] { let mut rx_cfds = rx_cfds.inner().clone(); let mut rx_order = rx_order.inner().clone(); let mut rx_wallet = rx_wallet.inner().clone(); let mut rx_quote = rx_quote.inner().clone(); + let mut rx_settlements = rx_settlements.inner().clone(); EventStream! { let wallet_info = rx_wallet.borrow().clone(); @@ -41,11 +43,7 @@ pub async fn maker_feed( let quote = rx_quote.borrow().clone(); yield quote.to_sse_event(); - let cfds_with_price = CfdsWithCurrentPrice { - cfds: rx_cfds.borrow().clone(), - current_price: quote.for_maker(), - }; - yield cfds_with_price.to_sse_event(); + yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Maker).to_sse_event(); loop{ select! { @@ -58,20 +56,15 @@ pub async fn maker_feed( yield order.to_sse_event(); } Ok(()) = rx_cfds.changed() => { - let cfds_with_price = CfdsWithCurrentPrice { - cfds: rx_cfds.borrow().clone(), - current_price: quote.for_maker(), - }; - yield cfds_with_price.to_sse_event(); + yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Maker).to_sse_event(); + } + Ok(()) = rx_settlements.changed() => { + yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Maker).to_sse_event(); } Ok(()) = rx_quote.changed() => { let quote = rx_quote.borrow().clone(); yield quote.to_sse_event(); - let cfds_with_price = CfdsWithCurrentPrice { - cfds: rx_cfds.borrow().clone(), - current_price: quote.for_maker(), - }; - yield cfds_with_price.to_sse_event(); + yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Maker).to_sse_event(); } } } @@ -133,18 +126,30 @@ pub async fn post_cfd_action( _auth: Authenticated, ) -> Result, status::BadRequest> { match action { - CfdAction::Accept => { + CfdAction::AcceptOrder => { cfd_actor_address .do_send_async(maker_cfd::AcceptOrder { order_id: id }) .await .expect("actor to always be available"); } - CfdAction::Reject => { + CfdAction::RejectOrder => { cfd_actor_address .do_send_async(maker_cfd::RejectOrder { order_id: id }) .await .expect("actor to always be available"); } + CfdAction::AcceptSettlement => { + cfd_actor_address + .do_send_async(maker_cfd::AcceptSettlement { order_id: id }) + .await + .expect("actor to always be available"); + } + CfdAction::RejectSettlement => { + cfd_actor_address + .do_send_async(maker_cfd::RejectSettlement { order_id: id }) + .await + .expect("actor to always be available"); + } CfdAction::Commit => { cfd_actor_address .do_send_async(maker_cfd::Commit { order_id: id }) diff --git a/daemon/src/routes_taker.rs b/daemon/src/routes_taker.rs index f731e6b..2afdb40 100644 --- a/daemon/src/routes_taker.rs +++ b/daemon/src/routes_taker.rs @@ -1,7 +1,7 @@ -use crate::model::cfd::{calculate_buy_margin, Cfd, Order, OrderId}; +use crate::model::cfd::{calculate_buy_margin, Cfd, Order, OrderId, Role, SettlementProposals}; use crate::model::{Leverage, Usd, WalletInfo}; use crate::routes::EmbeddedFileExt; -use crate::to_sse_event::{CfdAction, CfdsWithCurrentPrice, ToSseEvent}; +use crate::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent}; use crate::{bitmex_price_feed, taker_cfd}; use bdk::bitcoin::Amount; use rocket::http::{ContentType, Status}; @@ -23,11 +23,13 @@ pub async fn feed( rx_order: &State>>, rx_wallet: &State>, rx_quote: &State>, + rx_settlements: &State>, ) -> EventStream![] { let mut rx_cfds = rx_cfds.inner().clone(); let mut rx_order = rx_order.inner().clone(); let mut rx_wallet = rx_wallet.inner().clone(); let mut rx_quote = rx_quote.inner().clone(); + let mut rx_settlements = rx_settlements.inner().clone(); EventStream! { let wallet_info = rx_wallet.borrow().clone(); @@ -39,11 +41,7 @@ pub async fn feed( let quote = rx_quote.borrow().clone(); yield quote.to_sse_event(); - let cfds_with_price = CfdsWithCurrentPrice { - cfds: rx_cfds.borrow().clone(), - current_price: quote.for_maker(), - }; - yield cfds_with_price.to_sse_event(); + yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Taker).to_sse_event(); loop{ select! { @@ -56,20 +54,15 @@ pub async fn feed( yield order.to_sse_event(); } Ok(()) = rx_cfds.changed() => { - let cfds_with_price = CfdsWithCurrentPrice { - cfds: rx_cfds.borrow().clone(), - current_price: quote.for_maker(), - }; - yield cfds_with_price.to_sse_event(); + yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Taker).to_sse_event(); + } + Ok(()) = rx_settlements.changed() => { + yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Taker).to_sse_event(); } Ok(()) = rx_quote.changed() => { let quote = rx_quote.borrow().clone(); yield quote.to_sse_event(); - let cfds_with_price = CfdsWithCurrentPrice { - cfds: rx_cfds.borrow().clone(), - current_price: quote.for_maker(), - }; - yield cfds_with_price.to_sse_event(); + yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Taker).to_sse_event(); } } } @@ -104,10 +97,12 @@ pub async fn post_cfd_action( quote_updates: &State>, ) -> Result, status::BadRequest> { match action { - CfdAction::Accept | CfdAction::Reject => { + CfdAction::AcceptOrder + | CfdAction::RejectOrder + | CfdAction::AcceptSettlement + | CfdAction::RejectSettlement => { return Err(status::BadRequest(None)); } - CfdAction::Commit => { cfd_actor_address .do_send_async(taker_cfd::Commit { order_id: id }) diff --git a/daemon/src/taker.rs b/daemon/src/taker.rs index 076a9f8..e2ac692 100644 --- a/daemon/src/taker.rs +++ b/daemon/src/taker.rs @@ -1,4 +1,5 @@ use crate::db::load_all_cfds; +use crate::model::cfd::SettlementProposals; use crate::model::WalletInfo; use crate::wallet::Wallet; use anyhow::{Context, Result}; @@ -10,6 +11,7 @@ use model::cfd::Order; use rocket::fairing::AdHoc; use rocket_db_pools::Database; use seed::Seed; +use std::collections::HashMap; use std::net::SocketAddr; use std::path::PathBuf; use std::thread::sleep; @@ -106,6 +108,8 @@ async fn main() -> Result<()> { let (order_feed_sender, order_feed_receiver) = watch::channel::>(None); let (wallet_feed_sender, wallet_feed_receiver) = watch::channel::(wallet_info); + let (settlement_feed_sender, settlement_feed_receiver) = + watch::channel::(SettlementProposals::Outgoing(HashMap::new())); let (read, write) = loop { let socket = tokio::net::TcpSocket::new_v4()?; @@ -130,6 +134,7 @@ async fn main() -> Result<()> { rocket::custom(figment) .manage(order_feed_receiver) .manage(wallet_feed_receiver) + .manage(settlement_feed_receiver) .manage(quote_updates) .attach(Db::init()) .attach(AdHoc::try_on_ignite( @@ -177,6 +182,7 @@ async fn main() -> Result<()> { schnorrsig::PublicKey::from_keypair(SECP256K1, &oracle), cfd_feed_sender, order_feed_sender, + settlement_feed_sender, send_to_maker, monitor_actor_address.clone(), oracle_actor_address, diff --git a/daemon/src/taker_cfd.rs b/daemon/src/taker_cfd.rs index f132c03..ad92ca7 100644 --- a/daemon/src/taker_cfd.rs +++ b/daemon/src/taker_cfd.rs @@ -5,6 +5,7 @@ use crate::db::{ }; use crate::model::cfd::{ Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, Dlc, Order, OrderId, Origin, Role, + SettlementProposal, SettlementProposals, }; use crate::model::Usd; use crate::monitor::{self, MonitorParams}; @@ -16,6 +17,7 @@ use async_trait::async_trait; use bdk::bitcoin::secp256k1::schnorrsig; use futures::channel::mpsc; use futures::{future, SinkExt}; +use std::collections::HashMap; use std::time::SystemTime; use tokio::sync::watch; use xtra::prelude::*; @@ -57,11 +59,13 @@ pub struct Actor { oracle_pk: schnorrsig::PublicKey, cfd_feed_actor_inbox: watch::Sender>, order_feed_actor_inbox: watch::Sender>, + settlements_feed_sender: watch::Sender, send_to_maker: Address>, monitor_actor: Address>, setup_state: SetupState, latest_announcement: Option, _oracle_actor: Address>>, + current_settlement_proposals: HashMap, } impl Actor { @@ -72,6 +76,7 @@ impl Actor { oracle_pk: schnorrsig::PublicKey, cfd_feed_actor_inbox: watch::Sender>, order_feed_actor_inbox: watch::Sender>, + settlements_feed_sender: watch::Sender, send_to_maker: Address>, monitor_actor: Address>, oracle_actor: Address>>, @@ -82,14 +87,24 @@ impl Actor { oracle_pk, cfd_feed_actor_inbox, order_feed_actor_inbox, + settlements_feed_sender, send_to_maker, monitor_actor, setup_state: SetupState::None, latest_announcement: None, _oracle_actor: oracle_actor, + current_settlement_proposals: HashMap::new(), } } + fn send_current_settlement_proposals(&self) -> Result<()> { + Ok(self + .settlements_feed_sender + .send(SettlementProposals::Outgoing( + self.current_settlement_proposals.clone(), + ))?) + } + async fn handle_take_offer(&mut self, order_id: OrderId, quantity: Usd) -> Result<()> { let mut conn = self.db.acquire().await?; @@ -126,14 +141,28 @@ impl Actor { let mut conn = self.db.acquire().await?; let cfd = load_cfd_by_order_id(order_id, &mut conn).await?; - let settlement = cfd.calculate_settlement(current_price)?; + let proposal = cfd.calculate_settlement(current_price)?; + + if self + .current_settlement_proposals + .contains_key(&proposal.order_id) + { + anyhow::bail!( + "Settlement proposal for order id {} already present", + order_id + ) + } + + self.current_settlement_proposals + .insert(proposal.order_id, proposal.clone()); + self.send_current_settlement_proposals()?; self.send_to_maker .do_send_async(wire::TakerToMaker::ProposeSettlement { - order_id: settlement.order_id, - timestamp: settlement.timestamp, - taker: settlement.taker, - maker: settlement.maker, + order_id: proposal.order_id, + timestamp: proposal.timestamp, + taker: proposal.taker, + maker: proposal.maker, }) .await?; Ok(()) diff --git a/daemon/src/to_sse_event.rs b/daemon/src/to_sse_event.rs index d3484c8..0bff7ee 100644 --- a/daemon/src/to_sse_event.rs +++ b/daemon/src/to_sse_event.rs @@ -1,4 +1,4 @@ -use crate::model::cfd::{OrderId, Role}; +use crate::model::cfd::{OrderId, Role, SettlementProposals}; use crate::model::{Leverage, Position, TradingPair, Usd}; use crate::{bitmex_price_feed, model}; use bdk::bitcoin::{Amount, SignedAmount}; @@ -7,6 +7,7 @@ use rocket::response::stream::Event; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::sync::watch; #[derive(Debug, Clone, Serialize)] pub struct Cfd { @@ -33,12 +34,14 @@ pub struct Cfd { } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "camelCase")] pub enum CfdAction { - Accept, - Reject, + AcceptOrder, + RejectOrder, Commit, Settle, + AcceptSettlement, + RejectSettlement, } impl<'v> FromParam<'v> for CfdAction { @@ -61,6 +64,8 @@ pub enum CfdState { Open, PendingCommit, OpenCommitted, + IncomingSettlementProposal, + OutgoingSettlementProposal, MustRefund, Refunded, SetupFailed, @@ -89,13 +94,76 @@ pub trait ToSseEvent { fn to_sse_event(&self) -> Event; } -/// Intermediate struct to able to piggy-back current price along with cfds -pub struct CfdsWithCurrentPrice { +/// Intermediate struct to able to piggy-back additional information along with +/// cfds, so we can avoid a 1:1 mapping between the states in the model and seen +/// by UI +pub struct CfdsWithAuxData { pub cfds: Vec, pub current_price: Usd, + pub settlement_proposals: SettlementProposals, } -impl ToSseEvent for CfdsWithCurrentPrice { +enum SettlementProposalStatus { + Incoming, + Outgoing, + None, +} + +impl CfdsWithAuxData { + pub fn new( + rx_cfds: &watch::Receiver>, + rx_quote: &watch::Receiver, + rx_settlement: &watch::Receiver, + role: Role, + ) -> Self { + let quote = rx_quote.borrow().clone(); + let current_price = match role { + Role::Maker => quote.for_maker(), + Role::Taker => quote.for_taker(), + }; + + let settlement_proposals = rx_settlement.borrow().clone(); + + // Test whether the correct settlement proposals were sent + match settlement_proposals { + SettlementProposals::Incoming(_) => { + if role == Role::Taker { + panic!("Taker should never receive incoming settlement proposals"); + } + } + SettlementProposals::Outgoing(_) => { + if role == Role::Maker { + panic!("Maker should never receive outgoing settlement proposals"); + } + } + } + + CfdsWithAuxData { + cfds: rx_cfds.borrow().clone(), + current_price, + settlement_proposals, + } + } + + /// Check whether given CFD has any active settlement proposals + fn settlement_proposal_status(&self, cfd: &model::cfd::Cfd) -> SettlementProposalStatus { + match &self.settlement_proposals { + SettlementProposals::Incoming(proposals) => { + if proposals.contains_key(&cfd.order.id) { + return SettlementProposalStatus::Incoming; + } + } + SettlementProposals::Outgoing(proposals) => { + if proposals.contains_key(&cfd.order.id) { + return SettlementProposalStatus::Outgoing; + } + } + } + SettlementProposalStatus::None + } +} + +impl ToSseEvent for CfdsWithAuxData { // TODO: This conversion can fail, we might want to change the API fn to_sse_event(&self) -> Event { let current_price = self.current_price; @@ -113,6 +181,8 @@ impl ToSseEvent for CfdsWithCurrentPrice { (SignedAmount::ZERO, Decimal::ZERO.into()) }); + let state = to_cfd_state(&cfd.state, self.settlement_proposal_status(cfd)); + Cfd { order_id: cfd.order.id, initial_price: cfd.order.price, @@ -123,8 +193,8 @@ impl ToSseEvent for CfdsWithCurrentPrice { quantity_usd: cfd.quantity_usd, profit_btc, profit_in_percent: profit_in_percent.to_string(), - state: cfd.state.clone().into(), - actions: actions_for_state(cfd.state.clone(), cfd.role()), + state: state.clone(), + actions: available_actions(state, cfd.role()), state_transition_timestamp: cfd .state .get_transition_timestamp() @@ -186,9 +256,14 @@ impl ToSseEvent for model::WalletInfo { } } -impl From for CfdState { - fn from(cfd_state: model::cfd::CfdState) -> Self { - match cfd_state { +fn to_cfd_state( + cfd_state: &model::cfd::CfdState, + proposal_status: SettlementProposalStatus, +) -> CfdState { + match proposal_status { + SettlementProposalStatus::Incoming => CfdState::IncomingSettlementProposal, + SettlementProposalStatus::Outgoing => CfdState::OutgoingSettlementProposal, + SettlementProposalStatus::None => match cfd_state { model::cfd::CfdState::OutgoingOrderRequest { .. } => CfdState::OutgoingOrderRequest, model::cfd::CfdState::IncomingOrderRequest { .. } => CfdState::IncomingOrderRequest, model::cfd::CfdState::Accepted { .. } => CfdState::Accepted, @@ -201,7 +276,7 @@ impl From for CfdState { model::cfd::CfdState::Refunded { .. } => CfdState::Refunded, model::cfd::CfdState::SetupFailed { .. } => CfdState::SetupFailed, model::cfd::CfdState::PendingCommit { .. } => CfdState::PendingCommit, - } + }, } } @@ -230,15 +305,23 @@ fn into_unix_secs(time: SystemTime) -> u64 { .as_secs() } -fn actions_for_state(state: model::cfd::CfdState, role: Role) -> Vec { +fn available_actions(state: CfdState, role: Role) -> Vec { match (state, role) { - (model::cfd::CfdState::IncomingOrderRequest { .. }, Role::Maker) => { - vec![CfdAction::Accept, CfdAction::Reject] + (CfdState::IncomingOrderRequest { .. }, Role::Maker) => { + vec![CfdAction::AcceptOrder, CfdAction::RejectOrder] + } + (CfdState::IncomingSettlementProposal { .. }, Role::Maker) => { + vec![CfdAction::AcceptSettlement, CfdAction::RejectSettlement] + } + // If there is an outgoing settlement proposal already, user can't + // initiate new one + (CfdState::OutgoingSettlementProposal { .. }, Role::Maker) => { + vec![CfdAction::Commit] } - (model::cfd::CfdState::Open { .. }, Role::Taker) => { + (CfdState::Open { .. }, Role::Taker) => { vec![CfdAction::Commit, CfdAction::Settle] } - (model::cfd::CfdState::Open { .. }, Role::Maker) => vec![CfdAction::Commit], + (CfdState::Open { .. }, Role::Maker) => vec![CfdAction::Commit], _ => vec![], } } diff --git a/frontend/src/MakerApp.tsx b/frontend/src/MakerApp.tsx index b7e9383..bf8bb61 100644 --- a/frontend/src/MakerApp.tsx +++ b/frontend/src/MakerApp.tsx @@ -64,7 +64,10 @@ export default function App() { }, }); - const acceptOrReject = cfds.filter((value) => value.state.getGroup() === StateGroupKey.ACCEPT_OR_REJECT); + const acceptOrRejectOrder = cfds.filter((value) => value.state.getGroup() === StateGroupKey.ACCEPT_OR_REJECT_ORDER); + const acceptOrRejectSettlement = cfds.filter((value) => + value.state.getGroup() === StateGroupKey.ACCEPT_OR_REJECT_SETTLEMENT + ); const opening = cfds.filter((value) => value.state.getGroup() === StateGroupKey.OPENING); const open = cfds.filter((value) => value.state.getGroup() === StateGroupKey.OPEN); const closed = cfds.filter((value) => value.state.getGroup() === StateGroupKey.CLOSED); @@ -156,7 +159,8 @@ export default function App() { Open [{open.length}] - Accept / Reject [{acceptOrReject.length}] + Accept / Reject Order [{acceptOrRejectOrder.length}] + Accept / Reject Settlement [{acceptOrRejectSettlement.length}] Opening [{opening.length}] Closed [{closed.length}] @@ -166,7 +170,10 @@ export default function App() { - + + + + diff --git a/frontend/src/components/Types.tsx b/frontend/src/components/Types.tsx index 4b8fc1f..be7e548 100644 --- a/frontend/src/components/Types.tsx +++ b/frontend/src/components/Types.tsx @@ -56,9 +56,9 @@ export class State { public getLabel(): string { switch (this.key) { case StateKey.INCOMING_ORDER_REQUEST: - return "Take Requested"; + return "Order Requested"; case StateKey.OUTGOING_ORDER_REQUEST: - return "Take Requested"; + return "Order Requested"; case StateKey.ACCEPTED: return "Accepted"; case StateKey.REJECTED: @@ -73,6 +73,10 @@ export class State { return "Pending Commit"; case StateKey.OPEN_COMMITTED: return "Open (commit-tx published)"; + case StateKey.INCOMING_SETTLEMENT_PROPOSAL: + return "Settlement Proposed"; + case StateKey.OUTGOING_SETTLEMENT_PROPOSAL: + return "Settlement Proposed"; case StateKey.MUST_REFUND: return "Refunding"; case StateKey.REFUNDED: @@ -103,6 +107,8 @@ export class State { case StateKey.OUTGOING_ORDER_REQUEST: case StateKey.INCOMING_ORDER_REQUEST: + case StateKey.OUTGOING_SETTLEMENT_PROPOSAL: + case StateKey.INCOMING_SETTLEMENT_PROPOSAL: case StateKey.CONTRACT_SETUP: case StateKey.PENDING_OPEN: case StateKey.REFUNDED: @@ -114,7 +120,7 @@ export class State { public getGroup(): StateGroupKey { switch (this.key) { case StateKey.INCOMING_ORDER_REQUEST: - return StateGroupKey.ACCEPT_OR_REJECT; + return StateGroupKey.ACCEPT_OR_REJECT_ORDER; case StateKey.OUTGOING_ORDER_REQUEST: case StateKey.ACCEPTED: @@ -126,8 +132,12 @@ export class State { case StateKey.PENDING_COMMIT: case StateKey.OPEN_COMMITTED: case StateKey.MUST_REFUND: + case StateKey.OUTGOING_SETTLEMENT_PROPOSAL: return StateGroupKey.OPEN; + case StateKey.INCOMING_SETTLEMENT_PROPOSAL: + return StateGroupKey.ACCEPT_OR_REJECT_SETTLEMENT; + case StateKey.REJECTED: case StateKey.REFUNDED: case StateKey.SETUP_FAILED: @@ -137,10 +147,12 @@ export class State { } export enum Action { - ACCEPT = "accept", - REJECT = "reject", + ACCEPT_ORDER = "acceptOrder", + REJECT_ORDER = "rejectOrder", COMMIT = "commit", SETTLE = "settle", + ACCEPT_SETTLEMENT = "acceptSettlement", + REJECT_SETTLEMENT = "rejectSettlement", } const enum StateKey { @@ -153,6 +165,8 @@ const enum StateKey { OPEN = "Open", PENDING_COMMIT = "PendingCommit", OPEN_COMMITTED = "OpenCommitted", + OUTGOING_SETTLEMENT_PROPOSAL = "OutgoingSettlementProposal", + INCOMING_SETTLEMENT_PROPOSAL = "IncomingSettlementProposal", MUST_REFUND = "MustRefund", REFUNDED = "Refunded", SETUP_FAILED = "SetupFailed", @@ -161,9 +175,10 @@ const enum StateKey { export enum StateGroupKey { /// A CFD which is still being set up (not on chain yet) OPENING = "Opening", - ACCEPT_OR_REJECT = "Accept / Reject", + ACCEPT_OR_REJECT_ORDER = "Accept / Reject Order", /// A CFD that is an ongoing open position (on chain) OPEN = "Open", + ACCEPT_OR_REJECT_SETTLEMENT = "Accept / Reject Settlement", /// A CFD that has been successfully or not-successfully terminated CLOSED = "Closed", } diff --git a/frontend/src/components/cfdtables/CfdTable.tsx b/frontend/src/components/cfdtables/CfdTable.tsx index 648b12e..a024b03 100644 --- a/frontend/src/components/cfdtables/CfdTable.tsx +++ b/frontend/src/components/cfdtables/CfdTable.tsx @@ -187,27 +187,35 @@ export function CfdTable( function iconForAction(action: Action): any { switch (action) { - case Action.ACCEPT: + case Action.ACCEPT_ORDER: return ; - case Action.REJECT: + case Action.REJECT_ORDER: return ; case Action.COMMIT: return ; case Action.SETTLE: return ; + case Action.ACCEPT_SETTLEMENT: + return ; + case Action.REJECT_SETTLEMENT: + return ; } } function colorSchemaForAction(action: Action): string { switch (action) { - case Action.ACCEPT: + case Action.ACCEPT_ORDER: return "green"; - case Action.REJECT: + case Action.REJECT_ORDER: return "red"; case Action.COMMIT: return "red"; case Action.SETTLE: return "green"; + case Action.ACCEPT_SETTLEMENT: + return "green"; + case Action.REJECT_SETTLEMENT: + return "red"; } }