diff --git a/cfd_protocol/src/protocol.rs b/cfd_protocol/src/protocol.rs index 6c8a7fc..09293fa 100644 --- a/cfd_protocol/src/protocol.rs +++ b/cfd_protocol/src/protocol.rs @@ -409,6 +409,18 @@ pub fn generate_payouts( } impl Payout { + pub fn digits(&self) -> &interval::Digits { + &self.digits + } + + pub fn maker_amount(&self) -> &Amount { + &self.maker_amount + } + + pub fn taker_amount(&self) -> &Amount { + &self.taker_amount + } + fn into_txouts(self, maker_address: &Address, taker_address: &Address) -> Vec { let txouts = [ (self.maker_amount, maker_address), diff --git a/daemon/src/maker_cfd.rs b/daemon/src/maker_cfd.rs index 67a2024..a2ec805 100644 --- a/daemon/src/maker_cfd.rs +++ b/daemon/src/maker_cfd.rs @@ -5,8 +5,8 @@ use crate::db::{ use crate::maker_inc_connections::TakerCommand; use crate::model::cfd::{ Attestation, Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, Dlc, Order, OrderId, Origin, - Role, RollOverProposal, SettlementKind, SettlementProposal, UpdateCfdProposal, - UpdateCfdProposals, + Role, RollOverProposal, SettlementKind, SettlementProposal, TimestampedTransaction, + UpdateCfdProposal, UpdateCfdProposals, }; use crate::model::{TakerId, Usd}; use crate::monitor::MonitorParams; @@ -298,23 +298,34 @@ impl Actor { let mut conn = self.db.acquire().await?; - let cfd = load_cfd_by_order_id(order_id, &mut conn).await?; + let mut cfd = load_cfd_by_order_id(order_id, &mut conn).await?; let dlc = cfd.open_dlc().context("CFD was in wrong state")?; let (tx, sig_maker) = dlc.close_transaction(proposal)?; + + cfd.handle(CfdStateChangeEvent::ProposalSigned( + TimestampedTransaction::new(tx.clone()), + ))?; + insert_new_cfd_state_by_order_id(cfd.order.id, cfd.state.clone(), &mut conn).await?; + let spend_tx = dlc.finalize_spend_transaction((tx, sig_maker), sig_taker)?; - self.wallet - .try_broadcast_transaction(spend_tx) + let txid = self + .wallet + .try_broadcast_transaction(spend_tx.clone()) .await .context("Broadcasting spend transaction")?; + tracing::info!("Close transaction published with txid {}", txid); + + cfd.handle(CfdStateChangeEvent::CloseSent(TimestampedTransaction::new( + spend_tx, + )))?; + insert_new_cfd_state_by_order_id(cfd.order.id, cfd.state, &mut conn).await?; self.current_agreed_proposals .remove(&order_id) .context("remove accepted proposal after signing")?; - // TODO: Monitor for the transaction - Ok(()) } @@ -461,6 +472,7 @@ impl Actor { }, dlc: dlc.clone(), attestation: None, + collaborative_close: None, }, &mut conn, ) diff --git a/daemon/src/model/cfd.rs b/daemon/src/model/cfd.rs index 5772d4b..bbb751c 100644 --- a/daemon/src/model/cfd.rs +++ b/daemon/src/model/cfd.rs @@ -1,5 +1,5 @@ use crate::model::{BitMexPriceEventId, Leverage, Percent, Position, TakerId, TradingPair, Usd}; -use crate::{monitor, oracle}; +use crate::{monitor, oracle, payout_curve}; use anyhow::{bail, Context, Result}; use bdk::bitcoin::secp256k1::{SecretKey, Signature}; use bdk::bitcoin::{Address, Amount, PublicKey, Script, SignedAmount, Transaction, Txid}; @@ -216,6 +216,7 @@ pub enum CfdState { common: CfdStateCommon, dlc: Dlc, attestation: Option, + collaborative_close: Option, }, /// The commit transaction was published but it not final yet @@ -251,6 +252,17 @@ pub enum CfdState { attestation: Attestation, }, + /// The collaborative close transaction was published but is not final yet. + /// + /// This state applies to taker and maker. + /// This state is needed, because otherwise the user does not get any feedback. + PendingClose { + common: CfdStateCommon, + dlc: Dlc, + attestation: Option, + collaborative_close: TimestampedTransaction, + }, + /// The position was closed collaboratively or non-collaboratively /// /// This state applies to taker and maker. @@ -260,7 +272,8 @@ pub enum CfdState { /// commit + cet). Closed { common: CfdStateCommon, - attestation: Attestation, + // TODO: Use an enum of either Attestation or CollaborativeSettlement + attestation: Option, }, // TODO: Can be extended with CetStatus @@ -391,6 +404,7 @@ impl CfdState { CfdState::SetupFailed { common, .. } => common, CfdState::PendingCommit { common, .. } => common, CfdState::PendingCet { common, .. } => common, + CfdState::PendingClose { common, .. } => common, CfdState::Closed { common, .. } => common, }; @@ -400,6 +414,20 @@ impl CfdState { pub fn get_transition_timestamp(&self) -> SystemTime { self.get_common().transition_timestamp } + + pub fn get_collaborative_close(&self) -> Option { + match self { + CfdState::Open { + collaborative_close, + .. + } => collaborative_close.clone(), + CfdState::PendingClose { + collaborative_close, + .. + } => Some(collaborative_close.clone()), + _ => None, + } + } } impl fmt::Display for CfdState { @@ -444,6 +472,9 @@ impl fmt::Display for CfdState { CfdState::PendingCet { .. } => { write!(f, "Pending CET") } + CfdState::PendingClose { .. } => { + write!(f, "Pending Close") + } CfdState::Closed { .. } => { write!(f, "Closed") } @@ -531,7 +562,7 @@ impl Cfd { pub fn profit(&self, current_price: Usd) -> Result<(SignedAmount, Percent)> { // TODO: We should use the payout curve here and not just the current price! - + // TODO: Use the collab settlement if there was one let current_price = if let Some(attestation) = self.attestation() { attestation.price() } else { @@ -550,14 +581,22 @@ impl Cfd { } #[allow(dead_code)] // Not used by all binaries. - pub fn calculate_settlement(&self, _current_price: Usd) -> Result { - // TODO: Calculate values for taker and maker - // For the time being, assume that everybody loses :) + pub fn calculate_settlement(&self, current_price: Usd) -> Result { + let payout_curve = + payout_curve::calculate(self.order.price, self.quantity_usd, self.order.leverage)?; + + let current_price = current_price.try_into_u64()?; + + let payout = payout_curve + .iter() + .find(|&x| x.digits().range().contains(¤t_price)) + .context("find current price on the payout curve")?; + let settlement = SettlementProposal { order_id: self.order.id, timestamp: SystemTime::now(), - taker: Amount::ZERO, - maker: Amount::ZERO, + taker: *payout.taker_amount(), + maker: *payout.maker_amount(), }; Ok(settlement) @@ -626,9 +665,13 @@ impl Cfd { }, dlc, attestation: None, + collaborative_close: None, } } else if let Open { - dlc, attestation, .. + dlc, + attestation, + collaborative_close, + .. } = self.state.clone() { CfdState::Open { @@ -637,6 +680,7 @@ impl Cfd { }, dlc, attestation, + collaborative_close, } } else { bail!( @@ -679,6 +723,12 @@ impl Cfd { }, } } + monitor::Event::CloseFinality(_) => CfdState::Closed { + common: CfdStateCommon { + transition_timestamp: SystemTime::now(), + }, + attestation: None + }, monitor::Event::CetTimelockExpired(_) => match self.state.clone() { CfdState::OpenCommitted { dlc, @@ -769,7 +819,7 @@ impl Cfd { common: CfdStateCommon { transition_timestamp: SystemTime::now(), }, - attestation, + attestation: Some(attestation), } } monitor::Event::RevokedTransactionFound(_) => { @@ -777,19 +827,21 @@ impl Cfd { } }, CfdStateChangeEvent::CommitTxSent => { - let (dlc, attestation) = if let PendingOpen { - dlc, attestation, .. - } - | Open { - dlc, attestation, .. - } = self.state.clone() - { - (dlc, attestation) - } else { - bail!( - "Cannot transition to PendingCommit because of unexpected state {}", - self.state - ) + let (dlc, attestation ) = match self.state.clone() { + PendingOpen { + dlc, attestation, .. + } => (dlc, attestation), + Open { + dlc, + attestation, + .. + } => (dlc, attestation), + _ => { + bail!( + "Cannot transition to PendingCommit because of unexpected state {}", + self.state + ) + } }; PendingCommit { @@ -814,8 +866,12 @@ impl Cfd { }, dlc, attestation: Some(attestation), + collaborative_close: None, }, - CfdState::PendingCommit { dlc, .. } => CfdState::PendingCommit { + CfdState::PendingCommit { + dlc, + .. + } => CfdState::PendingCommit { common: CfdStateCommon { transition_timestamp: SystemTime::now(), }, @@ -862,6 +918,44 @@ impl Cfd { dlc, attestation, } + + }, + CfdStateChangeEvent::ProposalSigned(collaborative_close) => match self.state.clone() { + CfdState::Open { + common, + dlc, + attestation, + .. + } => CfdState::Open { + common, + dlc, + attestation, + collaborative_close: Some(collaborative_close), + }, + _ => bail!( + "Cannot add proposed settlement details to state because of unexpected state {}", + self.state + ), + }, + CfdStateChangeEvent::CloseSent(collaborative_close) => match self.state.clone() { + CfdState::Open { + common, + dlc, + attestation, + collaborative_close : Some(_), + } => CfdState::PendingClose { + common, + dlc, + attestation, + collaborative_close, + }, + CfdState::Open { + collaborative_close : None, + .. + } => bail!("Cannot transition to PendingClose because Open state did not record a settlement proposal beforehand"), + _ => bail!( + "Cannot transition to PendingClose because of unexpected state {}", + self.state) } }; @@ -1058,6 +1152,7 @@ impl Cfd { | CfdState::Accepted { .. } | CfdState::Rejected { .. } | CfdState::ContractSetup { .. } + | CfdState::PendingClose { .. } | CfdState::Closed { .. } | CfdState::MustRefund { .. } | CfdState::Refunded { .. } @@ -1084,7 +1179,10 @@ impl Cfd { .. } | CfdState::PendingCet { attestation, .. } - | CfdState::Closed { attestation, .. } => Some(attestation), + | CfdState::Closed { + attestation: Some(attestation), + .. + } => Some(attestation), CfdState::OutgoingOrderRequest { .. } | CfdState::IncomingOrderRequest { .. } @@ -1094,6 +1192,8 @@ impl Cfd { | CfdState::PendingOpen { .. } | CfdState::Open { .. } | CfdState::PendingCommit { .. } + | CfdState::PendingClose { .. } + | CfdState::Closed { .. } | CfdState::OpenCommitted { .. } | CfdState::MustRefund { .. } | CfdState::Refunded { .. } @@ -1109,6 +1209,7 @@ pub struct NotReadyYet { } #[derive(Debug, Clone)] +#[allow(dead_code)] // Not all variants are used by all binaries. pub enum CfdStateChangeEvent { // TODO: group other events by actors into enums and add them here so we can bundle all // transitions into cfd.transition_to(...) @@ -1116,6 +1217,10 @@ pub enum CfdStateChangeEvent { CommitTxSent, OracleAttestation(Attestation), CetSent, + /// Settlement proposal was signed by the taker and sent to the maker + ProposalSigned(TimestampedTransaction), + /// Maker signed and finalized the close transaction with both signatures + CloseSent(TimestampedTransaction), } /// Returns the Profit/Loss (P/L) as Bitcoin. Losses are capped by the provided margin @@ -1577,3 +1682,22 @@ pub struct RevokedCommit { pub txid: Txid, pub script_pubkey: Script, } + +/// Used when transactions (e.g. collaborative close) are recorded as a part of +/// CfdState in the cases when we can't solely rely on state transition +/// timestamp as it could have occured for different reasons (like a new +/// attestation in Open state) +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct TimestampedTransaction { + pub tx: Transaction, + pub timestamp: SystemTime, +} + +impl TimestampedTransaction { + pub fn new(tx: Transaction) -> Self { + Self { + tx, + timestamp: SystemTime::now(), + } + } +} diff --git a/daemon/src/monitor.rs b/daemon/src/monitor.rs index 09ffe23..50d600b 100644 --- a/daemon/src/monitor.rs +++ b/daemon/src/monitor.rs @@ -1,4 +1,4 @@ -use crate::model::cfd::{CetStatus, Cfd, CfdState, Dlc, OrderId}; +use crate::model::cfd::{CetStatus, Cfd, CfdState, Dlc, OrderId, TimestampedTransaction}; use crate::model::BitMexPriceEventId; use crate::oracle::Attestation; use crate::{log_error, model, oracle}; @@ -89,6 +89,13 @@ where actor.monitor_commit_cet_timelock(¶ms, cfd.order.id); actor.monitor_commit_refund_timelock(¶ms, cfd.order.id); actor.monitor_refund_finality(¶ms,cfd.order.id); + + if let Some(TimestampedTransaction { tx, ..} + ) = cfd.state.get_collaborative_close() { + let close_params = (tx.txid(), + tx.output.first().expect("have output").script_pubkey.clone()); + actor.monitor_close_finality(close_params,cfd.order.id); + } } CfdState::OpenCommitted { dlc, cet_status, .. } => { let params = MonitorParams::from_dlc_and_timelocks(dlc.clone(), cfd.refund_timelock_in_blocks()); @@ -124,6 +131,12 @@ where actor.monitor_cet_finality(map_cets(dlc.cets), attestation.into(), cfd.order.id)?; actor.monitor_commit_refund_timelock(¶ms, cfd.order.id); actor.monitor_refund_finality(¶ms,cfd.order.id); + } + CfdState::PendingClose { collaborative_close, .. } => { + let transaction = collaborative_close.tx; + let close_params = (transaction.txid(), + transaction.output.first().expect("have output").script_pubkey.clone()); + actor.monitor_close_finality(close_params,cfd.order.id); } CfdState::MustRefund { dlc, .. } => { let params = MonitorParams::from_dlc_and_timelocks(dlc.clone(), cfd.refund_timelock_in_blocks()); @@ -179,6 +192,13 @@ where .push((ScriptStatus::finality(), Event::CommitFinality(order_id))); } + fn monitor_close_finality(&mut self, close_params: (Txid, Script), order_id: OrderId) { + self.awaiting_status + .entry(close_params) + .or_default() + .push((ScriptStatus::finality(), Event::CloseFinality(order_id))); + } + fn monitor_commit_cet_timelock(&mut self, params: &MonitorParams, order_id: OrderId) { self.awaiting_status .entry((params.commit.0, params.commit.1.script_pubkey())) @@ -516,6 +536,7 @@ impl xtra::Message for StartMonitoring { pub enum Event { LockFinality(OrderId), CommitFinality(OrderId), + CloseFinality(OrderId), CetTimelockExpired(OrderId), CetFinality(OrderId), RefundTimelockExpired(OrderId), @@ -528,6 +549,7 @@ impl Event { let order_id = match self { Event::LockFinality(order_id) => order_id, Event::CommitFinality(order_id) => order_id, + Event::CloseFinality(order_id) => order_id, Event::CetTimelockExpired(order_id) => order_id, Event::RefundTimelockExpired(order_id) => order_id, Event::RefundFinality(order_id) => order_id, diff --git a/daemon/src/oracle.rs b/daemon/src/oracle.rs index 125bc24..770b3eb 100644 --- a/daemon/src/oracle.rs +++ b/daemon/src/oracle.rs @@ -78,7 +78,8 @@ impl Actor { | CfdState::Open { .. } | CfdState::PendingCommit { .. } | CfdState::OpenCommitted { .. } - | CfdState::PendingCet { .. } => { + | CfdState::PendingCet { .. } => + { pending_attestations.insert(cfd.order.oracle_event_id); } @@ -88,6 +89,7 @@ impl Actor { | CfdState::Accepted { .. } | CfdState::Rejected { .. } | CfdState::ContractSetup { .. } + | CfdState::PendingClose { .. } // Final states | CfdState::Closed { .. } diff --git a/daemon/src/taker_cfd.rs b/daemon/src/taker_cfd.rs index 5d42921..05fe4d7 100644 --- a/daemon/src/taker_cfd.rs +++ b/daemon/src/taker_cfd.rs @@ -4,8 +4,8 @@ use crate::db::{ }; use crate::model::cfd::{ Attestation, Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, Dlc, Order, OrderId, Origin, - Role, RollOverProposal, SettlementKind, SettlementProposal, UpdateCfdProposal, - UpdateCfdProposals, + Role, RollOverProposal, SettlementKind, SettlementProposal, TimestampedTransaction, + UpdateCfdProposal, UpdateCfdProposals, }; use crate::model::{BitMexPriceEventId, Usd}; use crate::monitor::{self, MonitorParams}; @@ -356,11 +356,11 @@ impl Actor { let mut conn = self.db.acquire().await?; - let cfd = load_cfd_by_order_id(order_id, &mut conn).await?; + let mut cfd = load_cfd_by_order_id(order_id, &mut conn).await?; let dlc = cfd.open_dlc().context("CFD was in wrong state")?; let proposal = self.get_settlement_proposal(order_id)?; - let (_tx, sig_taker) = dlc.close_transaction(proposal)?; + let (tx, sig_taker) = dlc.close_transaction(proposal)?; self.send_to_maker .do_send_async(wire::TakerToMaker::InitiateSettlement { @@ -369,7 +369,10 @@ impl Actor { }) .await?; - // TODO: Monitor for the transaction + cfd.handle(CfdStateChangeEvent::ProposalSigned( + TimestampedTransaction::new(tx), + ))?; + insert_new_cfd_state_by_order_id(cfd.order.id, cfd.state, &mut conn).await?; self.remove_pending_proposal(&order_id)?; @@ -547,6 +550,7 @@ impl Actor { }, dlc: dlc.clone(), attestation: None, + collaborative_close: None, }, &mut conn, ) diff --git a/daemon/src/to_sse_event.rs b/daemon/src/to_sse_event.rs index 3429065..a145655 100644 --- a/daemon/src/to_sse_event.rs +++ b/daemon/src/to_sse_event.rs @@ -133,6 +133,7 @@ pub enum CfdState { Open, PendingCommit, PendingCet, + PendingClose, OpenCommitted, IncomingSettlementProposal, OutgoingSettlementProposal, @@ -329,6 +330,7 @@ fn to_cfd_state( model::cfd::CfdState::SetupFailed { .. } => CfdState::SetupFailed, model::cfd::CfdState::PendingCommit { .. } => CfdState::PendingCommit, model::cfd::CfdState::PendingCet { .. } => CfdState::PendingCet, + model::cfd::CfdState::PendingClose { .. } => CfdState::PendingClose, model::cfd::CfdState::Closed { .. } => CfdState::Closed, }, Some(UpdateCfdProposal::RollOverProposal { @@ -386,10 +388,19 @@ fn to_cfd_details(state: model::cfd::CfdState, role: Role, network: Network) -> ], Some(attestation.payout()), ), - Closed { attestation, .. } => ( + Closed { + attestation: Some(attestation), + .. + } => ( vec![tx_ub.cet(attestation.txid())], Some(attestation.payout()), ), + Closed { + attestation: None, .. + } => { + // TODO: Provide CfdDetails about collaborative settlement + (vec![], None) + } MustRefund { dlc, .. } => ( vec![tx_ub.lock(&dlc), tx_ub.commit(&dlc), tx_ub.refund(&dlc)], Some(dlc.refund_amount(role)), @@ -397,6 +408,7 @@ fn to_cfd_details(state: model::cfd::CfdState, role: Role, network: Network) -> Refunded { dlc, .. } => (vec![tx_ub.refund(&dlc)], Some(dlc.refund_amount(role))), OutgoingOrderRequest { .. } | IncomingOrderRequest { .. } + | PendingClose { .. } | Accepted { .. } | Rejected { .. } | ContractSetup { .. }