Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(asb): Print more information when history command is invoked #218

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- ASB: The `history` command will now display additional information about each swap such as the amounts involved, the current state and the txid of the Bitcoin lock transaction.

## [1.0.0-rc.10] - 2024-12-05

- GUI: Release .deb installer for Debian-based systems
Expand Down
21 changes: 15 additions & 6 deletions swap/src/asb/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ where
env_config: env_config(testnet),
cmd: Command::Start { resume_only },
},
RawCommand::History => Arguments {
RawCommand::History { only_unfinished } => Arguments {
testnet,
json,
config_path: config_path(config, testnet)?,
env_config: env_config(testnet),
cmd: Command::History,
cmd: Command::History { only_unfinished },
},
RawCommand::Logs {
logs_dir: dir_path,
Expand Down Expand Up @@ -197,7 +197,9 @@ pub enum Command {
Start {
resume_only: bool,
},
History,
History {
only_unfinished: bool,
},
Config,
Logs {
logs_dir: Option<PathBuf>,
Expand Down Expand Up @@ -295,7 +297,10 @@ pub enum RawCommand {
swap_id: Option<Uuid>,
},
#[structopt(about = "Prints swap-id and the state of each swap ever made.")]
History,
History {
#[structopt(long = "only-unfinished", help = "Only print in progress swaps")]
only_unfinished: bool,
},
#[structopt(about = "Prints the current config")]
Config,
#[structopt(about = "Allows withdrawing BTC from the internal Bitcoin wallet.")]
Expand Down Expand Up @@ -411,7 +416,9 @@ mod tests {
json: 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);
Expand Down Expand Up @@ -586,7 +593,9 @@ mod tests {
json: 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);
Expand Down
157 changes: 143 additions & 14 deletions swap/src/bin/asb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
use anyhow::{bail, Context, Result};
use comfy_table::Table;
use libp2p::Swarm;
use rust_decimal::prelude::FromPrimitive;
use rust_decimal::Decimal;
use std::convert::TryInto;
use std::env;
use std::sync::Arc;
Expand All @@ -31,10 +33,13 @@ 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::{bitcoin, kraken, monero};
use tracing_subscriber::filter::LevelFilter;
use uuid::Uuid;

const DEFAULT_WALLET_NAME: &str = "asb-wallet";

Expand Down Expand Up @@ -95,9 +100,11 @@ pub async fn main() -> Result<()> {
let seed =
Seed::from_file_or_generate(&config.data.dir).expect("Could not retrieve/initialize seed");

let db_file = config.data.dir.join("sqlite");

match cmd {
Command::Start { resume_only } => {
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite, None).await?;
let db = open_db(db_file, AccessMode::ReadWrite, None).await?;

// check and warn for duplicate rendezvous points
let mut rendezvous_addrs = config.network.rendezvous_point.clone();
Expand Down Expand Up @@ -228,19 +235,49 @@ pub async fn main() -> Result<()> {

event_loop.run().await;
}
Command::History => {
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadOnly, None).await?;

Command::History { only_unfinished } => {
let db = open_db(db_file, AccessMode::ReadOnly, None).await?;
let mut table = Table::new();

table.set_header(vec!["SWAP ID", "STATE"]);
table.set_header(vec![
"Swap ID",
"Start Date",
"State",
"Bitcoin Lock TxId",
"BTC Amount",
"XMR Amount",
"Exchange Rate",
"Taker Peer ID",
"Completed",
]);

let all_swaps = db.all().await?;
for (swap_id, state) in all_swaps {
let state: AliceState = state
.try_into()
.expect("Alice database only has Alice states");

if only_unfinished && is_complete(&state) {
continue;
}

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()]);
match SwapDetails::from_db_state(swap_id.clone(), state, &db).await {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're calling from_db_state for every swap, but from_db_state iterates through all swaps itself. That's O(n^2), so we should probably move that into a single database query

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from_db_state does not iterate through all swaps. It iterates through all saved states of a particular swap.

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)?;
Expand Down Expand Up @@ -289,7 +326,7 @@ pub async fn main() -> Result<()> {
tracing::info!(%bitcoin_balance, %monero_balance, "Current balance");
}
Command::Cancel { swap_id } => {
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite, None).await?;
let db = open_db(db_file, AccessMode::ReadWrite, None).await?;

let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;

Expand All @@ -298,7 +335,7 @@ pub async fn main() -> Result<()> {
tracing::info!("Cancel transaction successfully published with id {}", txid);
}
Command::Refund { swap_id } => {
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite, None).await?;
let db = open_db(db_file, AccessMode::ReadWrite, None).await?;

let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
let monero_wallet = init_monero_wallet(&config, env_config).await?;
Expand All @@ -314,7 +351,7 @@ pub async fn main() -> Result<()> {
tracing::info!("Monero successfully refunded");
}
Command::Punish { swap_id } => {
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite, None).await?;
let db = open_db(db_file, AccessMode::ReadWrite, None).await?;

let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;

Expand All @@ -323,7 +360,7 @@ pub async fn main() -> Result<()> {
tracing::info!("Punish transaction successfully published with id {}", txid);
}
Command::SafelyAbort { swap_id } => {
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite, None).await?;
let db = open_db(db_file, AccessMode::ReadWrite, None).await?;

safely_abort(swap_id, db).await?;

Expand All @@ -333,7 +370,7 @@ pub async fn main() -> Result<()> {
swap_id,
do_not_await_finality,
} => {
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite, None).await?;
let db = open_db(db_file, AccessMode::ReadWrite, None).await?;

let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;

Expand Down Expand Up @@ -393,3 +430,95 @@ async fn init_monero_wallet(

Ok(wallet)
}

/// 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,
latest_state: AliceState,
db: &Arc<dyn Database + Send + Sync>,
) -> Result<Self> {
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,
})
}

fn calculate_exchange_rate(btc: bitcoin::Amount, xmr: monero::Amount) -> Result<String> {
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"))?;

let rate = btc_decimal
.checked_div(xmr_decimal)
.ok_or_else(|| anyhow::anyhow!("Division by zero or overflow"))?;

Ok(format!("{} XMR/BTC", rate.round_dp(8)))
}

fn to_table_row(&self) -> Vec<String> {
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(),
]
}

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,
taker_peer_id = %self.peer_id,
completed = self.completed,
"Found swap in database"
);
}
}
5 changes: 5 additions & 0 deletions swap/src/monero.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bitcoin::Amount> {
Expand Down
4 changes: 2 additions & 2 deletions swap/src/protocol/alice/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion swap/src/protocol/alice/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ where
})
}

pub(crate) fn is_complete(state: &AliceState) -> bool {
pub fn is_complete(state: &AliceState) -> bool {
matches!(
state,
AliceState::XmrRefunded
Expand Down
Loading