diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml index 74ff34ba..9b9c849d 100644 --- a/.github/workflows/draft-new-release.yml +++ b/.github/workflows/draft-new-release.yml @@ -20,7 +20,7 @@ jobs: run: git checkout -b release/${{ github.event.inputs.version }} - name: Update changelog - uses: thomaseizinger/keep-a-changelog-new-release@3.0.0 + uses: thomaseizinger/keep-a-changelog-new-release@3.1.0 with: version: ${{ github.event.inputs.version }} changelogPath: CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 5152d646..bb8e53e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.13.4] - 2024-07-25 + - ASB: The `history` command can now be used while the asb is running. +- ASB: Retry locking of Monero if it fails on first attempt ## [0.13.3] - 2024-07-15 @@ -372,7 +375,8 @@ It is possible to migrate critical data from the old db to the sqlite but there - Fixed an issue where Alice would not verify if Bob's Bitcoin lock transaction is semantically correct, i.e. pays the agreed upon amount to an output owned by both of them. Fixing this required a **breaking change** on the network layer and hence old versions are not compatible with this version. -[unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.3...HEAD +[unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.4...HEAD +[0.13.4]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.3...0.13.4 [0.13.3]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.2...0.13.3 [0.13.2]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.1...0.13.2 [0.13.1]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.0...0.13.1 diff --git a/Cargo.lock b/Cargo.lock index 1b58ff6e..682aa86f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -752,7 +752,7 @@ dependencies = [ "nom", "pathdiff", "serde", - "toml 0.8.15", + "toml 0.8.19", ] [[package]] @@ -1783,6 +1783,7 @@ dependencies = [ "http 1.0.0", "http-body 1.0.0", "httparse", + "httpdate", "itoa", "pin-project-lite 0.2.13", "smallvec", @@ -2602,14 +2603,19 @@ dependencies = [ [[package]] name = "mockito" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f6e023aa5bdf392aa06c78e4a4e6d498baab5138d0c993503350ebbc37bf1e" +checksum = "09b34bd91b9e5c5b06338d392463e1318d683cf82ec3d3af4014609be6e2108d" dependencies = [ "assert-json-diff", + "bytes", "colored", - "futures-core", - "hyper 0.14.28", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.4.1", + "hyper-util", "log", "rand 0.8.3", "regex", @@ -4016,9 +4022,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.204" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" dependencies = [ "serde_derive", ] @@ -4045,9 +4051,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", @@ -4056,20 +4062,21 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.118" +version = "1.0.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" +checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -4536,7 +4543,7 @@ checksum = "8049cf85f0e715d6af38dde439cb0ccb91f67fb9f5f63c80f8b43e48356e1a3f" [[package]] name = "swap" -version = "0.13.3" +version = "0.13.4" dependencies = [ "anyhow", "async-compression", @@ -4602,7 +4609,7 @@ dependencies = [ "tokio-tar", "tokio-tungstenite", "tokio-util", - "toml 0.8.15", + "toml 0.8.19", "torut", "tracing", "tracing-appender", @@ -4657,14 +4664,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if 1.0.0", "fastrand", + "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4864,9 +4872,9 @@ dependencies = [ [[package]] name = "tokio-socks" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" dependencies = [ "either", "futures-util", @@ -4942,9 +4950,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.15" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", @@ -4954,18 +4962,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.16" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap 2.1.0", "serde", @@ -5640,7 +5648,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -5660,17 +5677,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -5681,9 +5699,9 @@ checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -5699,9 +5717,9 @@ checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -5717,9 +5735,15 @@ checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -5735,9 +5759,9 @@ checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -5753,9 +5777,9 @@ checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -5765,9 +5789,9 @@ checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -5783,15 +5807,15 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.5" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 4fd43a2e..8354ec0a 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swap" -version = "0.13.3" +version = "0.13.4" authors = [ "The COMIT guys " ] edition = "2021" description = "XMR/BTC trustless atomic swaps." @@ -83,7 +83,7 @@ bitcoin-harness = { git = "https://github.com/delta1/bitcoin-harness-rs.git", re get-port = "3" hyper = "1.4" jsonrpsee = { version = "0.16.2", features = [ "ws-client" ] } -mockito = "1.4" +mockito = "1.5" monero-harness = { path = "../monero-harness" } port_check = "0.2" proptest = "1" diff --git a/swap/src/api.rs b/swap/src/api.rs index ddd8e45f..c2b6f73a 100644 --- a/swap/src/api.rs +++ b/swap/src/api.rs @@ -170,6 +170,7 @@ pub struct Context { pub swap_lock: Arc, pub config: Config, pub tasks: Arc, + pub is_daemon: bool, } #[allow(clippy::too_many_arguments)] @@ -183,6 +184,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); @@ -251,6 +253,7 @@ impl Context { }, swap_lock: Arc::new(SwapLock::new()), tasks: Arc::new(PendingTaskList::default()), + is_daemon, }; Ok(context) @@ -275,6 +278,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 d1878e5f..863737da 100644 --- a/swap/src/api/request.rs +++ b/swap/src/api/request.rs @@ -9,9 +9,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; @@ -652,14 +655,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::Logs { logs_dir, redact, swap_id } => { let dir = logs_dir.unwrap_or(context.config.data_dir.join("logs")); diff --git a/swap/src/asb/command.rs b/swap/src/asb/command.rs index 07bf2ff6..7e78bc9d 100644 --- a/swap/src/asb/command.rs +++ b/swap/src/asb/command.rs @@ -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, @@ -197,7 +197,9 @@ pub enum Command { Start { resume_only: bool, }, - History, + History { + only_unfinished: bool, + }, Config, Logs { logs_dir: Option, @@ -275,8 +277,6 @@ pub enum RawCommand { )] resume_only: bool, }, - #[structopt(about = "Prints swap-id and the state of each swap ever made.")] - History, #[structopt(about = "Prints all logging messages issued in the past.")] Logs { #[structopt( @@ -296,6 +296,14 @@ pub enum RawCommand { )] swap_id: Option }, + #[structopt(about = "Prints swap-id and the state of each swap ever made.")] + 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.")] @@ -411,7 +419,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); @@ -586,7 +596,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); diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 44d848b5..0876fbc9 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -19,6 +19,8 @@ use libp2p::core::Multiaddr; use libp2p::swarm::AddressScore; use libp2p::Swarm; use swap::common::tracing_util::Format; +use rust_decimal::prelude::FromPrimitive; +use rust_decimal::Decimal; use std::convert::TryInto; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; @@ -34,7 +36,9 @@ use swap::common::{self, check_latest_version, get_logs}; 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::{bitcoin, kraken, monero, tor}; @@ -233,19 +237,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 1f407389..26d95978 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -78,6 +78,7 @@ where debug, json, None, + false, ) .await?; @@ -100,7 +101,8 @@ 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::Logs { @@ -117,7 +119,8 @@ where 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 } => { @@ -134,6 +137,7 @@ where debug, json, None, + false, ) .await?; (context, request) @@ -155,6 +159,7 @@ where debug, json, server_address, + true, ) .await?; (context, request) @@ -176,6 +181,7 @@ where debug, json, None, + false, ) .await?; (context, request) @@ -197,6 +203,7 @@ where debug, json, None, + false, ) .await?; (context, request) @@ -217,6 +224,7 @@ where debug, json, None, + false, ) .await?; (context, request) @@ -227,8 +235,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) } @@ -244,6 +262,7 @@ where debug, json, None, + false, ) .await?; (context, request) @@ -254,7 +273,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 79236563..14f718d3 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -1,11 +1,14 @@ //! Run an XMR/BTC swap in the role of Alice. //! Alice holds XMR and wishes receive BTC. +use std::time::Duration; + use crate::asb::{EventLoopHandle, LatestRate}; use crate::bitcoin::ExpiredTimelocks; use crate::env::Config; use crate::protocol::alice::{AliceState, Swap}; use crate::{bitcoin, monero}; use anyhow::{bail, Context, Result}; +use backoff::ExponentialBackoffBuilder; use tokio::select; use tokio::time::timeout; use uuid::Uuid; @@ -111,23 +114,63 @@ where } } AliceState::BtcLocked { state3 } => { - match state3.expired_timelocks(bitcoin_wallet).await? { - ExpiredTimelocks::None { .. } => { - // Record the current monero wallet block height so we don't have to scan from - // block 0 for scenarios where we create a refund wallet. - let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; + // We retry to lock the Monero wallet until we succeed or until the cancel timelock expires. + // + // This is necessary because the monero-wallet-rpc can sometimes error out due to various reasons, such as + // - no connection to the daemon + // - "failed to get output distribution" + // See https://github.com/comit-network/xmr-btc-swap/issues/1726 + let backoff = ExponentialBackoffBuilder::new() + .with_initial_interval(Duration::from_secs(5)) + .with_max_interval(Duration::from_secs(60 * 3)) + .with_max_elapsed_time(None) + .build(); - let transfer_proof = monero_wallet - .transfer(state3.lock_xmr_transfer_request()) - .await?; + let result = backoff::future::retry_notify( + backoff, + || async { + match state3.expired_timelocks(bitcoin_wallet).await { + Ok(ExpiredTimelocks::None { .. }) => { + // Record the current monero wallet block height so we don't have to scan from + // block 0 for scenarios where we create a refund wallet. + let monero_wallet_restore_blockheight = monero_wallet + .block_height() + .await + .map_err(backoff::Error::transient)?; + let transfer_proof = monero_wallet + .transfer(state3.lock_xmr_transfer_request()) + .await + .map_err(backoff::Error::transient)?; + + Ok(Some((monero_wallet_restore_blockheight, transfer_proof))) + } + Ok(_) => Ok(None), + Err(e) => Err(backoff::Error::transient(e)), + } + }, + |err, delay: Duration| { + tracing::warn!( + %err, + delay_secs = delay.as_secs(), + "Failed to lock XMR. We will retry after a delay" + ); + }, + ) + .await; + + match result { + Ok(Some((monero_wallet_restore_blockheight, transfer_proof))) => { AliceState::XmrLockTransactionSent { monero_wallet_restore_blockheight, transfer_proof, state3, } } - _ => AliceState::SafelyAborted, + Ok(None) => AliceState::SafelyAborted, + Err(e) => { + unreachable!("We should retry forever until the cancel timelock expires. But we got an error: {:#}", e); + } } } AliceState::XmrLockTransactionSent { @@ -397,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())