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. 207
      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 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(
opts.network.electrum(),
@ -189,6 +190,7 @@ async fn main() -> Result<()> {
.manage(update_cfd_feed_receiver)
.manage(auth_password)
.manage(quote_updates)
.manage(bitcoin_network)
.attach(Db::init())
.attach(AdHoc::try_on_ignite(
"SQL migrations",

16
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

207
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
@ -268,7 +271,7 @@ pub enum CfdState {
///
/// This state applies to taker and maker.
/// 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
///
@ -284,17 +287,61 @@ pub enum CfdState {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Attestation {
pub id: OracleEventId,
pub price: u64,
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 {
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<SecretKey>,
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)> {
// 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,
@ -694,25 +749,29 @@ impl Cfd {
}
}
monitor::Event::RefundFinality(_) => {
if let MustRefund { .. } = self.state.clone() {
} else {
tracing::debug!(
"Was in unexpected state {}, jumping ahead to Refunded",
self.state
);
}
let dlc = self
.dlc()
.context("No dlc available when reaching refund finality")?;
Refunded {
common: CfdStateCommon {
transition_timestamp: SystemTime::now(),
},
dlc,
}
}
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::CetFinality(_) => Closed {
common: CfdStateCommon {
transition_timestamp: SystemTime::now(),
},
},
monitor::Event::RevokedTransactionFound(_) => {
todo!("Punish bad counterparty")
}
@ -790,21 +849,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 +944,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 +1044,62 @@ impl Cfd {
pub fn role(&self) -> Role {
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)]
@ -1432,6 +1544,21 @@ impl Dlc {
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

11
daemon/src/monitor.rs

@ -91,8 +91,7 @@ where
actor.monitor_commit_refund_timelock(&params, cfd.order.id);
actor.monitor_refund_finality(&params,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(&params, cfd.order.id);
actor.monitor_refund_finality(&params,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());

34
daemon/src/routes_maker.rs

@ -5,6 +5,7 @@ use crate::routes::EmbeddedFileExt;
use crate::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent};
use crate::{bitmex_price_feed, maker_cfd};
use anyhow::Result;
use bdk::bitcoin::Network;
use rocket::http::{ContentType, Header, Status};
use rocket::response::stream::EventStream;
use rocket::response::{status, Responder};
@ -25,6 +26,7 @@ pub async fn maker_feed(
rx_wallet: &State<watch::Receiver<WalletInfo>>,
rx_quote: &State<watch::Receiver<bitmex_price_feed::Quote>>,
rx_settlements: &State<watch::Receiver<UpdateCfdProposals>>,
network: &State<Network>,
_auth: Authenticated,
) -> EventStream![] {
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_quote = rx_quote.inner().clone();
let mut rx_settlements = rx_settlements.inner().clone();
let network = *network.inner();
EventStream! {
let wallet_info = rx_wallet.borrow().clone();
@ -43,7 +46,12 @@ pub async fn maker_feed(
let quote = rx_quote.borrow().clone();
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{
select! {
@ -56,15 +64,33 @@ pub async fn maker_feed(
yield order.to_sse_event();
}
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() => {
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() => {
let quote = rx_quote.borrow().clone();
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::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent};
use crate::{bitmex_price_feed, taker_cfd};
use bdk::bitcoin::Amount;
use bdk::bitcoin::{Amount, Network};
use rocket::http::{ContentType, Status};
use rocket::response::stream::EventStream;
use rocket::response::{status, Responder};
@ -24,12 +24,14 @@ pub async fn feed(
rx_wallet: &State<watch::Receiver<WalletInfo>>,
rx_quote: &State<watch::Receiver<bitmex_price_feed::Quote>>,
rx_settlements: &State<watch::Receiver<UpdateCfdProposals>>,
network: &State<Network>,
) -> 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();
let network = *network.inner();
EventStream! {
let wallet_info = rx_wallet.borrow().clone();
@ -41,7 +43,13 @@ pub async fn feed(
let quote = rx_quote.borrow().clone();
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{
select! {
@ -54,15 +62,33 @@ pub async fn feed(
yield order.to_sse_event();
}
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() => {
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() => {
let quote = rx_quote.borrow().clone();
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 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(
opts.network.electrum(),
@ -188,6 +189,7 @@ async fn main() -> Result<()> {
.manage(wallet_feed_receiver)
.manage(update_feed_receiver)
.manage(quote_updates)
.manage(bitcoin_network)
.attach(Db::init())
.attach(AdHoc::try_on_ignite(
"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,
};
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

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::{bitmex_price_feed, model};
use bdk::bitcoin::{Amount, SignedAmount};
use bdk::bitcoin::{Amount, Network, SignedAmount, Txid};
use rocket::request::FromParam;
use rocket::response::stream::Event;
use rust_decimal::Decimal;
@ -32,6 +34,69 @@ pub struct Cfd {
pub state: CfdState,
pub actions: Vec<CfdAction>,
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)]
@ -109,6 +174,7 @@ pub struct CfdsWithAuxData {
pub cfds: Vec<model::cfd::Cfd>,
pub current_price: Usd,
pub pending_proposals: UpdateCfdProposals,
pub network: Network,
}
impl CfdsWithAuxData {
@ -117,6 +183,7 @@ impl CfdsWithAuxData {
rx_quote: &watch::Receiver<bitmex_price_feed::Quote>,
rx_updates: &watch::Receiver<UpdateCfdProposals>,
role: Role,
network: Network,
) -> Self {
let quote = rx_quote.borrow().clone();
let current_price = match role {
@ -130,6 +197,7 @@ impl CfdsWithAuxData {
cfds: rx_cfds.borrow().clone(),
current_price,
pending_proposals,
network,
}
}
}
@ -138,6 +206,7 @@ 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;
let network = self.network;
let cfds = self
.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
// 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),
}
})
.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)]
pub struct Quote {
bid: Usd,

11
frontend/src/components/Types.tsx

@ -48,6 +48,17 @@ export interface Cfd {
state: State;
actions: Action[];
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 {

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

@ -4,6 +4,7 @@ import {
ChevronRightIcon,
ChevronUpIcon,
CloseIcon,
ExternalLinkIcon,
RepeatIcon,
TriangleDownIcon,
TriangleUpIcon,
@ -15,6 +16,7 @@ import {
chakra,
HStack,
IconButton,
Link,
Table as CUITable,
Tbody,
Td,
@ -22,6 +24,7 @@ import {
Thead,
Tr,
useToast,
VStack,
} from "@chakra-ui/react";
import React from "react";
import { useAsync } from "react-async";
@ -89,6 +92,26 @@ export function CfdTable(
Header: "OrderId",
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",
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
const hiddenColumns = ["order_id", "leverage", "state_transition_timestamp"];
const hiddenColumns = ["order_id", "leverage", "state_transition_timestamp", "Details"];
return (
<Table
@ -233,10 +256,9 @@ function colorSchemaForAction(action: Action): string {
}
function renderRowSubComponent(row: Row<Cfd>) {
// TODO: I would show additional information here such as txids, timestamps, actions
let cells = row.allCells
.filter((cell) => {
return ["state_transition_timestamp"].includes(cell.column.id);
return ["Details"].includes(cell.column.id);
})
.map((cell) => {
return cell;
@ -244,11 +266,10 @@ function renderRowSubComponent(row: Row<Cfd>) {
return (
<>
Showing some more information here...
<HStack>
{cells.map(cell => (
<Box key={cell.column.id}>
{cell.column.id} = {cell.render("Cell")}
{cell.render("Cell")}
</Box>
))}
</HStack>

Loading…
Cancel
Save