diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 9ca4da0b4..14fc74562 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -17,6 +17,8 @@ use comfy_table::Table; use libp2p::core::multiaddr::Protocol; use libp2p::core::Multiaddr; use libp2p::Swarm; +use rust_decimal::prelude::FromPrimitive; +use rust_decimal::Decimal; use std::convert::TryInto; use std::env; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; @@ -33,11 +35,14 @@ use swap::common::{self, get_logs, warn_if_outdated}; use swap::database::{open_db, AccessMode}; use swap::network::rendezvous::XmrBtcNamespace; use swap::network::swarm; +use swap::protocol::alice::swap::is_complete; use swap::protocol::alice::{run, AliceState}; +use swap::protocol::{Database, State}; use swap::seed::Seed; use swap::tor::AuthenticatedClient; use swap::{bitcoin, kraken, monero, tor}; use tracing_subscriber::filter::LevelFilter; +use uuid::Uuid; const DEFAULT_WALLET_NAME: &str = "asb-wallet"; @@ -228,17 +233,39 @@ pub async fn main() -> Result<()> { } Command::History => { let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadOnly, None).await?; - let mut table = Table::new(); - table.set_header(vec!["SWAP ID", "STATE"]); - - for (swap_id, state) in db.all().await? { - let state: AliceState = state.try_into()?; - table.add_row(vec![swap_id.to_string(), state.to_string()]); + table.set_header(vec![ + "Swap ID", + "Start Date", + "State", + "Bitcoin Lock TxId", + "BTC Amount", + "XMR Amount", + "Exchange Rate", + "Trading Partner Peer ID", + "Completed", + ]); + + let all_swaps = db.all().await?; + for (swap_id, state) in all_swaps { + match SwapDetails::from_db_state(swap_id.clone(), state, &db).await { + Ok(details) => { + if json { + details.log_info(); + } else { + table.add_row(details.to_table_row()); + } + } + Err(e) => { + tracing::error!(swap_id = %swap_id, error = %e, "Failed to get swap details"); + } + } } - println!("{}", table); + if !json { + println!("{}", table); + } } Command::Config => { let config_json = serde_json::to_string_pretty(&config)?; @@ -392,45 +419,96 @@ async fn init_monero_wallet( Ok(wallet) } -/// Registers a hidden service for each network. -/// Note: Once ac goes out of scope, the services will be de-registered. -async fn register_tor_services( - networks: Vec, - tor_client: tor::Client, - seed: &Seed, -) -> Result { - let mut ac = tor_client.into_authenticated_client().await?; - - let hidden_services_details = networks - .iter() - .flat_map(|network| { - network.iter().map(|protocol| match protocol { - Protocol::Tcp(port) => Some(( - port, - SocketAddr::new(IpAddr::from(Ipv4Addr::new(127, 0, 0, 1)), port), - )), - _ => { - // We only care for Tcp for now. - None - } +/// This struct is used to extract swap details from the database and print them in a table format +#[derive(Debug)] +struct SwapDetails { + swap_id: String, + start_date: String, + state: String, + btc_lock_txid: String, + btc_amount: String, + xmr_amount: String, + exchange_rate: String, + peer_id: String, + completed: bool, +} + +impl SwapDetails { + async fn from_db_state( + swap_id: Uuid, + state: State, + db: &Arc, + ) -> Result { + let latest_state: AliceState = state.try_into()?; + let completed = is_complete(&latest_state); + + let all_states = db.get_states(swap_id.clone()).await?; + let state3 = all_states + .iter() + .find_map(|s| match s { + State::Alice(AliceState::BtcLockTransactionSeen { state3 }) => Some(state3), + _ => None, }) + .context("Failed to get \"BtcLockTransactionSeen\" state")?; + + let exchange_rate = Self::calculate_exchange_rate(state3.btc, state3.xmr)?; + let start_date = db.get_swap_start_date(swap_id.clone()).await?; + let btc_lock_txid = state3.tx_lock.txid(); + let peer_id = db.get_peer_id(swap_id.clone()).await?; + + Ok(Self { + swap_id: swap_id.to_string(), + start_date: start_date.to_string(), + state: latest_state.to_string(), + btc_lock_txid: btc_lock_txid.to_string(), + btc_amount: state3.btc.to_string(), + xmr_amount: state3.xmr.to_string(), + exchange_rate, + peer_id: peer_id.to_string(), + completed, }) - .flatten() - .collect::>(); + } - let key = seed.derive_torv3_key(); + fn calculate_exchange_rate(btc: bitcoin::Amount, xmr: monero::Amount) -> Result { + let btc_decimal = Decimal::from_f64(btc.to_btc()) + .ok_or_else(|| anyhow::anyhow!("Failed to convert BTC amount to Decimal"))?; + let xmr_decimal = Decimal::from_f64(xmr.as_xmr()) + .ok_or_else(|| anyhow::anyhow!("Failed to convert XMR amount to Decimal"))?; - ac.add_services(&hidden_services_details, &key).await?; + let rate = btc_decimal + .checked_div(xmr_decimal) + .ok_or_else(|| anyhow::anyhow!("Division by zero or overflow"))?; - let onion_address = key - .public() - .get_onion_address() - .get_address_without_dot_onion(); + Ok(format!("{} XMR/BTC", rate.round_dp(8))) + } - hidden_services_details.iter().for_each(|(port, _)| { - let onion_address = format!("/onion3/{}:{}", onion_address, port); - tracing::info!(%onion_address, "Successfully created hidden service"); - }); + fn to_table_row(&self) -> Vec { + vec![ + self.swap_id.clone(), + self.start_date.clone(), + self.state.clone(), + self.btc_lock_txid.clone(), + self.btc_amount.clone(), + self.xmr_amount.clone(), + self.exchange_rate.clone(), + self.peer_id.clone(), + self.completed.to_string(), + ] + } - Ok(ac) + fn log_info(&self) { + tracing::info!( + swap_id = %self.swap_id, + swap_start_date = %self.start_date, + latest_state = %self.state, + btc_lock_txid = %self.btc_lock_txid, + btc_amount = %self.btc_amount, + xmr_amount = %self.xmr_amount, + exchange_rate = %self.exchange_rate, + trading_partner_peer_id = %self.peer_id, + completed = self.completed, + "Found swap in database" + ); + } +>>>>>>> 0900fb9f (feat(asb): Enhance history command) } diff --git a/swap/src/monero.rs b/swap/src/monero.rs index f23254ff9..391f65a90 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -109,6 +109,11 @@ impl Amount { self.0 } + /// Return Monero Amount as XMR. + pub fn as_xmr(&self) -> f64 { + self.0 as f64 / PICONERO_OFFSET as f64 + } + /// Calculate the maximum amount of Bitcoin that can be bought at a given /// asking price for this amount of Monero including the median fee. pub fn max_bitcoin_for_price(&self, ask_price: bitcoin::Amount) -> Option { diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index f0acab232..7627a5295 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -384,8 +384,8 @@ pub struct State3 { S_b_bitcoin: bitcoin::PublicKey, pub v: monero::PrivateViewKey, #[serde(with = "::bitcoin::util::amount::serde::as_sat")] - btc: bitcoin::Amount, - xmr: monero::Amount, + pub btc: bitcoin::Amount, + pub xmr: monero::Amount, pub cancel_timelock: CancelTimelock, pub punish_timelock: PunishTimelock, refund_address: bitcoin::Address, diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 907cfe818..547a353df 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -453,7 +453,7 @@ where }) } -pub(crate) fn is_complete(state: &AliceState) -> bool { +pub fn is_complete(state: &AliceState) -> bool { matches!( state, AliceState::XmrRefunded