diff --git a/daemon/src/maker_cfd.rs b/daemon/src/maker_cfd.rs index 2c95d73..2452e98 100644 --- a/daemon/src/maker_cfd.rs +++ b/daemon/src/maker_cfd.rs @@ -5,8 +5,9 @@ use crate::db::{ }; use crate::maker_inc_connections::TakerCommand; use crate::model::cfd::{ - Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, Dlc, Order, OrderId, Origin, Role, - RollOverProposal, SettlementKind, SettlementProposal, UpdateCfdProposal, UpdateCfdProposals, + Attestation, Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, Dlc, Order, OrderId, Origin, + Role, RollOverProposal, SettlementKind, SettlementProposal, UpdateCfdProposal, + UpdateCfdProposals, }; use crate::model::{TakerId, Usd}; use crate::monitor::MonitorParams; @@ -891,9 +892,14 @@ impl Actor { for mut cfd in cfds { if cfd - .handle(CfdStateChangeEvent::OracleAttestation( - attestation.clone().into(), - ))? + .handle(CfdStateChangeEvent::OracleAttestation(Attestation::new( + attestation.id.clone(), + attestation.price, + attestation.scalars.clone(), + cfd.dlc() + .context("No DLC available when attestation was received")?, + cfd.role(), + )?))? .is_none() { // if we don't transition to a new state after oracle attestation we ignore the cfd diff --git a/daemon/src/model/cfd.rs b/daemon/src/model/cfd.rs index 9b42423..0c5d09f 100644 --- a/daemon/src/model/cfd.rs +++ b/daemon/src/model/cfd.rs @@ -248,7 +248,7 @@ pub enum CfdState { PendingCet { common: CfdStateCommon, dlc: Dlc, - cet_status: CetStatus, + attestation: Attestation, }, /// The position was closed collaboratively or non-collaboratively @@ -258,7 +258,10 @@ pub enum CfdState { /// This is the final state for all happy-path scenarios where we had an open position and then /// "settled" it. Settlement can be collaboratively or non-collaboratively (by publishing /// commit + cet). - Closed { common: CfdStateCommon }, + Closed { + common: CfdStateCommon, + attestation: Attestation, + }, // TODO: Can be extended with CetStatus /// The CFD contract's refund transaction was published but it not final yet @@ -284,17 +287,50 @@ pub enum CfdState { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Attestation { pub id: OracleEventId, - pub price: u64, pub scalars: Vec, + #[serde(with = "::bdk::bitcoin::util::amount::serde::as_sat")] + payout: Amount, + price: u64, } -impl From for Attestation { - fn from(attestation: oracle::Attestation) -> Self { - Attestation { - id: attestation.id, - price: attestation.price, - scalars: attestation.scalars, - } +impl Attestation { + pub fn new( + id: OracleEventId, + price: u64, + scalars: Vec, + dlc: Dlc, + role: Role, + ) -> Result { + let cet = dlc + .cets + .iter() + .find_map(|(_, cet)| cet.iter().find(|cet| cet.range.contains(&price))) + .context("Unable to find attested price in any range")?; + + let payout = cet + .tx + .output + .iter() + .find_map(|output| { + (output.script_pubkey + == match role { + Role::Maker => dlc.maker_address.script_pubkey(), + Role::Taker => dlc.taker_address.script_pubkey(), + }) + .then(|| Amount::from_sat(output.value)) + }) + .unwrap_or_default(); + + Ok(Self { + id, + price, + scalars, + payout, + }) + } + + pub fn price(&self) -> Usd { + Usd(Decimal::from(self.price)) } } @@ -483,6 +519,14 @@ 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! + + let current_price = if let Some(attestation) = self.attestation() { + attestation.price() + } else { + current_price + }; + let (p_n_l, p_n_l_percent) = calculate_profit( self.order.price, current_price, @@ -708,11 +752,18 @@ impl Cfd { }, } } - monitor::Event::CetFinality(_) => Closed { - common: CfdStateCommon { - transition_timestamp: SystemTime::now(), - }, - }, + monitor::Event::CetFinality(_) => { + let attestation = self + .attestation() + .context("No attestation available when reaching CET finality")?; + + CfdState::Closed { + common: CfdStateCommon { + transition_timestamp: SystemTime::now(), + }, + attestation, + } + } monitor::Event::RevokedTransactionFound(_) => { todo!("Punish bad counterparty") } @@ -790,21 +841,20 @@ impl Cfd { self.state ), }, - CfdStateChangeEvent::CetSent => match self.state.clone() { - CfdState::OpenCommitted { - common, - dlc, - cet_status, - } => CfdState::PendingCet { - common, + CfdStateChangeEvent::CetSent => { + let dlc = self.dlc().context("No DLC available after CET was sent")?; + let attestation = self + .attestation() + .context("No attestation available after CET was sent")?; + + CfdState::PendingCet { + common: CfdStateCommon { + transition_timestamp: SystemTime::now(), + }, dlc, - cet_status, - }, - _ => bail!( - "Cannot transition to PendingCet because of unexpected state {}", - self.state - ), - }, + attestation, + } + } }; self.state = new_state.clone(); @@ -886,9 +936,7 @@ impl Cfd { .. } | CfdState::PendingCet { - dlc, - cet_status: CetStatus::Ready(attestation), - .. + dlc, attestation, .. } => (dlc, attestation), CfdState::OpenCommitted { cet_status, .. } => { return Ok(Err(NotReadyYet { cet_status })); @@ -988,6 +1036,62 @@ impl Cfd { pub fn role(&self) -> Role { self.order.origin.into() } + + pub fn dlc(&self) -> Option { + match self.state.clone() { + CfdState::PendingOpen { dlc, .. } + | CfdState::Open { dlc, .. } + | CfdState::PendingCommit { dlc, .. } + | CfdState::OpenCommitted { dlc, .. } + | CfdState::PendingCet { dlc, .. } => Some(dlc), + + CfdState::OutgoingOrderRequest { .. } + | CfdState::IncomingOrderRequest { .. } + | CfdState::Accepted { .. } + | CfdState::Rejected { .. } + | CfdState::ContractSetup { .. } + | CfdState::Closed { .. } + | CfdState::MustRefund { .. } + | CfdState::Refunded { .. } + | CfdState::SetupFailed { .. } => None, + } + } + + fn attestation(&self) -> Option { + match self.state.clone() { + CfdState::PendingOpen { + attestation: Some(attestation), + .. + } + | CfdState::Open { + attestation: Some(attestation), + .. + } + | CfdState::PendingCommit { + attestation: Some(attestation), + .. + } + | CfdState::OpenCommitted { + cet_status: CetStatus::OracleSigned(attestation) | CetStatus::Ready(attestation), + .. + } + | CfdState::PendingCet { attestation, .. } + | CfdState::Closed { attestation, .. } => Some(attestation), + + CfdState::OutgoingOrderRequest { .. } + | CfdState::IncomingOrderRequest { .. } + | CfdState::Accepted { .. } + | CfdState::Rejected { .. } + | CfdState::ContractSetup { .. } + | CfdState::PendingOpen { .. } + | CfdState::Open { .. } + | CfdState::PendingCommit { .. } + | CfdState::OpenCommitted { .. } + | CfdState::MustRefund { .. } + | CfdState::Refunded { .. } + | CfdState::SetupFailed { .. } => None, + } + } } #[derive(thiserror::Error, Debug, Clone)] diff --git a/daemon/src/monitor.rs b/daemon/src/monitor.rs index ce7cbb4..51222b0 100644 --- a/daemon/src/monitor.rs +++ b/daemon/src/monitor.rs @@ -91,8 +91,7 @@ where actor.monitor_commit_refund_timelock(¶ms, cfd.order.id); actor.monitor_refund_finality(¶ms,cfd.order.id); } - CfdState::OpenCommitted { dlc, cet_status, .. } - | CfdState::PendingCet { dlc, cet_status, .. } => { + CfdState::OpenCommitted { dlc, cet_status, .. } => { let params = MonitorParams::from_dlc_and_timelocks(dlc.clone(), cfd.refund_timelock_in_blocks()); actor.cfds.insert(cfd.order.id, params.clone()); @@ -119,6 +118,14 @@ where } } } + CfdState::PendingCet { dlc, attestation, .. } => { + let params = MonitorParams::from_dlc_and_timelocks(dlc.clone(), cfd.refund_timelock_in_blocks()); + actor.cfds.insert(cfd.order.id, params.clone()); + + 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::MustRefund { dlc, .. } => { let params = MonitorParams::from_dlc_and_timelocks(dlc.clone(), cfd.refund_timelock_in_blocks()); actor.cfds.insert(cfd.order.id, params.clone()); diff --git a/daemon/src/taker_cfd.rs b/daemon/src/taker_cfd.rs index a03c61d..c308004 100644 --- a/daemon/src/taker_cfd.rs +++ b/daemon/src/taker_cfd.rs @@ -4,8 +4,9 @@ use crate::db::{ load_cfd_by_order_id, load_cfds_by_oracle_event_id, load_order_by_id, }; use crate::model::cfd::{ - Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, Dlc, Order, OrderId, Origin, Role, - RollOverProposal, SettlementKind, SettlementProposal, UpdateCfdProposal, UpdateCfdProposals, + Attestation, Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, Dlc, Order, OrderId, Origin, + Role, RollOverProposal, SettlementKind, SettlementProposal, UpdateCfdProposal, + UpdateCfdProposals, }; use crate::model::{OracleEventId, Usd}; use crate::monitor::{self, MonitorParams}; @@ -632,9 +633,14 @@ impl Actor { for mut cfd in cfds { if cfd - .handle(CfdStateChangeEvent::OracleAttestation( - attestation.clone().into(), - ))? + .handle(CfdStateChangeEvent::OracleAttestation(Attestation::new( + attestation.id.clone(), + attestation.price, + attestation.scalars.clone(), + cfd.dlc() + .context("No DLC available when attestation was received")?, + cfd.role(), + )?))? .is_none() { // if we don't transition to a new state after oracle attestation we ignore the cfd