feat: Enhance history command with more swap details (#1725)

This commit is contained in:
binarybaron 2024-08-01 18:35:03 +02:00 committed by GitHub
parent b18ba95e8c
commit cc854be8f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 248 additions and 36 deletions

View File

@ -167,6 +167,7 @@ pub struct Context {
pub swap_lock: Arc<SwapLock>,
pub config: Config,
pub tasks: Arc<PendingTaskList>,
pub is_daemon: bool,
}
#[allow(clippy::too_many_arguments)]
@ -180,6 +181,7 @@ impl Context {
debug: bool,
json: bool,
server_address: Option<SocketAddr>,
is_daemon: bool,
) -> Result<Context> {
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,
}
}

View File

@ -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?;

View File

@ -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<Amount>,
@ -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);

View File

@ -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)?;

View File

@ -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)
}

View File

@ -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<Self> {
let piconeros_dec =
amount.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));
@ -184,11 +192,8 @@ impl From<Amount> 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)
}
}

View File

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

View File

@ -440,7 +440,7 @@ where
})
}
pub(crate) fn is_complete(state: &AliceState) -> bool {
pub fn is_complete(state: &AliceState) -> bool {
matches!(
state,
AliceState::XmrRefunded

View File

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

View File

@ -103,13 +103,26 @@ mod test {
let (client, _, _) = setup_daemon(harness_ctx).await;
let response: HashMap<String, Vec<(Uuid, String)>> = client
let response: HashMap<String, Vec<Value>> = 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<String, HashMap<Uuid, Vec<Value>>> = client
.request("get_raw_states", ObjectParams::new())