Browse Source

Transaction ids and payout in the UI

The daemon adds `CfdDetails` that contains:
* `Vec<TxUrl>` - relevant transaction-id URLs to block explorer based on the state and bitcoin network
* `Option<bitcoin::Amount>` - The payout if we already have an attestation
* Display tx-urls and payout when unfolding the cfd row in table
refactor/no-log-handler
Daniel Karzel 3 years ago
parent
commit
5beb401e79
No known key found for this signature in database GPG Key ID: 30C3FC2E438ADB6E
  1. 4
      daemon/src/maker.rs
  2. 47
      daemon/src/model/cfd.rs
  3. 34
      daemon/src/routes_maker.rs
  4. 36
      daemon/src/routes_taker.rs
  5. 4
      daemon/src/taker.rs
  6. 141
      daemon/src/to_sse_event.rs
  7. 11
      frontend/src/components/Types.tsx
  8. 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",

47
daemon/src/model/cfd.rs

@ -271,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
///
@ -310,17 +310,16 @@ 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 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))
(output.script_pubkey == our_script_pubkey).then(|| Amount::from_sat(output.value))
})
.unwrap_or_default();
@ -336,6 +335,14 @@ impl Attestation {
pub fn price(&self) -> Usd {
Usd(Decimal::from(self.price))
}
pub fn txid(&self) -> Txid {
self.txid
}
pub fn payout(&self) -> Amount {
self.payout
}
}
impl From<Attestation> for oracle::Attestation {
@ -742,18 +749,15 @@ 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(_) => {
@ -1540,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

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",

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