Browse Source

Merge #272

272: Transaction IDs and payout in the UI r=da-kami a=da-kami

- [x] Profit based on attested price.
- [x] Show the payout and transaction ids in the UI
![image](https://user-images.githubusercontent.com/5557790/136896082-2e95bf94-fd88-4e6b-8e79-d2690bd98913.png)


Co-authored-by: Daniel Karzel <daniel@comit.network>
refactor/no-log-handler
bors[bot] 3 years ago
committed by GitHub
parent
commit
0b7ebdee21
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      daemon/src/maker.rs
  2. 16
      daemon/src/maker_cfd.rs
  3. 199
      daemon/src/model/cfd.rs
  4. 11
      daemon/src/monitor.rs
  5. 34
      daemon/src/routes_maker.rs
  6. 36
      daemon/src/routes_taker.rs
  7. 4
      daemon/src/taker.rs
  8. 16
      daemon/src/taker_cfd.rs
  9. 141
      daemon/src/to_sse_event.rs
  10. 11
      frontend/src/components/Types.tsx
  11. 31
      frontend/src/components/cfdtables/CfdTable.tsx

4
daemon/src/maker.rs

@ -142,7 +142,8 @@ async fn main() -> Result<()> {
let seed = Seed::initialize(&data_dir.join("maker_seed"), opts.generate_seed).await?; let seed = Seed::initialize(&data_dir.join("maker_seed"), opts.generate_seed).await?;
let ext_priv_key = seed.derive_extended_priv_key(opts.network.bitcoin_network())?; let bitcoin_network = opts.network.bitcoin_network();
let ext_priv_key = seed.derive_extended_priv_key(bitcoin_network)?;
let wallet = Wallet::new( let wallet = Wallet::new(
opts.network.electrum(), opts.network.electrum(),
@ -189,6 +190,7 @@ async fn main() -> Result<()> {
.manage(update_cfd_feed_receiver) .manage(update_cfd_feed_receiver)
.manage(auth_password) .manage(auth_password)
.manage(quote_updates) .manage(quote_updates)
.manage(bitcoin_network)
.attach(Db::init()) .attach(Db::init())
.attach(AdHoc::try_on_ignite( .attach(AdHoc::try_on_ignite(
"SQL migrations", "SQL migrations",

16
daemon/src/maker_cfd.rs

@ -5,8 +5,9 @@ use crate::db::{
}; };
use crate::maker_inc_connections::TakerCommand; use crate::maker_inc_connections::TakerCommand;
use crate::model::cfd::{ use crate::model::cfd::{
Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, Dlc, Order, OrderId, Origin, Role, Attestation, Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, Dlc, Order, OrderId, Origin,
RollOverProposal, SettlementKind, SettlementProposal, UpdateCfdProposal, UpdateCfdProposals, Role, RollOverProposal, SettlementKind, SettlementProposal, UpdateCfdProposal,
UpdateCfdProposals,
}; };
use crate::model::{TakerId, Usd}; use crate::model::{TakerId, Usd};
use crate::monitor::MonitorParams; use crate::monitor::MonitorParams;
@ -891,9 +892,14 @@ impl Actor {
for mut cfd in cfds { for mut cfd in cfds {
if cfd if cfd
.handle(CfdStateChangeEvent::OracleAttestation( .handle(CfdStateChangeEvent::OracleAttestation(Attestation::new(
attestation.clone().into(), attestation.id.clone(),
))? attestation.price,
attestation.scalars.clone(),
cfd.dlc()
.context("No DLC available when attestation was received")?,
cfd.role(),
)?))?
.is_none() .is_none()
{ {
// if we don't transition to a new state after oracle attestation we ignore the cfd // if we don't transition to a new state after oracle attestation we ignore the cfd

199
daemon/src/model/cfd.rs

@ -248,7 +248,7 @@ pub enum CfdState {
PendingCet { PendingCet {
common: CfdStateCommon, common: CfdStateCommon,
dlc: Dlc, dlc: Dlc,
cet_status: CetStatus, attestation: Attestation,
}, },
/// The position was closed collaboratively or non-collaboratively /// 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 /// 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 /// "settled" it. Settlement can be collaboratively or non-collaboratively (by publishing
/// commit + cet). /// commit + cet).
Closed { common: CfdStateCommon }, Closed {
common: CfdStateCommon,
attestation: Attestation,
},
// TODO: Can be extended with CetStatus // TODO: Can be extended with CetStatus
/// The CFD contract's refund transaction was published but it not final yet /// The CFD contract's refund transaction was published but it not final yet
@ -268,7 +271,7 @@ pub enum CfdState {
/// ///
/// This state applies to taker and maker. /// This state applies to taker and maker.
/// This is a final state. /// This is a final state.
Refunded { common: CfdStateCommon }, Refunded { common: CfdStateCommon, dlc: Dlc },
/// The Cfd was in a state that could not be continued after the application got interrupted /// The Cfd was in a state that could not be continued after the application got interrupted
/// ///
@ -284,17 +287,61 @@ pub enum CfdState {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Attestation { pub struct Attestation {
pub id: OracleEventId, pub id: OracleEventId,
pub price: u64,
pub scalars: Vec<SecretKey>, pub scalars: Vec<SecretKey>,
#[serde(with = "::bdk::bitcoin::util::amount::serde::as_sat")]
payout: Amount,
price: u64,
txid: Txid,
} }
impl From<oracle::Attestation> for Attestation { impl Attestation {
fn from(attestation: oracle::Attestation) -> Self { pub fn new(
Attestation { id: OracleEventId,
id: attestation.id, price: u64,
price: attestation.price, scalars: Vec<SecretKey>,
scalars: attestation.scalars, dlc: Dlc,
role: Role,
) -> Result<Self> {
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 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 payout = cet
.tx
.output
.iter()
.find_map(|output| {
(output.script_pubkey == our_script_pubkey).then(|| Amount::from_sat(output.value))
})
.unwrap_or_default();
Ok(Self {
id,
price,
scalars,
payout,
txid,
})
} }
pub fn price(&self) -> Usd {
Usd(Decimal::from(self.price))
}
pub fn txid(&self) -> Txid {
self.txid
}
pub fn payout(&self) -> Amount {
self.payout
} }
} }
@ -483,6 +530,14 @@ impl Cfd {
} }
pub fn profit(&self, current_price: Usd) -> Result<(SignedAmount, Percent)> { 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( let (p_n_l, p_n_l_percent) = calculate_profit(
self.order.price, self.order.price,
current_price, current_price,
@ -694,25 +749,29 @@ impl Cfd {
} }
} }
monitor::Event::RefundFinality(_) => { monitor::Event::RefundFinality(_) => {
if let MustRefund { .. } = self.state.clone() { let dlc = self
} else { .dlc()
tracing::debug!( .context("No dlc available when reaching refund finality")?;
"Was in unexpected state {}, jumping ahead to Refunded",
self.state
);
}
Refunded { Refunded {
common: CfdStateCommon { common: CfdStateCommon {
transition_timestamp: SystemTime::now(), transition_timestamp: SystemTime::now(),
}, },
dlc,
} }
} }
monitor::Event::CetFinality(_) => Closed { monitor::Event::CetFinality(_) => {
let attestation = self
.attestation()
.context("No attestation available when reaching CET finality")?;
CfdState::Closed {
common: CfdStateCommon { common: CfdStateCommon {
transition_timestamp: SystemTime::now(), transition_timestamp: SystemTime::now(),
}, },
}, attestation,
}
}
monitor::Event::RevokedTransactionFound(_) => { monitor::Event::RevokedTransactionFound(_) => {
todo!("Punish bad counterparty") todo!("Punish bad counterparty")
} }
@ -790,21 +849,20 @@ impl Cfd {
self.state self.state
), ),
}, },
CfdStateChangeEvent::CetSent => match self.state.clone() { CfdStateChangeEvent::CetSent => {
CfdState::OpenCommitted { let dlc = self.dlc().context("No DLC available after CET was sent")?;
common, let attestation = self
dlc, .attestation()
cet_status, .context("No attestation available after CET was sent")?;
} => CfdState::PendingCet {
common, CfdState::PendingCet {
dlc, common: CfdStateCommon {
cet_status, transition_timestamp: SystemTime::now(),
},
_ => bail!(
"Cannot transition to PendingCet because of unexpected state {}",
self.state
),
}, },
dlc,
attestation,
}
}
}; };
self.state = new_state.clone(); self.state = new_state.clone();
@ -886,9 +944,7 @@ impl Cfd {
.. ..
} }
| CfdState::PendingCet { | CfdState::PendingCet {
dlc, dlc, attestation, ..
cet_status: CetStatus::Ready(attestation),
..
} => (dlc, attestation), } => (dlc, attestation),
CfdState::OpenCommitted { cet_status, .. } => { CfdState::OpenCommitted { cet_status, .. } => {
return Ok(Err(NotReadyYet { cet_status })); return Ok(Err(NotReadyYet { cet_status }));
@ -988,6 +1044,62 @@ impl Cfd {
pub fn role(&self) -> Role { pub fn role(&self) -> Role {
self.order.origin.into() self.order.origin.into()
} }
pub fn dlc(&self) -> Option<Dlc> {
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<Attestation> {
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)] #[derive(thiserror::Error, Debug, Clone)]
@ -1432,6 +1544,21 @@ impl Dlc {
Ok(spend_tx) Ok(spend_tx)
} }
pub fn refund_amount(&self, role: Role) -> Amount {
let our_script_pubkey = match role {
Role::Taker => self.taker_address.script_pubkey(),
Role::Maker => self.maker_address.script_pubkey(),
};
self.refund
.0
.output
.iter()
.find(|output| output.script_pubkey == our_script_pubkey)
.map(|output| Amount::from_sat(output.value))
.unwrap_or_default()
}
} }
/// Information which we need to remember in order to construct a /// Information which we need to remember in order to construct a

11
daemon/src/monitor.rs

@ -91,8 +91,7 @@ where
actor.monitor_commit_refund_timelock(&params, cfd.order.id); actor.monitor_commit_refund_timelock(&params, cfd.order.id);
actor.monitor_refund_finality(&params,cfd.order.id); actor.monitor_refund_finality(&params,cfd.order.id);
} }
CfdState::OpenCommitted { dlc, cet_status, .. } CfdState::OpenCommitted { dlc, cet_status, .. } => {
| CfdState::PendingCet { dlc, cet_status, .. } => {
let params = MonitorParams::from_dlc_and_timelocks(dlc.clone(), cfd.refund_timelock_in_blocks()); let params = MonitorParams::from_dlc_and_timelocks(dlc.clone(), cfd.refund_timelock_in_blocks());
actor.cfds.insert(cfd.order.id, params.clone()); 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(&params, cfd.order.id);
actor.monitor_refund_finality(&params,cfd.order.id);
}
CfdState::MustRefund { dlc, .. } => { CfdState::MustRefund { dlc, .. } => {
let params = MonitorParams::from_dlc_and_timelocks(dlc.clone(), cfd.refund_timelock_in_blocks()); let params = MonitorParams::from_dlc_and_timelocks(dlc.clone(), cfd.refund_timelock_in_blocks());
actor.cfds.insert(cfd.order.id, params.clone()); actor.cfds.insert(cfd.order.id, params.clone());

34
daemon/src/routes_maker.rs

@ -5,6 +5,7 @@ use crate::routes::EmbeddedFileExt;
use crate::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent}; use crate::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent};
use crate::{bitmex_price_feed, maker_cfd}; use crate::{bitmex_price_feed, maker_cfd};
use anyhow::Result; use anyhow::Result;
use bdk::bitcoin::Network;
use rocket::http::{ContentType, Header, Status}; use rocket::http::{ContentType, Header, Status};
use rocket::response::stream::EventStream; use rocket::response::stream::EventStream;
use rocket::response::{status, Responder}; use rocket::response::{status, Responder};
@ -25,6 +26,7 @@ pub async fn maker_feed(
rx_wallet: &State<watch::Receiver<WalletInfo>>, rx_wallet: &State<watch::Receiver<WalletInfo>>,
rx_quote: &State<watch::Receiver<bitmex_price_feed::Quote>>, rx_quote: &State<watch::Receiver<bitmex_price_feed::Quote>>,
rx_settlements: &State<watch::Receiver<UpdateCfdProposals>>, rx_settlements: &State<watch::Receiver<UpdateCfdProposals>>,
network: &State<Network>,
_auth: Authenticated, _auth: Authenticated,
) -> EventStream![] { ) -> EventStream![] {
let mut rx_cfds = rx_cfds.inner().clone(); let mut rx_cfds = rx_cfds.inner().clone();
@ -32,6 +34,7 @@ pub async fn maker_feed(
let mut rx_wallet = rx_wallet.inner().clone(); let mut rx_wallet = rx_wallet.inner().clone();
let mut rx_quote = rx_quote.inner().clone(); let mut rx_quote = rx_quote.inner().clone();
let mut rx_settlements = rx_settlements.inner().clone(); let mut rx_settlements = rx_settlements.inner().clone();
let network = *network.inner();
EventStream! { EventStream! {
let wallet_info = rx_wallet.borrow().clone(); let wallet_info = rx_wallet.borrow().clone();
@ -43,7 +46,12 @@ pub async fn maker_feed(
let quote = rx_quote.borrow().clone(); let quote = rx_quote.borrow().clone();
yield quote.to_sse_event(); yield quote.to_sse_event();
yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Maker).to_sse_event(); yield CfdsWithAuxData::new(
&rx_cfds,
&rx_quote,
&rx_settlements,
Role::Maker, network
).to_sse_event();
loop{ loop{
select! { select! {
@ -56,15 +64,33 @@ pub async fn maker_feed(
yield order.to_sse_event(); yield order.to_sse_event();
} }
Ok(()) = rx_cfds.changed() => { Ok(()) = rx_cfds.changed() => {
yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Maker).to_sse_event(); yield CfdsWithAuxData::new(
&rx_cfds,
&rx_quote,
&rx_settlements,
Role::Maker,
network
).to_sse_event();
} }
Ok(()) = rx_settlements.changed() => { Ok(()) = rx_settlements.changed() => {
yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Maker).to_sse_event(); yield CfdsWithAuxData::new(
&rx_cfds,
&rx_quote,
&rx_settlements,
Role::Maker,
network
).to_sse_event();
} }
Ok(()) = rx_quote.changed() => { Ok(()) = rx_quote.changed() => {
let quote = rx_quote.borrow().clone(); let quote = rx_quote.borrow().clone();
yield quote.to_sse_event(); yield quote.to_sse_event();
yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Maker).to_sse_event(); yield CfdsWithAuxData::new(
&rx_cfds,
&rx_quote,
&rx_settlements,
Role::Maker,
network
).to_sse_event();
} }
} }
} }

36
daemon/src/routes_taker.rs

@ -3,7 +3,7 @@ use crate::model::{Leverage, Usd, WalletInfo};
use crate::routes::EmbeddedFileExt; use crate::routes::EmbeddedFileExt;
use crate::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent}; use crate::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent};
use crate::{bitmex_price_feed, taker_cfd}; use crate::{bitmex_price_feed, taker_cfd};
use bdk::bitcoin::Amount; use bdk::bitcoin::{Amount, Network};
use rocket::http::{ContentType, Status}; use rocket::http::{ContentType, Status};
use rocket::response::stream::EventStream; use rocket::response::stream::EventStream;
use rocket::response::{status, Responder}; use rocket::response::{status, Responder};
@ -24,12 +24,14 @@ pub async fn feed(
rx_wallet: &State<watch::Receiver<WalletInfo>>, rx_wallet: &State<watch::Receiver<WalletInfo>>,
rx_quote: &State<watch::Receiver<bitmex_price_feed::Quote>>, rx_quote: &State<watch::Receiver<bitmex_price_feed::Quote>>,
rx_settlements: &State<watch::Receiver<UpdateCfdProposals>>, rx_settlements: &State<watch::Receiver<UpdateCfdProposals>>,
network: &State<Network>,
) -> EventStream![] { ) -> EventStream![] {
let mut rx_cfds = rx_cfds.inner().clone(); let mut rx_cfds = rx_cfds.inner().clone();
let mut rx_order = rx_order.inner().clone(); let mut rx_order = rx_order.inner().clone();
let mut rx_wallet = rx_wallet.inner().clone(); let mut rx_wallet = rx_wallet.inner().clone();
let mut rx_quote = rx_quote.inner().clone(); let mut rx_quote = rx_quote.inner().clone();
let mut rx_settlements = rx_settlements.inner().clone(); let mut rx_settlements = rx_settlements.inner().clone();
let network = *network.inner();
EventStream! { EventStream! {
let wallet_info = rx_wallet.borrow().clone(); let wallet_info = rx_wallet.borrow().clone();
@ -41,7 +43,13 @@ pub async fn feed(
let quote = rx_quote.borrow().clone(); let quote = rx_quote.borrow().clone();
yield quote.to_sse_event(); yield quote.to_sse_event();
yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Taker).to_sse_event(); yield CfdsWithAuxData::new(
&rx_cfds,
&rx_quote,
&rx_settlements,
Role::Taker,
network
).to_sse_event();
loop{ loop{
select! { select! {
@ -54,15 +62,33 @@ pub async fn feed(
yield order.to_sse_event(); yield order.to_sse_event();
} }
Ok(()) = rx_cfds.changed() => { Ok(()) = rx_cfds.changed() => {
yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Taker).to_sse_event(); yield CfdsWithAuxData::new(
&rx_cfds,
&rx_quote,
&rx_settlements,
Role::Taker,
network
).to_sse_event();
} }
Ok(()) = rx_settlements.changed() => { Ok(()) = rx_settlements.changed() => {
yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Taker).to_sse_event(); yield CfdsWithAuxData::new(
&rx_cfds,
&rx_quote,
&rx_settlements,
Role::Taker,
network
).to_sse_event();
} }
Ok(()) = rx_quote.changed() => { Ok(()) = rx_quote.changed() => {
let quote = rx_quote.borrow().clone(); let quote = rx_quote.borrow().clone();
yield quote.to_sse_event(); yield quote.to_sse_event();
yield CfdsWithAuxData::new(&rx_cfds, &rx_quote, &rx_settlements, Role::Taker).to_sse_event(); yield CfdsWithAuxData::new(
&rx_cfds,
&rx_quote,
&rx_settlements,
Role::Taker,
network
).to_sse_event();
} }
} }
} }

4
daemon/src/taker.rs

@ -142,7 +142,8 @@ async fn main() -> Result<()> {
let seed = Seed::initialize(&data_dir.join("taker_seed"), opts.generate_seed).await?; let seed = Seed::initialize(&data_dir.join("taker_seed"), opts.generate_seed).await?;
let ext_priv_key = seed.derive_extended_priv_key(opts.network.bitcoin_network())?; let bitcoin_network = opts.network.bitcoin_network();
let ext_priv_key = seed.derive_extended_priv_key(bitcoin_network)?;
let wallet = Wallet::new( let wallet = Wallet::new(
opts.network.electrum(), opts.network.electrum(),
@ -188,6 +189,7 @@ async fn main() -> Result<()> {
.manage(wallet_feed_receiver) .manage(wallet_feed_receiver)
.manage(update_feed_receiver) .manage(update_feed_receiver)
.manage(quote_updates) .manage(quote_updates)
.manage(bitcoin_network)
.attach(Db::init()) .attach(Db::init())
.attach(AdHoc::try_on_ignite( .attach(AdHoc::try_on_ignite(
"SQL migrations", "SQL migrations",

16
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, load_cfd_by_order_id, load_cfds_by_oracle_event_id, load_order_by_id,
}; };
use crate::model::cfd::{ use crate::model::cfd::{
Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, Dlc, Order, OrderId, Origin, Role, Attestation, Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, Dlc, Order, OrderId, Origin,
RollOverProposal, SettlementKind, SettlementProposal, UpdateCfdProposal, UpdateCfdProposals, Role, RollOverProposal, SettlementKind, SettlementProposal, UpdateCfdProposal,
UpdateCfdProposals,
}; };
use crate::model::{OracleEventId, Usd}; use crate::model::{OracleEventId, Usd};
use crate::monitor::{self, MonitorParams}; use crate::monitor::{self, MonitorParams};
@ -632,9 +633,14 @@ impl Actor {
for mut cfd in cfds { for mut cfd in cfds {
if cfd if cfd
.handle(CfdStateChangeEvent::OracleAttestation( .handle(CfdStateChangeEvent::OracleAttestation(Attestation::new(
attestation.clone().into(), attestation.id.clone(),
))? attestation.price,
attestation.scalars.clone(),
cfd.dlc()
.context("No DLC available when attestation was received")?,
cfd.role(),
)?))?
.is_none() .is_none()
{ {
// if we don't transition to a new state after oracle attestation we ignore the cfd // if we don't transition to a new state after oracle attestation we ignore the cfd

141
daemon/src/to_sse_event.rs

@ -1,7 +1,9 @@
use crate::model::cfd::{OrderId, Role, SettlementKind, UpdateCfdProposal, UpdateCfdProposals}; use crate::model::cfd::{
CetStatus, Dlc, OrderId, Role, SettlementKind, UpdateCfdProposal, UpdateCfdProposals,
};
use crate::model::{Leverage, Position, TradingPair, Usd}; use crate::model::{Leverage, Position, TradingPair, Usd};
use crate::{bitmex_price_feed, model}; use crate::{bitmex_price_feed, model};
use bdk::bitcoin::{Amount, SignedAmount}; use bdk::bitcoin::{Amount, Network, SignedAmount, Txid};
use rocket::request::FromParam; use rocket::request::FromParam;
use rocket::response::stream::Event; use rocket::response::stream::Event;
use rust_decimal::Decimal; use rust_decimal::Decimal;
@ -32,6 +34,69 @@ pub struct Cfd {
pub state: CfdState, pub state: CfdState,
pub actions: Vec<CfdAction>, pub actions: Vec<CfdAction>,
pub state_transition_timestamp: u64, pub state_transition_timestamp: u64,
pub details: CfdDetails,
}
#[derive(Debug, Clone, Serialize)]
pub struct CfdDetails {
tx_url_list: Vec<TxUrl>,
#[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc::opt")]
payout: Option<Amount>,
}
#[derive(Debug, Clone, Serialize)]
pub struct TxUrl {
pub label: TxLabel,
pub url: String,
}
impl TxUrl {
pub fn new(txid: Txid, network: Network, label: TxLabel) -> Self {
Self {
label,
url: match network {
Network::Bitcoin => format!("https://mempool.space/tx/{}", txid),
Network::Testnet => format!("https://mempool.space/testnet/tx/{}", txid),
Network::Signet => format!("https://mempool.space/signet/tx/{}", txid),
Network::Regtest => txid.to_string(),
},
}
}
}
struct TxUrlBuilder {
network: Network,
}
impl TxUrlBuilder {
pub fn new(network: Network) -> Self {
Self { network }
}
pub fn lock(&self, dlc: &Dlc) -> TxUrl {
TxUrl::new(dlc.lock.0.txid(), self.network, TxLabel::Lock)
}
pub fn commit(&self, dlc: &Dlc) -> TxUrl {
TxUrl::new(dlc.commit.0.txid(), self.network, TxLabel::Commit)
}
pub fn cet(&self, txid: Txid) -> TxUrl {
TxUrl::new(txid, self.network, TxLabel::Cet)
}
pub fn refund(&self, dlc: &Dlc) -> TxUrl {
TxUrl::new(dlc.refund.0.txid(), self.network, TxLabel::Refund)
}
}
#[derive(Debug, Clone, Serialize)]
pub enum TxLabel {
Lock,
Commit,
Cet,
Refund,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@ -109,6 +174,7 @@ pub struct CfdsWithAuxData {
pub cfds: Vec<model::cfd::Cfd>, pub cfds: Vec<model::cfd::Cfd>,
pub current_price: Usd, pub current_price: Usd,
pub pending_proposals: UpdateCfdProposals, pub pending_proposals: UpdateCfdProposals,
pub network: Network,
} }
impl CfdsWithAuxData { impl CfdsWithAuxData {
@ -117,6 +183,7 @@ impl CfdsWithAuxData {
rx_quote: &watch::Receiver<bitmex_price_feed::Quote>, rx_quote: &watch::Receiver<bitmex_price_feed::Quote>,
rx_updates: &watch::Receiver<UpdateCfdProposals>, rx_updates: &watch::Receiver<UpdateCfdProposals>,
role: Role, role: Role,
network: Network,
) -> Self { ) -> Self {
let quote = rx_quote.borrow().clone(); let quote = rx_quote.borrow().clone();
let current_price = match role { let current_price = match role {
@ -130,6 +197,7 @@ impl CfdsWithAuxData {
cfds: rx_cfds.borrow().clone(), cfds: rx_cfds.borrow().clone(),
current_price, current_price,
pending_proposals, pending_proposals,
network,
} }
} }
} }
@ -138,6 +206,7 @@ impl ToSseEvent for CfdsWithAuxData {
// TODO: This conversion can fail, we might want to change the API // TODO: This conversion can fail, we might want to change the API
fn to_sse_event(&self) -> Event { fn to_sse_event(&self) -> Event {
let current_price = self.current_price; let current_price = self.current_price;
let network = self.network;
let cfds = self let cfds = self
.cfds .cfds
@ -177,6 +246,7 @@ impl ToSseEvent for CfdsWithAuxData {
// TODO: Depending on the state the margin might be set (i.e. in Open we save it // 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 // in the DB internally) and does not have to be calculated
margin: cfd.margin().unwrap(), margin: cfd.margin().unwrap(),
details: to_cfd_details(cfd.state.clone(), cfd.role(), network),
} }
}) })
.collect::<Vec<Cfd>>(); .collect::<Vec<Cfd>>();
@ -272,6 +342,73 @@ fn to_cfd_state(
} }
} }
fn to_cfd_details(state: model::cfd::CfdState, role: Role, network: Network) -> CfdDetails {
use model::cfd::CfdState::*;
let tx_ub = TxUrlBuilder::new(network);
let (txs, payout) = match state {
PendingOpen {
dlc, attestation, ..
}
| 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()),
),
PendingCet {
dlc, attestation, ..
} => (
vec![
tx_ub.lock(&dlc),
tx_ub.commit(&dlc),
tx_ub.cet(attestation.txid()),
],
Some(attestation.payout()),
),
Closed { attestation, .. } => (
vec![tx_ub.cet(attestation.txid())],
Some(attestation.payout()),
),
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))),
OutgoingOrderRequest { .. }
| IncomingOrderRequest { .. }
| Accepted { .. }
| Rejected { .. }
| ContractSetup { .. }
| SetupFailed { .. } => (vec![], None),
};
CfdDetails {
tx_url_list: txs,
payout,
}
}
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct Quote { pub struct Quote {
bid: Usd, bid: Usd,

11
frontend/src/components/Types.tsx

@ -48,6 +48,17 @@ export interface Cfd {
state: State; state: State;
actions: Action[]; actions: Action[];
state_transition_timestamp: number; state_transition_timestamp: number;
details: CfdDetails;
}
export interface CfdDetails {
tx_url_list: Tx[];
payout?: number;
}
export interface Tx {
label: string;
url: string;
} }
export class State { export class State {

31
frontend/src/components/cfdtables/CfdTable.tsx

@ -4,6 +4,7 @@ import {
ChevronRightIcon, ChevronRightIcon,
ChevronUpIcon, ChevronUpIcon,
CloseIcon, CloseIcon,
ExternalLinkIcon,
RepeatIcon, RepeatIcon,
TriangleDownIcon, TriangleDownIcon,
TriangleUpIcon, TriangleUpIcon,
@ -15,6 +16,7 @@ import {
chakra, chakra,
HStack, HStack,
IconButton, IconButton,
Link,
Table as CUITable, Table as CUITable,
Tbody, Tbody,
Td, Td,
@ -22,6 +24,7 @@ import {
Thead, Thead,
Tr, Tr,
useToast, useToast,
VStack,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import React from "react"; import React from "react";
import { useAsync } from "react-async"; import { useAsync } from "react-async";
@ -89,6 +92,26 @@ export function CfdTable(
Header: "OrderId", Header: "OrderId",
accessor: "order_id", // accessor is the "key" in the data accessor: "order_id", // accessor is the "key" in the data
}, },
{
Header: "Details",
accessor: ({ details }) => {
const txs = details.tx_url_list.map((tx) => {
return (<Link href={tx.url} key={tx.url} isExternal>
{tx.label + " transaction"}
<ExternalLinkIcon mx="2px" />
</Link>);
});
return (
<Box>
<VStack>
{txs}
{details.payout && <Box>Payout: {details.payout}</Box>}
</VStack>
</Box>
);
},
},
{ {
Header: "Position", Header: "Position",
accessor: ({ position }) => { accessor: ({ position }) => {
@ -174,7 +197,7 @@ export function CfdTable(
); );
// if we mark certain columns only as hidden, they are still around and we can render them in the sub-row // if we mark certain columns only as hidden, they are still around and we can render them in the sub-row
const hiddenColumns = ["order_id", "leverage", "state_transition_timestamp"]; const hiddenColumns = ["order_id", "leverage", "state_transition_timestamp", "Details"];
return ( return (
<Table <Table
@ -233,10 +256,9 @@ function colorSchemaForAction(action: Action): string {
} }
function renderRowSubComponent(row: Row<Cfd>) { function renderRowSubComponent(row: Row<Cfd>) {
// TODO: I would show additional information here such as txids, timestamps, actions
let cells = row.allCells let cells = row.allCells
.filter((cell) => { .filter((cell) => {
return ["state_transition_timestamp"].includes(cell.column.id); return ["Details"].includes(cell.column.id);
}) })
.map((cell) => { .map((cell) => {
return cell; return cell;
@ -244,11 +266,10 @@ function renderRowSubComponent(row: Row<Cfd>) {
return ( return (
<> <>
Showing some more information here...
<HStack> <HStack>
{cells.map(cell => ( {cells.map(cell => (
<Box key={cell.column.id}> <Box key={cell.column.id}>
{cell.column.id} = {cell.render("Cell")} {cell.render("Cell")}
</Box> </Box>
))} ))}
</HStack> </HStack>

Loading…
Cancel
Save