From 7665f4cdfc396bed090c7ae593e3035a5ff11c49 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Thu, 14 Oct 2021 11:07:27 +1100 Subject: [PATCH] Collaborative close payout Distinguish between collaborative close and cet closing. Moved the payout on the `Cfd` which decides what the payout of the `Cfd` is, where - `None` means the payout is not decided yet (we can optimize that and potentially combine this with `profit` in another iteration) - `Some(payout)` can be a refund, collaborative or cet payout Refund is handled in an early exit guard based on the state. Collaborative settlement has priority over cet payout (attestation). In case there is a collaborative settlement present we always return payout based on that. --- daemon/src/maker_cfd.rs | 3 +- daemon/src/model/cfd.rs | 119 +++++++++++++++++++++++++++++++++---- daemon/src/taker_cfd.rs | 5 +- daemon/src/to_sse_event.rs | 89 ++++++++++----------------- 4 files changed, 143 insertions(+), 73 deletions(-) diff --git a/daemon/src/maker_cfd.rs b/daemon/src/maker_cfd.rs index a2ec805..20aae7f 100644 --- a/daemon/src/maker_cfd.rs +++ b/daemon/src/maker_cfd.rs @@ -304,7 +304,7 @@ impl Actor { let (tx, sig_maker) = dlc.close_transaction(proposal)?; cfd.handle(CfdStateChangeEvent::ProposalSigned( - TimestampedTransaction::new(tx.clone()), + TimestampedTransaction::new(tx.clone(), dlc.script_pubkey_for(cfd.role())), ))?; insert_new_cfd_state_by_order_id(cfd.order.id, cfd.state.clone(), &mut conn).await?; @@ -319,6 +319,7 @@ impl Actor { cfd.handle(CfdStateChangeEvent::CloseSent(TimestampedTransaction::new( spend_tx, + dlc.script_pubkey_for(cfd.role()), )))?; insert_new_cfd_state_by_order_id(cfd.order.id, cfd.state, &mut conn).await?; diff --git a/daemon/src/model/cfd.rs b/daemon/src/model/cfd.rs index bbb751c..d7cd949 100644 --- a/daemon/src/model/cfd.rs +++ b/daemon/src/model/cfd.rs @@ -273,7 +273,7 @@ pub enum CfdState { Closed { common: CfdStateCommon, // TODO: Use an enum of either Attestation or CollaborativeSettlement - attestation: Option, + payout: Payout, }, // TODO: Can be extended with CetStatus @@ -297,6 +297,12 @@ pub enum CfdState { }, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum Payout { + CollaborativeClose(TimestampedTransaction), + Cet(Attestation), +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Attestation { pub id: BitMexPriceEventId, @@ -323,10 +329,7 @@ impl Attestation { let txid = cet.tx.txid(); - let our_script_pubkey = match role { - Role::Maker => dlc.maker_address.script_pubkey(), - Role::Taker => dlc.taker_address.script_pubkey(), - }; + let our_script_pubkey = dlc.script_pubkey_for(role); let payout = cet .tx .output @@ -723,11 +726,16 @@ impl Cfd { }, } } - monitor::Event::CloseFinality(_) => CfdState::Closed { - common: CfdStateCommon { - transition_timestamp: SystemTime::now(), - }, - attestation: None + monitor::Event::CloseFinality(_) => { + let collaborative_close = self.collaborative_close().context("No collaborative close after reaching collaborative close finality")?; + + CfdState::Closed { + common: CfdStateCommon { + transition_timestamp: SystemTime::now(), + }, + payout: Payout::CollaborativeClose(collaborative_close) + } + }, monitor::Event::CetTimelockExpired(_) => match self.state.clone() { CfdState::OpenCommitted { @@ -819,7 +827,7 @@ impl Cfd { common: CfdStateCommon { transition_timestamp: SystemTime::now(), }, - attestation: Some(attestation), + payout: Payout::Cet(attestation) } } monitor::Event::RevokedTransactionFound(_) => { @@ -1180,7 +1188,7 @@ impl Cfd { } | CfdState::PendingCet { attestation, .. } | CfdState::Closed { - attestation: Some(attestation), + payout: Payout::Cet(attestation), .. } => Some(attestation), @@ -1200,6 +1208,60 @@ impl Cfd { | CfdState::SetupFailed { .. } => None, } } + + pub fn collaborative_close(&self) -> Option { + match self.state.clone() { + CfdState::Open { + collaborative_close: Some(collaborative_close), + .. + } + | CfdState::PendingClose { + collaborative_close, + .. + } + | CfdState::Closed { + payout: Payout::CollaborativeClose(collaborative_close), + .. + } => Some(collaborative_close), + + CfdState::OutgoingOrderRequest { .. } + | CfdState::IncomingOrderRequest { .. } + | CfdState::Accepted { .. } + | CfdState::Rejected { .. } + | CfdState::ContractSetup { .. } + | CfdState::PendingOpen { .. } + | CfdState::Open { .. } + | CfdState::PendingCommit { .. } + | CfdState::PendingCet { .. } + | CfdState::Closed { .. } + | CfdState::OpenCommitted { .. } + | CfdState::MustRefund { .. } + | CfdState::Refunded { .. } + | CfdState::SetupFailed { .. } => None, + } + } + + /// Returns the payout of the Cfd + /// + /// In case the cfd's payout is not fixed yet (because we don't have attestation or + /// collaborative close transaction None is returned, which means that the payout is still + /// undecided + pub fn payout(&self) -> Option { + // early exit in case of refund scenario + if let CfdState::MustRefund { dlc, .. } | CfdState::Refunded { dlc, .. } = + self.state.clone() + { + return Some(dlc.refund_amount(self.role())); + } + + // decision between attestation and collaborative close payout + match (self.attestation(), self.collaborative_close()) { + (Some(_attestation), Some(collaborative_close)) => Some(collaborative_close.payout()), + (None, Some(collaborative_close)) => Some(collaborative_close.payout()), + (Some(attestation), None) => Some(attestation.payout()), + (None, None) => None, + } + } } #[derive(thiserror::Error, Debug, Clone)] @@ -1664,6 +1726,13 @@ impl Dlc { .map(|output| Amount::from_sat(output.value)) .unwrap_or_default() } + + pub fn script_pubkey_for(&self, role: Role) -> Script { + match role { + Role::Maker => self.maker_address.script_pubkey(), + Role::Taker => self.taker_address.script_pubkey(), + } + } } /// Information which we need to remember in order to construct a @@ -1691,13 +1760,37 @@ pub struct RevokedCommit { pub struct TimestampedTransaction { pub tx: Transaction, pub timestamp: SystemTime, + #[serde(with = "::bdk::bitcoin::util::amount::serde::as_sat")] + payout: Amount, } impl TimestampedTransaction { - pub fn new(tx: Transaction) -> Self { + pub fn new(tx: Transaction, own_script_pubkey: Script) -> Self { + // Falls back to Amount::ZERO in case we don't find an output that matches out script pubkey + // The assumption is, that this can happen for cases where we were liuqidated + let payout = match tx + .output + .iter() + .find(|output| output.script_pubkey == own_script_pubkey) + .map(|output| Amount::from_sat(output.value)) + { + Some(payout) => payout, + None => { + tracing::error!( + "Collaborative settlement with a zero amount, this should really not happen!" + ); + Amount::ZERO + } + }; + Self { tx, timestamp: SystemTime::now(), + payout, } } + + pub fn payout(&self) -> Amount { + self.payout + } } diff --git a/daemon/src/taker_cfd.rs b/daemon/src/taker_cfd.rs index 05fe4d7..c9a0de1 100644 --- a/daemon/src/taker_cfd.rs +++ b/daemon/src/taker_cfd.rs @@ -370,10 +370,13 @@ impl Actor { .await?; cfd.handle(CfdStateChangeEvent::ProposalSigned( - TimestampedTransaction::new(tx), + TimestampedTransaction::new(tx, dlc.script_pubkey_for(cfd.role())), ))?; insert_new_cfd_state_by_order_id(cfd.order.id, cfd.state, &mut conn).await?; + self.cfd_feed_actor_inbox + .send(load_all_cfds(&mut conn).await?)?; + self.remove_pending_proposal(&order_id)?; Ok(()) diff --git a/daemon/src/to_sse_event.rs b/daemon/src/to_sse_event.rs index a145655..7ce4362 100644 --- a/daemon/src/to_sse_event.rs +++ b/daemon/src/to_sse_event.rs @@ -1,5 +1,5 @@ use crate::model::cfd::{ - CetStatus, Dlc, OrderId, Role, SettlementKind, UpdateCfdProposal, UpdateCfdProposals, + Dlc, OrderId, Payout, Role, SettlementKind, UpdateCfdProposal, UpdateCfdProposals, }; use crate::model::{Leverage, Position, TradingPair, Usd}; use crate::{bitmex_price_feed, model}; @@ -86,6 +86,10 @@ impl TxUrlBuilder { TxUrl::new(txid, self.network, TxLabel::Cet) } + pub fn collaborative_close(&self, txid: Txid) -> TxUrl { + TxUrl::new(txid, self.network, TxLabel::Collaborative) + } + pub fn refund(&self, dlc: &Dlc) -> TxUrl { TxUrl::new(dlc.refund.0.txid(), self.network, TxLabel::Refund) } @@ -97,6 +101,7 @@ pub enum TxLabel { Commit, Cet, Refund, + Collaborative, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -225,6 +230,11 @@ 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: to_tx_url_list(cfd.state.clone(), network), + payout: cfd.payout(), + }; + Cfd { order_id: cfd.order.id, initial_price: cfd.order.price, @@ -247,7 +257,7 @@ impl ToSseEvent for CfdsWithAuxData { // 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().unwrap(), - details: to_cfd_details(cfd.state.clone(), cfd.role(), network), + details, } }) .collect::>(); @@ -344,80 +354,43 @@ fn to_cfd_state( } } -fn to_cfd_details(state: model::cfd::CfdState, role: Role, network: Network) -> CfdDetails { +fn to_tx_url_list(state: model::cfd::CfdState, network: Network) -> Vec { use model::cfd::CfdState::*; let tx_ub = TxUrlBuilder::new(network); - let (txs, payout) = match state { - PendingOpen { - dlc, attestation, .. + match state { + PendingOpen { dlc, .. } | Open { dlc, .. } => { + vec![tx_ub.lock(&dlc)] } - | Open { - dlc, attestation, .. - } => ( - vec![tx_ub.lock(&dlc)], - attestation.map(|attestation| attestation.payout()), - ), - PendingCommit { - dlc, attestation, .. - } => ( - vec![tx_ub.lock(&dlc), tx_ub.commit(&dlc)], - attestation.map(|attestation| attestation.payout()), - ), - OpenCommitted { - dlc, - cet_status: CetStatus::Unprepared | CetStatus::TimelockExpired, - .. - } => (vec![tx_ub.lock(&dlc), tx_ub.commit(&dlc)], None), - OpenCommitted { - dlc, - cet_status: CetStatus::OracleSigned(attestation) | CetStatus::Ready(attestation), - .. - } => ( - vec![tx_ub.lock(&dlc), tx_ub.commit(&dlc)], - Some(attestation.payout()), - ), + PendingCommit { dlc, .. } => vec![tx_ub.lock(&dlc), tx_ub.commit(&dlc)], + OpenCommitted { dlc, .. } => vec![tx_ub.lock(&dlc), tx_ub.commit(&dlc)], PendingCet { dlc, attestation, .. - } => ( - vec![ - tx_ub.lock(&dlc), - tx_ub.commit(&dlc), - tx_ub.cet(attestation.txid()), - ], - Some(attestation.payout()), - ), + } => vec![ + tx_ub.lock(&dlc), + tx_ub.commit(&dlc), + tx_ub.cet(attestation.txid()), + ], Closed { - attestation: Some(attestation), + payout: Payout::Cet(attestation), .. - } => ( - vec![tx_ub.cet(attestation.txid())], - Some(attestation.payout()), - ), + } => vec![tx_ub.cet(attestation.txid())], Closed { - attestation: None, .. + payout: Payout::CollaborativeClose(collaborative_close), + .. } => { - // TODO: Provide CfdDetails about collaborative settlement - (vec![], None) + vec![tx_ub.collaborative_close(collaborative_close.tx.txid())] } - MustRefund { dlc, .. } => ( - vec![tx_ub.lock(&dlc), tx_ub.commit(&dlc), tx_ub.refund(&dlc)], - Some(dlc.refund_amount(role)), - ), - Refunded { dlc, .. } => (vec![tx_ub.refund(&dlc)], Some(dlc.refund_amount(role))), + MustRefund { dlc, .. } => vec![tx_ub.lock(&dlc), tx_ub.commit(&dlc), tx_ub.refund(&dlc)], + Refunded { dlc, .. } => vec![tx_ub.refund(&dlc)], OutgoingOrderRequest { .. } | IncomingOrderRequest { .. } | PendingClose { .. } | Accepted { .. } | Rejected { .. } | ContractSetup { .. } - | SetupFailed { .. } => (vec![], None), - }; - - CfdDetails { - tx_url_list: txs, - payout, + | SetupFailed { .. } => vec![], } }