diff --git a/swap/src/api.rs b/swap/src/api.rs index 5640c833..a87db7c5 100644 --- a/swap/src/api.rs +++ b/swap/src/api.rs @@ -167,6 +167,7 @@ pub struct Context { pub swap_lock: Arc, pub config: Config, pub tasks: Arc, + pub is_daemon: bool, } #[allow(clippy::too_many_arguments)] @@ -180,6 +181,7 @@ impl Context { debug: bool, json: bool, server_address: Option, + is_daemon: bool, ) -> Result { let data_dir = data::data_dir_from(data, is_testnet)?; let env_config = env_config_from(is_testnet); @@ -241,6 +243,7 @@ impl Context { }, swap_lock: Arc::new(SwapLock::new()), tasks: Arc::new(PendingTaskList::default()), + is_daemon, }; Ok(context) @@ -265,6 +268,7 @@ impl Context { monero_rpc_process: None, swap_lock: Arc::new(SwapLock::new()), tasks: Arc::new(PendingTaskList::default()), + is_daemon: true, } } diff --git a/swap/src/api/request.rs b/swap/src/api/request.rs index b1e9c68c..d47e9a0d 100644 --- a/swap/src/api/request.rs +++ b/swap/src/api/request.rs @@ -8,9 +8,12 @@ use crate::protocol::bob::{BobState, Swap}; use crate::protocol::{bob, State}; use crate::{bitcoin, cli, monero, rpc}; use anyhow::{bail, Context as AnyContext, Result}; +use comfy_table::Table; use libp2p::core::Multiaddr; use qrcode::render::unicode; use qrcode::QrCode; +use rust_decimal::prelude::FromPrimitive; +use rust_decimal::Decimal; use serde_json::json; use std::cmp::min; use std::convert::TryInto; @@ -638,14 +641,97 @@ impl Request { }) } Method::History => { - let swaps = context.db.all().await?; - let mut vec: Vec<(Uuid, String)> = Vec::new(); - for (swap_id, state) in swaps { - let state: BobState = state.try_into()?; - vec.push((swap_id, state.to_string())); + let mut table = Table::new(); + table.set_header(vec![ + "Swap ID", + "Start Date", + "State", + "BTC Amount", + "XMR Amount", + "Exchange Rate", + "Trading Partner Peer ID", + ]); + + let all_swaps = context.db.all().await?; + let mut json_results = Vec::new(); + + for (swap_id, state) in all_swaps { + let result: Result<_> = async { + let latest_state: BobState = state.try_into()?; + let all_states = context.db.get_states(swap_id).await?; + let state3 = all_states + .iter() + .find_map(|s| { + if let State::Bob(BobState::BtcLocked { state3, .. }) = s { + Some(state3) + } else { + None + } + }) + .context("Failed to get \"BtcLocked\" state")?; + + let swap_start_date = context.db.get_swap_start_date(swap_id).await?; + let peer_id = context.db.get_peer_id(swap_id).await?; + let btc_amount = state3.tx_lock.lock_amount(); + let xmr_amount = state3.xmr; + let exchange_rate = Decimal::from_f64(btc_amount.to_btc()) + .ok_or_else(|| { + anyhow::anyhow!("Failed to convert BTC amount to Decimal") + })? + .checked_div(xmr_amount.as_xmr()) + .ok_or_else(|| anyhow::anyhow!("Division by zero or overflow"))?; + let exchange_rate = format!("{} XMR/BTC", exchange_rate.round_dp(8)); + + let swap_data = json!({ + "swapId": swap_id.to_string(), + "startDate": swap_start_date.to_string(), + "state": latest_state.to_string(), + "btcAmount": btc_amount.to_string(), + "xmrAmount": xmr_amount.to_string(), + "exchangeRate": exchange_rate, + "tradingPartnerPeerId": peer_id.to_string() + }); + + if context.config.json { + tracing::info!( + swap_id = %swap_id, + swap_start_date = %swap_start_date, + latest_state = %latest_state, + btc_amount = %btc_amount, + xmr_amount = %xmr_amount, + exchange_rate = %exchange_rate, + trading_partner_peer_id = %peer_id, + "Found swap in database" + ); + } else { + table.add_row(vec![ + swap_id.to_string(), + swap_start_date.to_string(), + latest_state.to_string(), + btc_amount.to_string(), + xmr_amount.to_string(), + exchange_rate, + peer_id.to_string(), + ]); + } + + Ok(swap_data) + } + .await; + + match result { + Ok(swap_data) => json_results.push(swap_data), + Err(e) => { + tracing::error!(swap_id = %swap_id, error = %e, "Failed to get swap details") + } + } } - Ok(json!({ "swaps": vec })) + if !context.config.json && !context.is_daemon { + println!("{}", table); + } + + Ok(json!({"swaps": json_results})) } Method::GetRawStates => { let raw_history = context.db.raw_all().await?; diff --git a/swap/src/asb/command.rs b/swap/src/asb/command.rs index f22e1500..260b5e9d 100644 --- a/swap/src/asb/command.rs +++ b/swap/src/asb/command.rs @@ -33,13 +33,13 @@ where env_config: env_config(testnet), cmd: Command::Start { resume_only }, }, - RawCommand::History => Arguments { + RawCommand::History { only_unfinished } => Arguments { testnet, json, disable_timestamp, config_path: config_path(config, testnet)?, env_config: env_config(testnet), - cmd: Command::History, + cmd: Command::History { only_unfinished }, }, RawCommand::WithdrawBtc { amount, address } => Arguments { testnet, @@ -195,7 +195,9 @@ pub enum Command { Start { resume_only: bool, }, - History, + History { + only_unfinished: bool, + }, Config, WithdrawBtc { amount: Option, @@ -269,7 +271,13 @@ pub enum RawCommand { resume_only: bool, }, #[structopt(about = "Prints swap-id and the state of each swap ever made.")] - History, + History { + #[structopt( + long = "only-unfinished", + help = "If set, only unfinished swaps will be printed." + )] + only_unfinished: bool, + }, #[structopt(about = "Prints the current config")] Config, #[structopt(about = "Allows withdrawing BTC from the internal Bitcoin wallet.")] @@ -387,7 +395,9 @@ mod tests { disable_timestamp: false, config_path: default_mainnet_conf_path, env_config: mainnet_env_config, - cmd: Command::History, + cmd: Command::History { + only_unfinished: false, + }, }; let args = parse_args(raw_ars).unwrap(); assert_eq!(expected_args, args); @@ -570,7 +580,9 @@ mod tests { disable_timestamp: false, config_path: default_testnet_conf_path, env_config: testnet_env_config, - cmd: Command::History, + cmd: Command::History { + only_unfinished: false, + }, }; let args = parse_args(raw_ars).unwrap(); assert_eq!(expected_args, args); diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 0f2f13b5..66630b88 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -18,6 +18,8 @@ use libp2p::core::multiaddr::Protocol; use libp2p::core::Multiaddr; use libp2p::swarm::AddressScore; 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,7 +35,9 @@ use swap::common::check_latest_version; 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::State; use swap::seed::Seed; use swap::tor::AuthenticatedClient; use swap::{asb, bitcoin, kraken, monero, tor}; @@ -224,19 +228,87 @@ async fn main() -> Result<()> { event_loop.run().await; } - Command::History => { + Command::History { only_unfinished } => { let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadOnly).await?; + let mut table: Table = Table::new(); - let mut table = Table::new(); + table.set_header(vec![ + "Swap ID", + "Start Date", + "State", + "BTC Amount", + "XMR Amount", + "Exchange Rate", + "Trading Partner Peer ID", + "Completed", + ]); - table.set_header(vec!["SWAP ID", "STATE"]); + let all_swaps = db.all().await?; + for (swap_id, state) in all_swaps { + if let Err(e) = async { + let latest_state: AliceState = state.try_into()?; + let is_completed = is_complete(&latest_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()]); + if only_unfinished && is_completed { + return Ok::<_, anyhow::Error>(()); + } + + let all_states = db.get_states(swap_id).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 swap_start_date = db.get_swap_start_date(swap_id).await?; + let peer_id = db.get_peer_id(swap_id).await?; + + let exchange_rate = Decimal::from_f64(state3.btc.to_btc()) + .ok_or_else(|| anyhow::anyhow!("Failed to convert BTC amount to Decimal"))? + .checked_div(state3.xmr.as_xmr()) + .ok_or_else(|| anyhow::anyhow!("Division by zero or overflow"))?; + let exchange_rate = format!("{} XMR/BTC", exchange_rate.round_dp(8)); + + if json { + tracing::info!( + swap_id = %swap_id, + swap_start_date = %swap_start_date, + latest_state = %latest_state, + btc_amount = %state3.btc, + xmr_amount = %state3.xmr, + exchange_rate = %exchange_rate, + trading_partner_peer_id = %peer_id, + completed = is_completed, + "Found swap in database" + ); + } else { + table.add_row(vec![ + swap_id.to_string(), + swap_start_date.to_string(), + latest_state.to_string(), + state3.btc.to_string(), + state3.xmr.to_string(), + exchange_rate, + peer_id.to_string(), + is_completed.to_string(), + ]); + } + + Ok::<_, anyhow::Error>(()) + } + .await + { + 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)?; diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index 4881d94e..6c460f82 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -78,6 +78,7 @@ where debug, json, None, + false, ) .await?; @@ -100,14 +101,16 @@ where let request = Request::new(Method::History); let context = - Context::build(None, None, None, data, is_testnet, debug, json, None).await?; + Context::build(None, None, None, data, is_testnet, debug, json, None, false) + .await?; (context, request) } CliCommand::Config => { let request = Request::new(Method::Config); let context = - Context::build(None, None, None, data, is_testnet, debug, json, None).await?; + Context::build(None, None, None, data, is_testnet, debug, json, None, false) + .await?; (context, request) } CliCommand::Balance { bitcoin } => { @@ -124,6 +127,7 @@ where debug, json, None, + false, ) .await?; (context, request) @@ -145,6 +149,7 @@ where debug, json, server_address, + true, ) .await?; (context, request) @@ -166,6 +171,7 @@ where debug, json, None, + false, ) .await?; (context, request) @@ -187,6 +193,7 @@ where debug, json, None, + false, ) .await?; (context, request) @@ -207,6 +214,7 @@ where debug, json, None, + false, ) .await?; (context, request) @@ -217,8 +225,18 @@ where } => { let request = Request::new(Method::ListSellers { rendezvous_point }); - let context = - Context::build(None, None, Some(tor), data, is_testnet, debug, json, None).await?; + let context = Context::build( + None, + None, + Some(tor), + data, + is_testnet, + debug, + json, + None, + false, + ) + .await?; (context, request) } @@ -234,6 +252,7 @@ where debug, json, None, + false, ) .await?; (context, request) @@ -244,7 +263,8 @@ where let request = Request::new(Method::MoneroRecovery { swap_id }); let context = - Context::build(None, None, None, data, is_testnet, debug, json, None).await?; + Context::build(None, None, None, data, is_testnet, debug, json, None, false) + .await?; (context, request) } diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 8205e75f..e5dd9e80 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -142,6 +142,14 @@ impl Amount { Decimal::from(self.as_piconero()) } + pub fn as_xmr(&self) -> Decimal { + let mut decimal = Decimal::from(self.0); + decimal + .set_scale(12) + .expect("12 is smaller than max precision of 28"); + decimal + } + fn from_decimal(amount: Decimal) -> Result { let piconeros_dec = amount.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64")); @@ -184,11 +192,8 @@ impl From for u64 { impl fmt::Display for Amount { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut decimal = Decimal::from(self.0); - decimal - .set_scale(12) - .expect("12 is smaller than max precision of 28"); - write!(f, "{} XMR", decimal) + let xmr_value = self.as_xmr(); + write!(f, "{} XMR", xmr_value) } } diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index f0acab23..7627a529 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 3848a2e1..14f718d3 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -440,7 +440,7 @@ where }) } -pub(crate) fn is_complete(state: &AliceState) -> bool { +pub fn is_complete(state: &AliceState) -> bool { matches!( state, AliceState::XmrRefunded diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index 8fe5ca32..04e7778e 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -369,7 +369,7 @@ pub struct State3 { S_a_monero: monero::PublicKey, S_a_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, - xmr: monero::Amount, + pub xmr: monero::Amount, pub cancel_timelock: CancelTimelock, punish_timelock: PunishTimelock, refund_address: bitcoin::Address, diff --git a/swap/tests/rpc.rs b/swap/tests/rpc.rs index 5dc640d4..553ccf46 100644 --- a/swap/tests/rpc.rs +++ b/swap/tests/rpc.rs @@ -103,13 +103,26 @@ mod test { let (client, _, _) = setup_daemon(harness_ctx).await; - let response: HashMap> = client + let response: HashMap> = client .request("get_history", ObjectParams::new()) .await .unwrap(); - let swaps: Vec<(Uuid, String)> = vec![(bob_swap_id, "btc is locked".to_string())]; - assert_eq!(response, HashMap::from([("swaps".to_string(), swaps)])); + let swaps = response.get("swaps").unwrap(); + assert_eq!(swaps.len(), 1); + + assert_has_keys_serde( + swaps[0].as_object().unwrap(), + &[ + "swapId", + "startDate", + "state", + "btcAmount", + "xmrAmount", + "exchangeRate", + "tradingPartnerPeerId", + ], + ); let response: HashMap>> = client .request("get_raw_states", ObjectParams::new())