diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c9b1cc8..13d9ffdf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,6 +130,4 @@ jobs: - uses: Swatinem/rust-cache@v1.2.0 - name: Run test ${{ matrix.test_name }} - run: cargo test --package swap --all-features --test ${{ matrix.test_name }} "" - env: - MONERO_ADDITIONAL_SLEEP_PERIOD: 60000 + run: cargo test --package swap --all-features --test ${{ matrix.test_name }} -- --nocapture diff --git a/Cargo.lock b/Cargo.lock index 96367206..ddeb23db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -356,7 +356,7 @@ dependencies = [ "bitcoincore-rpc-json", "futures", "hex 0.4.3", - "jsonrpc_client", + "jsonrpc_client 0.5.1", "reqwest", "serde", "serde_json", @@ -1629,7 +1629,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc8515639023bf4260cf89475355fa77301685418f655c680528c380759e7782" dependencies = [ "async-trait", - "jsonrpc_client_macro", + "jsonrpc_client_macro 0.2.0", + "reqwest", + "serde", + "serde_json", + "url 2.2.1", +] + +[[package]] +name = "jsonrpc_client" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a85cf2c5ce158eabf30b2ac4f535463d7b09ce7905502e11238b7d6048ef7d02" +dependencies = [ + "async-trait", + "jsonrpc_client_macro 0.3.0", "reqwest", "serde", "serde_json", @@ -1646,6 +1660,16 @@ dependencies = [ "syn", ] +[[package]] +name = "jsonrpc_client_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c11e429f0eaa41fe659013680b459d2368d8f0a3e69dccfb7a35800b0dc27b" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "keccak-hash" version = "0.7.0" @@ -2143,7 +2167,6 @@ dependencies = [ "anyhow", "futures", "monero-rpc", - "port_check", "rand 0.7.3", "spectral", "testcontainers 0.12.0", @@ -2157,6 +2180,8 @@ name = "monero-rpc" version = "0.1.0" dependencies = [ "anyhow", + "jsonrpc_client 0.6.0", + "monero", "reqwest", "serde", "serde_json", diff --git a/monero-harness/Cargo.toml b/monero-harness/Cargo.toml index fa57f1a9..0b090d0c 100644 --- a/monero-harness/Cargo.toml +++ b/monero-harness/Cargo.toml @@ -8,7 +8,6 @@ edition = "2018" anyhow = "1" futures = "0.3" monero-rpc = { path = "../monero-rpc" } -port_check = "0.1" rand = "0.7" spectral = "0.6" testcontainers = "0.12" diff --git a/monero-harness/src/image.rs b/monero-harness/src/image.rs index f0cfe105..4e1ee8bc 100644 --- a/monero-harness/src/image.rs +++ b/monero-harness/src/image.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, env::var, thread::sleep, time::Duration}; +use std::collections::HashMap; use testcontainers::{ core::{Container, Docker, WaitForMessage}, Image, @@ -6,42 +6,36 @@ use testcontainers::{ pub const MONEROD_DAEMON_CONTAINER_NAME: &str = "monerod"; pub const MONEROD_DEFAULT_NETWORK: &str = "monero_network"; -pub const MONEROD_RPC_PORT: u16 = 48081; -pub const WALLET_RPC_PORT: u16 = 48083; + +/// The port we use for all RPC communication. +/// +/// This is the default when running monerod. +/// For `monero-wallet-rpc` we always need to specify a port. To make things +/// simpler, we just specify the same one. They are in different containers so +/// this doesn't matter. +pub const RPC_PORT: u16 = 18081; #[derive(Debug)] -pub struct Monero { - tag: String, - args: Args, - entrypoint: Option, - wait_for_message: String, +pub struct Monerod { + args: MonerodArgs, } -impl Image for Monero { - type Args = Args; +impl Image for Monerod { + type Args = MonerodArgs; type EnvVars = HashMap; type Volumes = HashMap; type EntryPoint = str; fn descriptor(&self) -> String { - format!("xmrto/monero:{}", self.tag) + "xmrto/monero:v0.17.2.0".to_owned() } fn wait_until_ready(&self, container: &Container<'_, D, Self>) { container .logs() .stdout - .wait_for_message(&self.wait_for_message) + .wait_for_message("JOINING all threads") .unwrap(); - - let additional_sleep_period = - var("MONERO_ADDITIONAL_SLEEP_PERIOD").map(|value| value.parse()); - - if let Ok(Ok(sleep_period)) = additional_sleep_period { - let sleep_period = Duration::from_millis(sleep_period); - - sleep(sleep_period) - } } fn args(&self) -> ::Args { @@ -57,77 +51,80 @@ impl Image for Monero { } fn with_args(self, args: ::Args) -> Self { - Monero { args, ..self } - } - - fn with_entrypoint(self, entrypoint: &Self::EntryPoint) -> Self { - Self { - entrypoint: Some(entrypoint.to_string()), - ..self - } + Self { args } } fn entrypoint(&self) -> Option { - self.entrypoint.to_owned() + Some("".to_owned()) // an empty entrypoint disables the entrypoint + // script and gives us full control } } -impl Default for Monero { - fn default() -> Self { - Monero { - tag: "v0.17.2.0".into(), - args: Args::default(), - entrypoint: Some("".into()), - wait_for_message: "core RPC server started ok".to_string(), - } - } -} - -impl Monero { - pub fn with_tag(self, tag_str: &str) -> Self { - Monero { - tag: tag_str.to_string(), - ..self - } - } - - pub fn wallet(name: &str, daemon_address: String) -> Self { - let wallet = WalletArgs::new(name, daemon_address, WALLET_RPC_PORT); - let default = Monero::default(); - Self { - args: Args { - image_args: ImageArgs::WalletArgs(wallet), - }, - wait_for_message: "Run server thread name: RPC".to_string(), - ..default - } - } -} - -#[derive(Clone, Debug)] -pub struct Args { - image_args: ImageArgs, -} - -impl Default for Args { +impl Default for Monerod { fn default() -> Self { Self { - image_args: ImageArgs::MonerodArgs(MonerodArgs::default()), + args: MonerodArgs::default(), } } } -#[derive(Clone, Debug)] -pub enum ImageArgs { - MonerodArgs(MonerodArgs), - WalletArgs(WalletArgs), +#[derive(Debug)] +pub struct MoneroWalletRpc { + args: MoneroWalletRpcArgs, } -impl ImageArgs { - fn args(&self) -> String { - match self { - ImageArgs::MonerodArgs(monerod_args) => monerod_args.args(), - ImageArgs::WalletArgs(wallet_args) => wallet_args.args(), +impl Image for MoneroWalletRpc { + type Args = MoneroWalletRpcArgs; + type EnvVars = HashMap; + type Volumes = HashMap; + type EntryPoint = str; + + fn descriptor(&self) -> String { + "xmrto/monero:v0.17.2.0".to_owned() + } + + fn wait_until_ready(&self, container: &Container<'_, D, Self>) { + container + .logs() + .stdout + .wait_for_message("JOINING all threads") + .unwrap(); + } + + fn args(&self) -> ::Args { + self.args.clone() + } + + fn volumes(&self) -> Self::Volumes { + HashMap::new() + } + + fn env_vars(&self) -> Self::EnvVars { + HashMap::new() + } + + fn with_args(self, args: ::Args) -> Self { + Self { args } + } + + fn entrypoint(&self) -> Option { + Some("".to_owned()) // an empty entrypoint disables the entrypoint + // script and gives us full control + } +} + +impl Default for MoneroWalletRpc { + fn default() -> Self { + Self { + args: MoneroWalletRpcArgs::default(), + } + } +} + +impl MoneroWalletRpc { + pub fn new(name: &str, daemon_address: String) -> Self { + Self { + args: MoneroWalletRpcArgs::new(name, daemon_address), } } } @@ -138,51 +135,39 @@ pub struct MonerodArgs { pub offline: bool, pub rpc_payment_allow_free_loopback: bool, pub confirm_external_bind: bool, - pub non_interactive: bool, pub no_igd: bool, pub hide_my_port: bool, pub rpc_bind_ip: String, - pub rpc_bind_port: u16, pub fixed_difficulty: u32, pub data_dir: String, - pub log_level: u32, } -#[derive(Debug, Clone)] -pub struct WalletArgs { - pub disable_rpc_login: bool, - pub confirm_external_bind: bool, - pub wallet_dir: String, - pub rpc_bind_ip: String, - pub rpc_bind_port: u16, - pub daemon_address: String, - pub log_level: u32, -} - -/// Sane defaults for a mainnet regtest instance. impl Default for MonerodArgs { fn default() -> Self { - MonerodArgs { + Self { regtest: true, offline: true, rpc_payment_allow_free_loopback: true, confirm_external_bind: true, - non_interactive: true, no_igd: true, hide_my_port: true, rpc_bind_ip: "0.0.0.0".to_string(), - rpc_bind_port: MONEROD_RPC_PORT, fixed_difficulty: 1, data_dir: "/monero".to_string(), - log_level: 2, } } } -impl MonerodArgs { - // Return monerod args as is single string so we can pass it to bash. - fn args(&self) -> String { - let mut args = vec!["monerod".to_string()]; +impl IntoIterator for MonerodArgs { + type Item = String; + type IntoIter = ::std::vec::IntoIter; + + fn into_iter(self) -> ::IntoIter { + let mut args = vec![ + "monerod".to_string(), + "--log-level=4".to_string(), + "--non-interactive".to_string(), + ]; if self.regtest { args.push("--regtest".to_string()) @@ -200,10 +185,6 @@ impl MonerodArgs { args.push("--confirm-external-bind".to_string()) } - if self.non_interactive { - args.push("--non-interactive".to_string()) - } - if self.no_igd { args.push("--no-igd".to_string()) } @@ -213,45 +194,60 @@ impl MonerodArgs { } if !self.rpc_bind_ip.is_empty() { - args.push(format!("--rpc-bind-ip {}", self.rpc_bind_ip)); - } - - if self.rpc_bind_port != 0 { - args.push(format!("--rpc-bind-port {}", self.rpc_bind_port)); + args.push(format!("--rpc-bind-ip={}", self.rpc_bind_ip)); } if !self.data_dir.is_empty() { - args.push(format!("--data-dir {}", self.data_dir)); + args.push(format!("--data-dir={}", self.data_dir)); } if self.fixed_difficulty != 0 { - args.push(format!("--fixed-difficulty {}", self.fixed_difficulty)); + args.push(format!("--fixed-difficulty={}", self.fixed_difficulty)); } - if self.log_level != 0 { - args.push(format!("--log-level {}", self.log_level)); - } - - args.join(" ") + args.into_iter() } } -impl WalletArgs { - pub fn new(wallet_name: &str, daemon_address: String, rpc_port: u16) -> Self { - WalletArgs { +#[derive(Debug, Clone)] +pub struct MoneroWalletRpcArgs { + pub disable_rpc_login: bool, + pub confirm_external_bind: bool, + pub wallet_dir: String, + pub rpc_bind_ip: String, + pub daemon_address: String, +} + +impl Default for MoneroWalletRpcArgs { + fn default() -> Self { + unimplemented!("A default instance for `MoneroWalletRpc` doesn't make sense because we always need to connect to a node.") + } +} + +impl MoneroWalletRpcArgs { + pub fn new(wallet_name: &str, daemon_address: String) -> Self { + Self { disable_rpc_login: true, confirm_external_bind: true, wallet_dir: wallet_name.into(), rpc_bind_ip: "0.0.0.0".into(), - rpc_bind_port: rpc_port, daemon_address, - log_level: 4, } } +} - // Return monero-wallet-rpc args as is single string so we can pass it to bash. - fn args(&self) -> String { - let mut args = vec!["monero-wallet-rpc".to_string()]; +impl IntoIterator for MoneroWalletRpcArgs { + type Item = String; + type IntoIter = ::std::vec::IntoIter; + + fn into_iter(self) -> ::IntoIter { + let mut args = vec![ + "monero-wallet-rpc".to_string(), + format!("--wallet-dir={}", self.wallet_dir), + format!("--daemon-address={}", self.daemon_address), + format!("--rpc-bind-port={}", RPC_PORT), + "--log-level=4".to_string(), + ]; if self.disable_rpc_login { args.push("--disable-rpc-login".to_string()) @@ -261,40 +257,10 @@ impl WalletArgs { args.push("--confirm-external-bind".to_string()) } - if !self.wallet_dir.is_empty() { - args.push(format!("--wallet-dir {}", self.wallet_dir)); - } - if !self.rpc_bind_ip.is_empty() { - args.push(format!("--rpc-bind-ip {}", self.rpc_bind_ip)); + args.push(format!("--rpc-bind-ip={}", self.rpc_bind_ip)); } - if self.rpc_bind_port != 0 { - args.push(format!("--rpc-bind-port {}", self.rpc_bind_port)); - } - - if !self.daemon_address.is_empty() { - args.push(format!("--daemon-address {}", self.daemon_address)); - } - - if self.log_level != 0 { - args.push(format!("--log-level {}", self.log_level)); - } - - args.join(" ") - } -} - -impl IntoIterator for Args { - type Item = String; - type IntoIter = ::std::vec::IntoIter; - - fn into_iter(self) -> ::IntoIter { - vec![ - "/bin/bash".to_string(), - "-c".to_string(), - format!("{} ", self.image_args.args()), - ] - .into_iter() + args.into_iter() } } diff --git a/monero-harness/src/lib.rs b/monero-harness/src/lib.rs index 48615b92..3c7214af 100644 --- a/monero-harness/src/lib.rs +++ b/monero-harness/src/lib.rs @@ -22,16 +22,15 @@ //! Also provides standalone JSON RPC clients for monerod and monero-wallet-rpc. pub mod image; -use crate::image::{ - MONEROD_DAEMON_CONTAINER_NAME, MONEROD_DEFAULT_NETWORK, MONEROD_RPC_PORT, WALLET_RPC_PORT, -}; -use anyhow::{anyhow, bail, Result}; +use crate::image::{MONEROD_DAEMON_CONTAINER_NAME, MONEROD_DEFAULT_NETWORK, RPC_PORT}; +use anyhow::{anyhow, bail, Context, Result}; use monero_rpc::{ monerod, - wallet::{self, GetAddress, Refreshed, Transfer}, + monerod::MonerodRpc as _, + wallet::{self, GetAddress, MoneroWalletRpc as _, Refreshed, Transfer}, }; use std::time::Duration; -use testcontainers::{clients::Cli, core::Port, Container, Docker, RunArgs}; +use testcontainers::{clients::Cli, Container, Docker, RunArgs}; use tokio::time; /// How often we mine a block. @@ -55,15 +54,19 @@ impl<'c> Monero { /// miner wallet container name is: `miner` pub async fn new( cli: &'c Cli, - additional_wallets: Vec, - ) -> Result<(Self, Vec>)> { + additional_wallets: Vec<&'static str>, + ) -> Result<( + Self, + Container<'c, Cli, image::Monerod>, + Vec>, + )> { let prefix = format!("{}_", random_prefix()); let monerod_name = format!("{}{}", prefix, MONEROD_DAEMON_CONTAINER_NAME); let network = format!("{}{}", prefix, MONEROD_DEFAULT_NETWORK); tracing::info!("Starting monerod: {}", monerod_name); let (monerod, monerod_container) = Monerod::new(cli, monerod_name, network)?; - let mut containers = vec![monerod_container]; + let mut containers = vec![]; let mut wallets = vec![]; let miner = "miner"; @@ -81,7 +84,7 @@ impl<'c> Monero { containers.push(container); } - Ok((Self { monerod, wallets }, containers)) + Ok((Self { monerod, wallets }, monerod_container, containers)) } pub fn monerod(&self) -> &Monerod { @@ -104,7 +107,10 @@ impl<'c> Monero { // generate the first 70 as bulk let monerod = &self.monerod; - let res = monerod.client().generate_blocks(70, &miner_address).await?; + let res = monerod + .client() + .generateblocks(70, miner_address.clone()) + .await?; tracing::info!("Generated {:?} blocks", res.blocks.len()); miner_wallet.refresh().await?; @@ -123,7 +129,10 @@ impl<'c> Monero { if amount > 0 { miner_wallet.transfer(&address, amount).await?; tracing::info!("Funded {} wallet with {}", wallet.name, amount); - monerod.client().generate_blocks(10, &miner_address).await?; + monerod + .client() + .generateblocks(10, miner_address.clone()) + .await?; wallet.refresh().await?; } } @@ -139,7 +148,7 @@ impl<'c> Monero { monerod.start_miner(&miner_address).await?; tracing::info!("Waiting for miner wallet to catch up..."); - let block_height = monerod.client().get_block_count().await?; + let block_height = monerod.client().get_block_count().await?.count; miner_wallet .wait_for_wallet_height(block_height) .await @@ -158,17 +167,11 @@ impl<'c> Monero { fn random_prefix() -> String { use rand::Rng; - const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz"; - const LEN: usize = 4; - let mut rng = rand::thread_rng(); - let prefix: String = (0..LEN) - .map(|_| { - let idx = rng.gen_range(0, CHARSET.len()); - CHARSET[idx] as char - }) - .collect(); - prefix + rand::thread_rng() + .sample_iter(rand::distributions::Alphanumeric) + .take(4) + .collect() } #[derive(Clone, Debug)] @@ -176,6 +179,7 @@ pub struct Monerod { rpc_port: u16, name: String, network: String, + client: monerod::Client, } #[derive(Clone, Debug)] @@ -183,6 +187,7 @@ pub struct MoneroWalletRpc { rpc_port: u16, name: String, network: String, + client: wallet::Client, } impl<'c> Monerod { @@ -191,38 +196,35 @@ impl<'c> Monerod { cli: &'c Cli, name: String, network: String, - ) -> Result<(Self, Container<'c, Cli, image::Monero>)> { - let monerod_rpc_port: u16 = - port_check::free_local_port().ok_or_else(|| anyhow!("Could not retrieve free port"))?; - - let image = image::Monero::default(); + ) -> Result<(Self, Container<'c, Cli, image::Monerod>)> { + let image = image::Monerod::default(); let run_args = RunArgs::default() .with_name(name.clone()) - .with_network(network.clone()) - .with_mapped_port(Port { - local: monerod_rpc_port, - internal: MONEROD_RPC_PORT, - }); - let docker = cli.run_with_args(image, run_args); + .with_network(network.clone()); + let container = cli.run_with_args(image, run_args); + let monerod_rpc_port = container + .get_host_port(RPC_PORT) + .context("port not exposed")?; Ok(( Self { rpc_port: monerod_rpc_port, name, network, + client: monerod::Client::localhost(monerod_rpc_port)?, }, - docker, + container, )) } - pub fn client(&self) -> monerod::Client { - monerod::Client::localhost(self.rpc_port) + pub fn client(&self) -> &monerod::Client { + &self.client } /// Spawns a task to mine blocks in a regular interval to the provided /// address pub async fn start_miner(&self, miner_wallet_address: &str) -> Result<()> { - let monerod = self.client(); + let monerod = self.client().clone(); let _ = tokio::spawn(mine(monerod, miner_wallet_address.to_string())); Ok(()) } @@ -236,48 +238,46 @@ impl<'c> MoneroWalletRpc { name: &str, monerod: &Monerod, prefix: String, - ) -> Result<(Self, Container<'c, Cli, image::Monero>)> { - let wallet_rpc_port: u16 = - port_check::free_local_port().ok_or_else(|| anyhow!("Could not retrieve free port"))?; - - let daemon_address = format!("{}:{}", monerod.name, MONEROD_RPC_PORT); - let image = image::Monero::wallet(&name, daemon_address); + ) -> Result<(Self, Container<'c, Cli, image::MoneroWalletRpc>)> { + let daemon_address = format!("{}:{}", monerod.name, RPC_PORT); + let image = image::MoneroWalletRpc::new(&name, daemon_address); let network = monerod.network.clone(); let run_args = RunArgs::default() // prefix the container name so we can run multiple tests .with_name(format!("{}{}", prefix, name)) - .with_network(network.clone()) - .with_mapped_port(Port { - local: wallet_rpc_port, - internal: WALLET_RPC_PORT, - }); - let docker = cli.run_with_args(image, run_args); + .with_network(network.clone()); + let container = cli.run_with_args(image, run_args); + let wallet_rpc_port = container + .get_host_port(RPC_PORT) + .context("port not exposed")?; // create new wallet - wallet::Client::localhost(wallet_rpc_port) - .create_wallet(name) - .await - .unwrap(); + let client = wallet::Client::localhost(wallet_rpc_port)?; + + client + .create_wallet(name.to_owned(), "English".to_owned()) + .await?; Ok(( Self { rpc_port: wallet_rpc_port, name: name.to_string(), network, + client, }, - docker, + container, )) } - pub fn client(&self) -> wallet::Client { - wallet::Client::localhost(self.rpc_port) + pub fn client(&self) -> &wallet::Client { + &self.client } // It takes a little while for the wallet to sync with monerod. pub async fn wait_for_wallet_height(&self, height: u32) -> Result<()> { let mut retry: u8 = 0; - while self.client().block_height().await?.height < height { + while self.client().get_height().await?.height < height { if retry >= 30 { // ~30 seconds bail!("Wallet could not catch up with monerod after 30 retries.") @@ -290,26 +290,28 @@ impl<'c> MoneroWalletRpc { /// Sends amount to address pub async fn transfer(&self, address: &str, amount: u64) -> Result { - self.client().transfer(0, amount, address).await + Ok(self.client().transfer_single(0, amount, address).await?) } pub async fn address(&self) -> Result { - self.client().get_address(0).await + Ok(self.client().get_address(0).await?) } pub async fn balance(&self) -> Result { self.client().refresh().await?; - self.client().get_balance(0).await + let balance = self.client().get_balance(0).await?.balance; + + Ok(balance) } pub async fn refresh(&self) -> Result { - self.client().refresh().await + Ok(self.client().refresh().await?) } } /// Mine a block ever BLOCK_TIME_SECS seconds. async fn mine(monerod: monerod::Client, reward_address: String) -> Result<()> { loop { time::sleep(Duration::from_secs(BLOCK_TIME_SECS)).await; - monerod.generate_blocks(1, &reward_address).await?; + monerod.generateblocks(1, reward_address.clone()).await?; } } diff --git a/monero-harness/tests/monerod.rs b/monero-harness/tests/monerod.rs index 257e888d..d218167b 100644 --- a/monero-harness/tests/monerod.rs +++ b/monero-harness/tests/monerod.rs @@ -1,4 +1,5 @@ use monero_harness::Monero; +use monero_rpc::monerod::MonerodRpc as _; use spectral::prelude::*; use std::time::Duration; use testcontainers::clients::Cli; @@ -12,7 +13,7 @@ async fn init_miner_and_mine_to_miner_address() { .set_default(); let tc = Cli::default(); - let (monero, _monerod_container) = Monero::new(&tc, vec![]).await.unwrap(); + let (monero, _monerod_container, _wallet_containers) = Monero::new(&tc, vec![]).await.unwrap(); monero.init_and_start_miner().await.unwrap(); @@ -25,7 +26,7 @@ async fn init_miner_and_mine_to_miner_address() { time::sleep(Duration::from_millis(1010)).await; // after a bit more than 1 sec another block should have been mined - let block_height = monerod.client().get_block_count().await.unwrap(); + let block_height = monerod.client().get_block_count().await.unwrap().count; assert_that(&block_height).is_greater_than(70); } diff --git a/monero-harness/tests/wallet.rs b/monero-harness/tests/wallet.rs index a3f4e52e..b25683ad 100644 --- a/monero-harness/tests/wallet.rs +++ b/monero-harness/tests/wallet.rs @@ -1,4 +1,5 @@ use monero_harness::{Monero, MoneroWalletRpc}; +use monero_rpc::wallet::MoneroWalletRpc as _; use spectral::prelude::*; use std::time::Duration; use testcontainers::clients::Cli; @@ -16,9 +17,8 @@ async fn fund_transfer_and_check_tx_key() { let send_to_bob = 5_000_000_000; let tc = Cli::default(); - let (monero, _containers) = Monero::new(&tc, vec!["alice".to_string(), "bob".to_string()]) - .await - .unwrap(); + let (monero, _monerod_container, _wallet_containers) = + Monero::new(&tc, vec!["alice", "bob"]).await.unwrap(); let alice_wallet = monero.wallet("alice").unwrap(); let bob_wallet = monero.wallet("bob").unwrap(); @@ -45,10 +45,10 @@ async fn fund_transfer_and_check_tx_key() { // check if tx was actually seen let tx_id = transfer.tx_hash; - let tx_key = transfer.tx_key; + let tx_key = transfer.tx_key.unwrap().to_string(); let res = bob_wallet .client() - .check_tx_key(&tx_id, &tx_key, &bob_address) + .check_tx_key(tx_id, tx_key, bob_address) .await .expect("failed to check tx by key"); diff --git a/monero-rpc/Cargo.toml b/monero-rpc/Cargo.toml index c70f2797..8f1b68ed 100644 --- a/monero-rpc/Cargo.toml +++ b/monero-rpc/Cargo.toml @@ -10,3 +10,5 @@ reqwest = { version = "0.11", default-features = false, features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tracing = "0.1" +jsonrpc_client = { version = "0.6", features = ["reqwest"] } +monero = "0.11" diff --git a/monero-rpc/src/lib.rs b/monero-rpc/src/lib.rs index 1a83b3e1..a1b2eaaa 100644 --- a/monero-rpc/src/lib.rs +++ b/monero-rpc/src/lib.rs @@ -12,6 +12,5 @@ )] #![forbid(unsafe_code)] -mod rpc; - -pub use self::rpc::*; +pub mod monerod; +pub mod wallet; diff --git a/monero-rpc/src/monerod.rs b/monero-rpc/src/monerod.rs new file mode 100644 index 00000000..8d044134 --- /dev/null +++ b/monero-rpc/src/monerod.rs @@ -0,0 +1,63 @@ +use anyhow::{Context, Result}; +use serde::Deserialize; + +#[jsonrpc_client::api(version = "2.0")] +pub trait MonerodRpc { + async fn generateblocks(&self, amount_of_blocks: u32, wallet_address: String) + -> GenerateBlocks; + async fn get_block_header_by_height(&self, height: u32) -> BlockHeader; + async fn get_block_count(&self) -> BlockCount; +} + +#[jsonrpc_client::implement(MonerodRpc)] +#[derive(Debug, Clone)] +pub struct Client { + inner: reqwest::Client, + base_url: reqwest::Url, +} + +impl Client { + /// New local host monerod RPC client. + pub fn localhost(port: u16) -> Result { + Ok(Self { + inner: reqwest::ClientBuilder::new() + .connection_verbose(true) + .build()?, + base_url: format!("http://127.0.0.1:{}/json_rpc", port) + .parse() + .context("url is well formed")?, + }) + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct GenerateBlocks { + pub blocks: Vec, + pub height: u32, + pub status: String, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct BlockCount { + pub count: u32, + pub status: String, +} + +// We should be able to use monero-rs for this but it does not include all +// the fields. +#[derive(Clone, Debug, Deserialize)] +pub struct BlockHeader { + pub block_size: u32, + pub depth: u32, + pub difficulty: u32, + pub hash: String, + pub height: u32, + pub major_version: u32, + pub minor_version: u32, + pub nonce: u32, + pub num_txes: u32, + pub orphan_status: bool, + pub prev_hash: String, + pub reward: u64, + pub timestamp: u32, +} diff --git a/monero-rpc/src/rpc.rs b/monero-rpc/src/rpc.rs deleted file mode 100644 index e0e58ed7..00000000 --- a/monero-rpc/src/rpc.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! JSON RPC clients for `monerd` and `monero-wallet-rpc`. -pub mod monerod; -pub mod wallet; - -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Debug, Clone)] -pub struct Request { - /// JSON RPC version, we hard cod this to 2.0. - jsonrpc: String, - /// Client controlled identifier, we hard code this to 1. - id: String, - /// The method to call. - method: String, - /// The method parameters. - params: T, -} - -/// JSON RPC request. -impl Request { - pub fn new(method: &str, params: T) -> Self { - Self { - jsonrpc: "2.0".to_owned(), - id: "1".to_owned(), - method: method.to_owned(), - params, - } - } -} - -/// JSON RPC response. -#[derive(Deserialize, Serialize, Debug, Clone)] -struct Response { - pub id: String, - pub jsonrpc: String, - pub result: T, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[derive(Serialize, Debug, Clone)] - struct Params { - val: u32, - } - - #[test] - fn can_serialize_request_with_params() { - // Dummy method and parameters. - let params = Params { val: 0 }; - let method = "get_block"; - - let r = Request::new(method, ¶ms); - let got = serde_json::to_string(&r).expect("failed to serialize request"); - - let want = - "{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"method\":\"get_block\",\"params\":{\"val\":0}}" - .to_string(); - - assert_eq!(got, want); - } -} diff --git a/monero-rpc/src/rpc/monerod.rs b/monero-rpc/src/rpc/monerod.rs deleted file mode 100644 index a98300a7..00000000 --- a/monero-rpc/src/rpc/monerod.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::rpc::{Request, Response}; -use anyhow::Result; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use tracing::debug; - -/// RPC client for monerod and monero-wallet-rpc. -#[derive(Debug, Clone)] -pub struct Client { - pub inner: reqwest::Client, - pub url: Url, -} - -impl Client { - /// New local host monerod RPC client. - pub fn localhost(port: u16) -> Self { - let url = format!("http://127.0.0.1:{}/json_rpc", port); - let url = Url::parse(&url).expect("url is well formed"); - - Self { - inner: reqwest::Client::new(), - url, - } - } - - pub async fn generate_blocks( - &self, - amount_of_blocks: u32, - wallet_address: &str, - ) -> Result { - let params = GenerateBlocksParams { - amount_of_blocks, - wallet_address: wallet_address.to_owned(), - }; - let url = self.url.clone(); - // // Step 1: Get the auth header - // let res = self.inner.get(url.clone()).send().await?; - // let headers = res.headers(); - // let wwwauth = headers["www-authenticate"].to_str()?; - // - // // Step 2: Given the auth header, sign the digest for the real req. - // let tmp_url = url.clone(); - // let context = AuthContext::new("username", "password", tmp_url.path()); - // let mut prompt = digest_auth::parse(wwwauth)?; - // let answer = prompt.respond(&context)?.to_header_string(); - - let request = Request::new("generateblocks", params); - - let response = self - .inner - .post(url) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("generate blocks response: {}", response); - - let res: Response = serde_json::from_str(&response)?; - - Ok(res.result) - } - - // $ curl http://127.0.0.1:18081/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"get_block_header_by_height","params":{"height":1}}' -H 'Content-Type: application/json' - pub async fn get_block_header_by_height(&self, height: u32) -> Result { - let params = GetBlockHeaderByHeightParams { height }; - let request = Request::new("get_block_header_by_height", params); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("get block header by height response: {}", response); - - let res: Response = serde_json::from_str(&response)?; - - Ok(res.result.block_header) - } - - pub async fn get_block_count(&self) -> Result { - let request = Request::new("get_block_count", ""); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("get block count response: {}", response); - - let res: Response = serde_json::from_str(&response)?; - - Ok(res.result.count) - } -} - -#[derive(Clone, Debug, Serialize)] -struct GenerateBlocksParams { - amount_of_blocks: u32, - wallet_address: String, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct GenerateBlocks { - pub blocks: Vec, - pub height: u32, - pub status: String, -} - -#[derive(Clone, Debug, Serialize)] -struct GetBlockHeaderByHeightParams { - height: u32, -} - -#[derive(Clone, Debug, Deserialize)] -struct GetBlockHeaderByHeight { - block_header: BlockHeader, - status: String, - untrusted: bool, -} - -#[derive(Clone, Debug, Deserialize)] -struct BlockCount { - count: u32, - status: String, -} - -// We should be able to use monero-rs for this but it does not include all -// the fields. -#[derive(Clone, Debug, Deserialize)] -pub struct BlockHeader { - pub block_size: u32, - pub depth: u32, - pub difficulty: u32, - pub hash: String, - pub height: u32, - pub major_version: u32, - pub minor_version: u32, - pub nonce: u32, - pub num_txes: u32, - pub orphan_status: bool, - pub prev_hash: String, - pub reward: u64, - pub timestamp: u32, -} diff --git a/monero-rpc/src/rpc/wallet.rs b/monero-rpc/src/rpc/wallet.rs deleted file mode 100644 index 675be036..00000000 --- a/monero-rpc/src/rpc/wallet.rs +++ /dev/null @@ -1,569 +0,0 @@ -use crate::rpc::{Request, Response}; -use anyhow::{bail, Result}; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use tracing::debug; - -/// JSON RPC client for monero-wallet-rpc. -#[derive(Debug, Clone)] -pub struct Client { - pub inner: reqwest::Client, - pub url: Url, -} - -impl Client { - /// Constructs a monero-wallet-rpc client with localhost endpoint. - pub fn localhost(port: u16) -> Self { - let url = format!("http://127.0.0.1:{}/json_rpc", port); - let url = Url::parse(&url).expect("url is well formed"); - - Client::new(url) - } - - /// Constructs a monero-wallet-rpc client with `url` endpoint. - pub fn new(url: Url) -> Self { - Self { - inner: reqwest::Client::new(), - url, - } - } - - /// Get addresses for account by index. - pub async fn get_address(&self, account_index: u32) -> Result { - let params = GetAddressParams { account_index }; - let request = Request::new("get_address", params); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("get address RPC response: {}", response); - - let r = serde_json::from_str::>(&response)?; - Ok(r.result) - } - - /// Gets the balance of account by index. - pub async fn get_balance(&self, index: u32) -> Result { - let params = GetBalanceParams { - account_index: index, - }; - let request = Request::new("get_balance", params); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!( - "get balance of account index {} RPC response: {}", - index, response - ); - - let r = serde_json::from_str::>(&response)?; - - let balance = r.result.balance; - - Ok(balance) - } - - pub async fn create_account(&self, label: &str) -> Result { - let params = LabelParams { - label: label.to_owned(), - }; - let request = Request::new("create_account", params); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("create account RPC response: {}", response); - - let r = serde_json::from_str::>(&response)?; - Ok(r.result) - } - - /// Get accounts, filtered by tag ("" for no filtering). - pub async fn get_accounts(&self, tag: &str) -> Result { - let params = TagParams { - tag: tag.to_owned(), - }; - let request = Request::new("get_accounts", params); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("get accounts RPC response: {}", response); - - let r = serde_json::from_str::>(&response)?; - - Ok(r.result) - } - - /// Opens a wallet using `filename`. - pub async fn open_wallet(&self, filename: &str) -> Result<()> { - let params = OpenWalletParams { - filename: filename.to_owned(), - }; - let request = Request::new("open_wallet", params); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("open wallet RPC response: {}", response); - - // TODO: Proper error handling once switching to https://github.com/thomaseizinger/rust-jsonrpc-client/ - // Currently blocked by https://github.com/thomaseizinger/rust-jsonrpc-client/issues/20 - if response.contains("error") { - bail!("Failed to open wallet") - } - - Ok(()) - } - - /// Close the currently opened wallet, after trying to save it. - pub async fn close_wallet(&self) -> Result<()> { - let request = Request::new("close_wallet", ""); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("close wallet RPC response: {}", response); - - if response.contains("error") { - bail!("Failed to close wallet") - } - - Ok(()) - } - - /// Creates a wallet using `filename`. - pub async fn create_wallet(&self, filename: &str) -> Result<()> { - let params = CreateWalletParams { - filename: filename.to_owned(), - language: "English".to_owned(), - }; - let request = Request::new("create_wallet", params); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("create wallet RPC response: {}", response); - - if response.contains("error") { - bail!("Failed to create wallet") - } - - Ok(()) - } - - /// Transfers `amount` moneroj from `account_index` to `address`. - pub async fn transfer( - &self, - account_index: u32, - amount: u64, - address: &str, - ) -> Result { - let dest = vec![Destination { - amount, - address: address.to_owned(), - }]; - self.multi_transfer(account_index, dest).await - } - - /// Transfers moneroj from `account_index` to `destinations`. - pub async fn multi_transfer( - &self, - account_index: u32, - destinations: Vec, - ) -> Result { - let params = TransferParams { - account_index, - destinations, - get_tx_key: true, - }; - let request = Request::new("transfer", params); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("transfer RPC response: {}", response); - - let r = serde_json::from_str::>(&response)?; - Ok(r.result) - } - - /// Get wallet block height, this might be behind monerod height. - pub async fn block_height(&self) -> Result { - let request = Request::new("get_height", ""); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("wallet height RPC response: {}", response); - - let r = serde_json::from_str::>(&response)?; - Ok(r.result) - } - - /// Check a transaction in the blockchain with its secret key. - pub async fn check_tx_key( - &self, - tx_id: &str, - tx_key: &str, - address: &str, - ) -> Result { - let params = CheckTxKeyParams { - tx_id: tx_id.to_owned(), - tx_key: tx_key.to_owned(), - address: address.to_owned(), - }; - let request = Request::new("check_tx_key", params); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("check_tx_key RPC response: {}", response); - - let check_tx_key = serde_json::from_str::>(&response)?; - let mut check_tx_key = check_tx_key.result; - - // Due to a bug in monerod that causes check_tx_key confirmations - // to overflow we safeguard the confirmations to avoid unwanted - // side effects. - if check_tx_key.confirmations > u64::MAX - 1000 { - check_tx_key.confirmations = 0u64; - } - - Ok(check_tx_key) - } - - pub async fn generate_from_keys( - &self, - filename: &str, - address: &str, - spend_key: &str, - view_key: &str, - restore_height: u32, - ) -> Result { - let params = GenerateFromKeysParams { - restore_height, - filename: filename.into(), - address: address.into(), - spendkey: spend_key.into(), - viewkey: view_key.into(), - password: "".into(), - autosave_current: true, - }; - let request = Request::new("generate_from_keys", params); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("generate_from_keys RPC response: {}", response); - - let r = serde_json::from_str::>(&response)?; - Ok(r.result) - } - - pub async fn refresh(&self) -> Result { - let request = Request::new("refresh", ""); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("refresh RPC response: {}", response); - - let r = serde_json::from_str::>(&response)?; - Ok(r.result) - } - - /// Transfers the complete balance of the account to `address`. - pub async fn sweep_all(&self, address: &str) -> Result { - let params = SweepAllParams { - address: address.into(), - }; - let request = Request::new("sweep_all", params); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("sweep_all RPC response: {}", response); - - let r = serde_json::from_str::>(&response)?; - Ok(r.result) - } - - pub async fn get_version(&self) -> Result { - let request = Request::new("get_version", ""); - - let response = self - .inner - .post(self.url.clone()) - .json(&request) - .send() - .await? - .text() - .await?; - - debug!("get_version RPC response: {}", response); - - let r = serde_json::from_str::>(&response)?; - Ok(r.result) - } -} - -#[derive(Serialize, Debug, Clone)] -struct GetAddressParams { - account_index: u32, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct GetAddress { - pub address: String, -} - -#[derive(Serialize, Debug, Clone)] -struct GetBalanceParams { - account_index: u32, -} - -#[derive(Deserialize, Debug, Clone)] -struct GetBalance { - balance: u64, - blocks_to_unlock: u32, - multisig_import_needed: bool, - time_to_unlock: u32, - unlocked_balance: u64, -} - -#[derive(Serialize, Debug, Clone)] -struct LabelParams { - label: String, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct CreateAccount { - pub account_index: u32, - pub address: String, -} - -#[derive(Serialize, Debug, Clone)] -struct TagParams { - tag: String, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct GetAccounts { - pub subaddress_accounts: Vec, - pub total_balance: u64, - pub total_unlocked_balance: u64, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct SubAddressAccount { - pub account_index: u32, - pub balance: u32, - pub base_address: String, - pub label: String, - pub tag: String, - pub unlocked_balance: u64, -} - -#[derive(Serialize, Debug, Clone)] -struct OpenWalletParams { - filename: String, -} - -#[derive(Serialize, Debug, Clone)] -struct CreateWalletParams { - filename: String, - language: String, -} - -#[derive(Serialize, Debug, Clone)] -struct TransferParams { - // Transfer from this account. - account_index: u32, - // Destinations to receive XMR: - destinations: Vec, - // Return the transaction key after sending. - get_tx_key: bool, -} - -#[derive(Serialize, Debug, Clone)] -pub struct Destination { - amount: u64, - address: String, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct Transfer { - pub amount: u64, - pub fee: u64, - pub multisig_txset: String, - pub tx_blob: String, - pub tx_hash: String, - pub tx_key: String, - pub tx_metadata: String, - pub unsigned_txset: String, -} - -#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)] -pub struct BlockHeight { - pub height: u32, -} - -#[derive(Serialize, Debug, Clone)] -struct CheckTxKeyParams { - #[serde(rename = "txid")] - tx_id: String, - tx_key: String, - address: String, -} - -#[derive(Clone, Copy, Debug, Deserialize)] -pub struct CheckTxKey { - pub confirmations: u64, - pub received: u64, -} - -#[derive(Clone, Debug, Serialize)] -pub struct GenerateFromKeysParams { - pub restore_height: u32, - pub filename: String, - pub address: String, - pub spendkey: String, - pub viewkey: String, - pub password: String, - pub autosave_current: bool, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct GenerateFromKeys { - pub address: String, - pub info: String, -} - -#[derive(Clone, Copy, Debug, Deserialize)] -pub struct Refreshed { - pub blocks_fetched: u32, - pub received_money: bool, -} - -#[derive(Debug, Clone, Serialize)] -pub struct SweepAllParams { - pub address: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct SweepAll { - amount_list: Vec, - fee_list: Vec, - multisig_txset: String, - pub tx_hash_list: Vec, - unsigned_txset: String, - weight_list: Vec, -} - -#[derive(Debug, Copy, Clone, Deserialize)] -pub struct Version { - version: u32, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn can_deserialize_sweep_all_response() { - let response = r#"{ - "id": "0", - "jsonrpc": "2.0", - "result": { - "amount_list": [29921410000], - "fee_list": [78590000], - "multisig_txset": "", - "tx_hash_list": ["c1d8cfa87d445c1915a59d67be3e93ba8a29018640cf69b465f07b1840a8f8c8"], - "unsigned_txset": "", - "weight_list": [1448] - } - }"#; - - let _: Response = serde_json::from_str(&response).unwrap(); - } -} diff --git a/monero-rpc/src/wallet.rs b/monero-rpc/src/wallet.rs new file mode 100644 index 00000000..4f10f6ab --- /dev/null +++ b/monero-rpc/src/wallet.rs @@ -0,0 +1,260 @@ +use anyhow::{Context, Result}; +use serde::{de::Error, Deserialize, Deserializer, Serialize}; + +#[jsonrpc_client::api(version = "2.0")] +pub trait MoneroWalletRpc { + async fn get_address(&self, account_index: u32) -> GetAddress; + async fn get_balance(&self, account_index: u32) -> GetBalance; + async fn create_account(&self, label: String) -> CreateAccount; + async fn get_accounts(&self, tag: String) -> GetAccounts; + async fn open_wallet(&self, filename: String) -> WalletOpened; + async fn close_wallet(&self) -> WalletClosed; + async fn create_wallet(&self, filename: String, language: String) -> WalletCreated; + async fn transfer( + &self, + account_index: u32, + destinations: Vec, + get_tx_key: bool, + ) -> Transfer; + async fn get_height(&self) -> BlockHeight; + async fn check_tx_key(&self, txid: String, tx_key: String, address: String) -> CheckTxKey; + #[allow(clippy::too_many_arguments)] + async fn generate_from_keys( + &self, + filename: String, + address: String, + spendkey: String, + viewkey: String, + restore_height: u32, + password: String, + autosave_current: bool, + ) -> GenerateFromKeys; + async fn refresh(&self) -> Refreshed; + async fn sweep_all(&self, address: String) -> SweepAll; + async fn get_version(&self) -> Version; +} + +#[jsonrpc_client::implement(MoneroWalletRpc)] +#[derive(Debug, Clone)] +pub struct Client { + inner: reqwest::Client, + base_url: reqwest::Url, +} + +impl Client { + /// Constructs a monero-wallet-rpc client with localhost endpoint. + pub fn localhost(port: u16) -> Result { + Client::new( + format!("http://127.0.0.1:{}/json_rpc", port) + .parse() + .context("url is well formed")?, + ) + } + + /// Constructs a monero-wallet-rpc client with `url` endpoint. + pub fn new(url: reqwest::Url) -> Result { + Ok(Self { + inner: reqwest::ClientBuilder::new() + .connection_verbose(true) + .build()?, + base_url: url, + }) + } + + /// Transfers `amount` monero from `account_index` to `address`. + pub async fn transfer_single( + &self, + account_index: u32, + amount: u64, + address: &str, + ) -> Result { + let dest = vec![Destination { + amount, + address: address.to_owned(), + }]; + + Ok(self.transfer(account_index, dest, true).await?) + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct GetAddress { + pub address: String, +} + +#[derive(Deserialize, Debug, Clone, Copy)] +pub struct GetBalance { + pub balance: u64, + pub blocks_to_unlock: u32, + pub multisig_import_needed: bool, + pub time_to_unlock: u32, + pub unlocked_balance: u64, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct CreateAccount { + pub account_index: u32, + pub address: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct GetAccounts { + pub subaddress_accounts: Vec, + pub total_balance: u64, + pub total_unlocked_balance: u64, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct SubAddressAccount { + pub account_index: u32, + pub balance: u32, + pub base_address: String, + pub label: String, + pub tag: String, + pub unlocked_balance: u64, +} + +#[derive(Serialize, Debug, Clone)] +pub struct Destination { + pub amount: u64, + pub address: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Transfer { + pub amount: u64, + pub fee: u64, + pub multisig_txset: String, + pub tx_blob: String, + pub tx_hash: String, + #[serde(deserialize_with = "opt_key_from_blank")] + pub tx_key: Option, + pub tx_metadata: String, + pub unsigned_txset: String, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)] +pub struct BlockHeight { + pub height: u32, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(from = "CheckTxKeyResponse")] +pub struct CheckTxKey { + pub confirmations: u64, + pub received: u64, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +struct CheckTxKeyResponse { + pub confirmations: u64, + pub received: u64, +} + +impl From for CheckTxKey { + fn from(response: CheckTxKeyResponse) -> Self { + // Due to a bug in monerod that causes check_tx_key confirmations + // to overflow we safeguard the confirmations to avoid unwanted + // side effects. + let confirmations = if response.confirmations > u64::MAX - 1000 { + 0 + } else { + response.confirmations + }; + + CheckTxKey { + confirmations, + received: response.received, + } + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct GenerateFromKeys { + pub address: String, + pub info: String, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +pub struct Refreshed { + pub blocks_fetched: u32, + pub received_money: bool, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct SweepAll { + amount_list: Vec, + fee_list: Vec, + multisig_txset: String, + pub tx_hash_list: Vec, + unsigned_txset: String, + weight_list: Vec, +} + +#[derive(Debug, Copy, Clone, Deserialize)] +pub struct Version { + pub version: u32, +} + +pub type WalletCreated = Empty; +pub type WalletClosed = Empty; +pub type WalletOpened = Empty; + +/// Zero-sized struct to allow serde to deserialize an empty JSON object. +/// +/// With `serde`, an empty JSON object (`{ }`) does not deserialize into Rust's +/// `()`. With the adoption of `jsonrpc_client`, we need to be explicit about +/// what the response of every RPC call is. Unfortunately, monerod likes to +/// return empty objects instead of `null`s in certain cases. We use this struct +/// to all the "deserialization" to happily continue. +#[derive(Debug, Copy, Clone, Deserialize)] +pub struct Empty {} + +fn opt_key_from_blank<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let string = String::deserialize(deserializer)?; + + if string.is_empty() { + return Ok(None); + } + + Ok(Some(string.parse().map_err(D::Error::custom)?)) +} + +#[cfg(test)] +mod tests { + use super::*; + use jsonrpc_client::Response; + + #[test] + fn can_deserialize_sweep_all_response() { + let response = r#"{ + "id": "0", + "jsonrpc": "2.0", + "result": { + "amount_list": [29921410000], + "fee_list": [78590000], + "multisig_txset": "", + "tx_hash_list": ["c1d8cfa87d445c1915a59d67be3e93ba8a29018640cf69b465f07b1840a8f8c8"], + "unsigned_txset": "", + "weight_list": [1448] + } + }"#; + + let _: Response = serde_json::from_str(&response).unwrap(); + } + + #[test] + fn can_deserialize_create_wallet() { + let response = r#"{ + "id": 0, + "jsonrpc": "2.0", + "result": { + } + }"#; + + let _: Response = serde_json::from_str(&response).unwrap(); + } +} diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 7662506d..e66c7514 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -27,7 +27,7 @@ use swap::env::{Config, GetConfig}; use swap::network::quote::BidQuote; use swap::network::swarm; use swap::protocol::bob; -use swap::protocol::bob::{Builder, EventLoop}; +use swap::protocol::bob::{EventLoop, Swap}; use swap::seed::Seed; use swap::{bitcoin, cli, env, monero}; use tracing::{debug, error, info, warn}; @@ -105,17 +105,16 @@ async fn main() -> Result<()> { db.insert_peer_id(swap_id, alice_peer_id).await?; - let swap = Builder::new( + let swap = Swap::new( db, swap_id, - bitcoin_wallet.clone(), + bitcoin_wallet, Arc::new(monero_wallet), env_config, event_loop_handle, receive_monero_address, - ) - .with_init_params(send_bitcoin) - .build()?; + send_bitcoin, + ); tokio::select! { result = event_loop => { @@ -184,16 +183,15 @@ async fn main() -> Result<()> { EventLoop::new(swap_id, swarm, alice_peer_id, bitcoin_wallet.clone())?; let handle = tokio::spawn(event_loop.run()); - let swap = Builder::new( + let swap = Swap::from_db( db, swap_id, - bitcoin_wallet.clone(), + bitcoin_wallet, Arc::new(monero_wallet), env_config, event_loop_handle, receive_monero_address, - ) - .build()?; + )?; tokio::select! { event_loop_result = handle => { diff --git a/swap/src/monero/wallet.rs b/swap/src/monero/wallet.rs index b3561281..93af277e 100644 --- a/swap/src/monero/wallet.rs +++ b/swap/src/monero/wallet.rs @@ -5,7 +5,7 @@ use crate::monero::{ use ::monero::{Address, Network, PrivateKey, PublicKey}; use anyhow::{Context, Result}; use monero_rpc::wallet; -use monero_rpc::wallet::{BlockHeight, CheckTxKey, Refreshed}; +use monero_rpc::wallet::{BlockHeight, CheckTxKey, MoneroWalletRpc as _, Refreshed}; use std::future::Future; use std::str::FromStr; use std::time::Duration; @@ -25,11 +25,11 @@ pub struct Wallet { impl Wallet { /// Connect to a wallet RPC and load the given wallet by name. pub async fn open_or_create(url: Url, name: String, env_config: Config) -> Result { - let client = wallet::Client::new(url); + let client = wallet::Client::new(url)?; - let open_wallet_response = client.open_wallet(name.as_str()).await; + let open_wallet_response = client.open_wallet(name.clone()).await; if open_wallet_response.is_err() { - client.create_wallet(name.as_str()).await.context( + client.create_wallet(name.clone(), "English".to_owned()).await.context( "Unable to create Monero wallet, please ensure that the monero-wallet-rpc is available", )?; @@ -59,12 +59,12 @@ impl Wallet { self.inner .lock() .await - .open_wallet(self.name.as_str()) + .open_wallet(self.name.clone()) .await?; Ok(()) } - pub async fn open(&self, filename: &str) -> Result<()> { + pub async fn open(&self, filename: String) -> Result<()> { self.inner.lock().await.open_wallet(filename).await?; Ok(()) } @@ -73,7 +73,7 @@ impl Wallet { /// keys. The generated wallet will remain loaded. pub async fn create_from_and_load( &self, - file_name: &str, + file_name: String, private_spend_key: PrivateKey, private_view_key: PrivateViewKey, restore_height: BlockHeight, @@ -87,17 +87,23 @@ impl Wallet { // Properly close the wallet before generating the other wallet to ensure that // it saves its state correctly - let _ = wallet.close_wallet().await?; + let _ = wallet + .close_wallet() + .await + .context("Failed to close wallet")?; let _ = wallet .generate_from_keys( file_name, - &address.to_string(), - &private_spend_key.to_string(), - &PrivateKey::from(private_view_key).to_string(), + address.to_string(), + private_spend_key.to_string(), + PrivateKey::from(private_view_key).to_string(), restore_height.height, + String::from(""), + true, ) - .await?; + .await + .context("Failed to generate new wallet from keys")?; Ok(()) } @@ -108,7 +114,7 @@ impl Wallet { /// stored name. pub async fn create_from( &self, - file_name: &str, + file_name: String, private_spend_key: PrivateKey, private_view_key: PrivateViewKey, restore_height: BlockHeight, @@ -128,19 +134,18 @@ impl Wallet { let _ = wallet .generate_from_keys( file_name, - &temp_wallet_address.to_string(), - &private_spend_key.to_string(), - &PrivateKey::from(private_view_key).to_string(), + temp_wallet_address.to_string(), + private_spend_key.to_string(), + PrivateKey::from(private_view_key).to_string(), restore_height.height, + String::from(""), + true, ) .await?; // Try to send all the funds from the generated wallet to the default wallet match wallet.refresh().await { - Ok(_) => match wallet - .sweep_all(self.main_address.to_string().as_str()) - .await - { + Ok(_) => match wallet.sweep_all(self.main_address.to_string()).await { Ok(sweep_all) => { for tx in sweep_all.tx_hash_list { tracing::info!(%tx, "Monero transferred back to default wallet {}", self.main_address); @@ -159,7 +164,7 @@ impl Wallet { } } - let _ = wallet.open_wallet(self.name.as_str()).await?; + let _ = wallet.open_wallet(self.name.clone()).await?; Ok(()) } @@ -178,7 +183,7 @@ impl Wallet { .inner .lock() .await - .transfer(0, amount.as_piconero(), &destination_address.to_string()) + .transfer_single(0, amount.as_piconero(), &destination_address.to_string()) .await?; tracing::debug!( @@ -190,7 +195,8 @@ impl Wallet { Ok(TransferProof::new( TxHash(res.tx_hash), - PrivateKey::from_str(&res.tx_key)?, + res.tx_key + .context("Missing tx_key in `transfer` response")?, )) } @@ -210,16 +216,20 @@ impl Wallet { let address = Address::standard(self.network, public_spend_key, public_view_key.into()); let check_interval = tokio::time::interval(self.sync_interval); - let key = &transfer_proof.tx_key().to_string(); + let key = transfer_proof.tx_key().to_string(); wait_for_confirmations( txid.0, - |txid| async move { - self.inner - .lock() - .await - .check_tx_key(&txid, &key, &address.to_string()) - .await + move |txid| { + let key = key.clone(); + async move { + Ok(self + .inner + .lock() + .await + .check_tx_key(txid, key, address.to_string()) + .await?) + } }, check_interval, expected, @@ -235,7 +245,7 @@ impl Wallet { .inner .lock() .await - .sweep_all(address.to_string().as_str()) + .sweep_all(address.to_string()) .await?; let tx_hashes = sweep_all.tx_hash_list.into_iter().map(TxHash).collect(); @@ -244,13 +254,13 @@ impl Wallet { /// Get the balance of the primary account. pub async fn get_balance(&self) -> Result { - let amount = self.inner.lock().await.get_balance(0).await?; + let amount = self.inner.lock().await.get_balance(0).await?.balance; Ok(Amount::from_piconero(amount)) } pub async fn block_height(&self) -> Result { - self.inner.lock().await.block_height().await + Ok(self.inner.lock().await.get_height().await?) } pub fn get_main_address(&self) -> Address { @@ -258,7 +268,7 @@ impl Wallet { } pub async fn refresh(&self) -> Result { - self.inner.lock().await.refresh().await + Ok(self.inner.lock().await.refresh().await?) } pub fn static_tx_fee_estimate(&self) -> Amount { diff --git a/swap/src/monero/wallet_rpc.rs b/swap/src/monero/wallet_rpc.rs index 96138098..273337ef 100644 --- a/swap/src/monero/wallet_rpc.rs +++ b/swap/src/monero/wallet_rpc.rs @@ -2,7 +2,7 @@ use ::monero::Network; use anyhow::{Context, Result}; use big_bytes::BigByte; use futures::{StreamExt, TryStreamExt}; -use monero_rpc::wallet::Client; +use monero_rpc::wallet::{Client, MoneroWalletRpc as _}; use reqwest::header::CONTENT_LENGTH; use reqwest::Url; use std::io::ErrorKind; @@ -165,7 +165,7 @@ impl WalletRpc { } // Send a json rpc request to make sure monero_wallet_rpc is ready - Client::localhost(port).get_version().await?; + Client::localhost(port)?.get_version().await?; Ok(WalletRpcProcess { _child: child, diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 57e66a9f..6f97f2c0 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -2,9 +2,8 @@ //! Alice holds XMR and wishes receive BTC. use crate::bitcoin::ExpiredTimelocks; use crate::env::Config; -use crate::protocol::alice; use crate::protocol::alice::event_loop::EventLoopHandle; -use crate::protocol::alice::AliceState; +use crate::protocol::alice::{AliceState, Swap}; use crate::{bitcoin, database, monero}; use anyhow::{bail, Context, Result}; use tokio::select; @@ -12,15 +11,12 @@ use tokio::time::timeout; use tracing::{error, info}; use uuid::Uuid; -pub async fn run(swap: alice::Swap) -> Result { +pub async fn run(swap: Swap) -> Result { run_until(swap, |_| false).await } #[tracing::instrument(name = "swap", skip(swap,exit_early), fields(id = %swap.swap_id), err)] -pub async fn run_until( - mut swap: alice::Swap, - exit_early: fn(&AliceState) -> bool, -) -> Result { +pub async fn run_until(mut swap: Swap, exit_early: fn(&AliceState) -> bool) -> Result { let mut current_state = swap.state; while !is_complete(¤t_state) && !exit_early(¤t_state) { @@ -104,7 +100,13 @@ async fn next_state( ExpiredTimelocks::None => { monero_wallet .watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1)) - .await?; + .await + .with_context(|| { + format!( + "Failed to watch for transfer of XMR in transaction {}", + transfer_proof.tx_hash() + ) + })?; AliceState::XmrLocked { monero_wallet_restore_blockheight, @@ -299,7 +301,7 @@ async fn next_state( monero_wallet .create_from( - &swap_id.to_string(), + swap_id.to_string(), spend_key, view_key, monero_wallet_restore_blockheight, diff --git a/swap/src/protocol/bob.rs b/swap/src/protocol/bob.rs index 6d045aa3..fbcc7dd1 100644 --- a/swap/src/protocol/bob.rs +++ b/swap/src/protocol/bob.rs @@ -1,23 +1,17 @@ use crate::database::Database; -use crate::env::Config; -use crate::network::quote::BidQuote; -use crate::network::{encrypted_signature, quote, redial, spot_price, transfer_proof}; -use crate::protocol::bob; -use crate::{bitcoin, monero}; -use anyhow::{anyhow, Error, Result}; -use libp2p::core::Multiaddr; -use libp2p::request_response::{RequestId, ResponseChannel}; -use libp2p::{NetworkBehaviour, PeerId}; +use crate::{bitcoin, env, monero}; +use anyhow::Result; use std::sync::Arc; use uuid::Uuid; +pub use self::behaviour::{Behaviour, OutEvent}; pub use self::cancel::cancel; pub use self::event_loop::{EventLoop, EventLoopHandle}; pub use self::refund::refund; pub use self::state::*; pub use self::swap::{run, run_until}; -use std::time::Duration; +mod behaviour; pub mod cancel; pub mod event_loop; mod execution_setup; @@ -27,160 +21,59 @@ pub mod swap; pub struct Swap { pub state: BobState, - pub event_loop_handle: bob::EventLoopHandle, + pub event_loop_handle: EventLoopHandle, pub db: Database, pub bitcoin_wallet: Arc, pub monero_wallet: Arc, - pub env_config: Config, - pub swap_id: Uuid, - pub receive_monero_address: ::monero::Address, + pub env_config: env::Config, + pub id: Uuid, + pub receive_monero_address: monero::Address, } -pub struct Builder { - swap_id: Uuid, - db: Database, - - bitcoin_wallet: Arc, - monero_wallet: Arc, - - init_params: InitParams, - env_config: Config, - - event_loop_handle: EventLoopHandle, - - receive_monero_address: ::monero::Address, -} - -enum InitParams { - None, - New { btc_amount: bitcoin::Amount }, -} - -impl Builder { +impl Swap { #[allow(clippy::too_many_arguments)] pub fn new( db: Database, - swap_id: Uuid, + id: Uuid, bitcoin_wallet: Arc, monero_wallet: Arc, - env_config: Config, + env_config: env::Config, event_loop_handle: EventLoopHandle, - receive_monero_address: ::monero::Address, + receive_monero_address: monero::Address, + btc_amount: bitcoin::Amount, ) -> Self { Self { - swap_id, + state: BobState::Started { btc_amount }, + event_loop_handle, db, bitcoin_wallet, monero_wallet, - init_params: InitParams::None, env_config, - event_loop_handle, + id, receive_monero_address, } } - pub fn with_init_params(self, btc_amount: bitcoin::Amount) -> Self { - Self { - init_params: InitParams::New { btc_amount }, - ..self - } - } + pub fn from_db( + db: Database, + id: Uuid, + bitcoin_wallet: Arc, + monero_wallet: Arc, + env_config: env::Config, + event_loop_handle: EventLoopHandle, + receive_monero_address: monero::Address, + ) -> Result { + let state = db.get_state(id)?.try_into_bob()?.into(); - pub fn build(self) -> Result { - let state = match self.init_params { - InitParams::New { btc_amount } => BobState::Started { btc_amount }, - InitParams::None => self.db.get_state(self.swap_id)?.try_into_bob()?.into(), - }; - - Ok(Swap { + Ok(Self { state, - event_loop_handle: self.event_loop_handle, - db: self.db, - bitcoin_wallet: self.bitcoin_wallet.clone(), - monero_wallet: self.monero_wallet.clone(), - swap_id: self.swap_id, - env_config: self.env_config, - receive_monero_address: self.receive_monero_address, + event_loop_handle, + db, + bitcoin_wallet, + monero_wallet, + env_config, + id, + receive_monero_address, }) } } - -#[derive(Debug)] -pub enum OutEvent { - QuoteReceived { - id: RequestId, - response: BidQuote, - }, - SpotPriceReceived { - id: RequestId, - response: spot_price::Response, - }, - ExecutionSetupDone(Box>), - TransferProofReceived { - msg: Box, - channel: ResponseChannel<()>, - }, - EncryptedSignatureAcknowledged { - id: RequestId, - }, - AllRedialAttemptsExhausted { - peer: PeerId, - }, - Failure { - peer: PeerId, - error: Error, - }, - /// "Fallback" variant that allows the event mapping code to swallow certain - /// events that we don't want the caller to deal with. - Other, -} - -impl OutEvent { - pub fn unexpected_request(peer: PeerId) -> OutEvent { - OutEvent::Failure { - peer, - error: anyhow!("Unexpected request received"), - } - } - - pub fn unexpected_response(peer: PeerId) -> OutEvent { - OutEvent::Failure { - peer, - error: anyhow!("Unexpected response received"), - } - } -} - -/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. -#[derive(NetworkBehaviour)] -#[behaviour(out_event = "OutEvent", event_process = false)] -#[allow(missing_debug_implementations)] -pub struct Behaviour { - pub quote: quote::Behaviour, - pub spot_price: spot_price::Behaviour, - pub execution_setup: execution_setup::Behaviour, - pub transfer_proof: transfer_proof::Behaviour, - pub encrypted_signature: encrypted_signature::Behaviour, - pub redial: redial::Behaviour, -} - -impl Behaviour { - pub fn new(alice: PeerId) -> Self { - Self { - quote: quote::bob(), - spot_price: spot_price::bob(), - execution_setup: Default::default(), - transfer_proof: transfer_proof::bob(), - encrypted_signature: encrypted_signature::bob(), - redial: redial::Behaviour::new(alice, Duration::from_secs(2)), - } - } - - /// Add a known address for the given peer - pub fn add_address(&mut self, peer_id: PeerId, address: Multiaddr) { - self.quote.add_address(&peer_id, address.clone()); - self.spot_price.add_address(&peer_id, address.clone()); - self.transfer_proof.add_address(&peer_id, address.clone()); - self.encrypted_signature.add_address(&peer_id, address); - } -} diff --git a/swap/src/protocol/bob/behaviour.rs b/swap/src/protocol/bob/behaviour.rs new file mode 100644 index 00000000..06ca9a8c --- /dev/null +++ b/swap/src/protocol/bob/behaviour.rs @@ -0,0 +1,88 @@ +use crate::network::quote::BidQuote; +use crate::network::{encrypted_signature, quote, redial, spot_price, transfer_proof}; +use crate::protocol::bob::{execution_setup, State2}; +use anyhow::{anyhow, Error, Result}; +use libp2p::core::Multiaddr; +use libp2p::request_response::{RequestId, ResponseChannel}; +use libp2p::{NetworkBehaviour, PeerId}; +use std::time::Duration; + +#[derive(Debug)] +pub enum OutEvent { + QuoteReceived { + id: RequestId, + response: BidQuote, + }, + SpotPriceReceived { + id: RequestId, + response: spot_price::Response, + }, + ExecutionSetupDone(Box>), + TransferProofReceived { + msg: Box, + channel: ResponseChannel<()>, + }, + EncryptedSignatureAcknowledged { + id: RequestId, + }, + AllRedialAttemptsExhausted { + peer: PeerId, + }, + Failure { + peer: PeerId, + error: Error, + }, + /// "Fallback" variant that allows the event mapping code to swallow certain + /// events that we don't want the caller to deal with. + Other, +} + +impl OutEvent { + pub fn unexpected_request(peer: PeerId) -> OutEvent { + OutEvent::Failure { + peer, + error: anyhow!("Unexpected request received"), + } + } + + pub fn unexpected_response(peer: PeerId) -> OutEvent { + OutEvent::Failure { + peer, + error: anyhow!("Unexpected response received"), + } + } +} + +/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "OutEvent", event_process = false)] +#[allow(missing_debug_implementations)] +pub struct Behaviour { + pub quote: quote::Behaviour, + pub spot_price: spot_price::Behaviour, + pub execution_setup: execution_setup::Behaviour, + pub transfer_proof: transfer_proof::Behaviour, + pub encrypted_signature: encrypted_signature::Behaviour, + pub redial: redial::Behaviour, +} + +impl Behaviour { + pub fn new(alice: PeerId) -> Self { + Self { + quote: quote::bob(), + spot_price: spot_price::bob(), + execution_setup: Default::default(), + transfer_proof: transfer_proof::bob(), + encrypted_signature: encrypted_signature::bob(), + redial: redial::Behaviour::new(alice, Duration::from_secs(2)), + } + } + + /// Add a known address for the given peer + pub fn add_address(&mut self, peer_id: PeerId, address: Multiaddr) { + self.quote.add_address(&peer_id, address.clone()); + self.spot_price.add_address(&peer_id, address.clone()); + self.transfer_proof.add_address(&peer_id, address.clone()); + self.encrypted_signature.add_address(&peer_id, address); + } +} diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 84b8257f..88ad040d 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -33,7 +33,7 @@ pub async fn run_until( while !is_target_state(¤t_state) { current_state = next_state( - swap.swap_id, + swap.id, current_state, &mut swap.event_loop_handle, swap.bitcoin_wallet.as_ref(), @@ -45,7 +45,7 @@ pub async fn run_until( let db_state = current_state.clone().into(); swap.db - .insert_latest_state(swap.swap_id, Swap::Bob(db_state)) + .insert_latest_state(swap.id, Swap::Bob(db_state)) .await?; } @@ -197,21 +197,24 @@ async fn next_state( BobState::BtcRedeemed(state) => { let (spend_key, view_key) = state.xmr_keys(); - let generated_wallet_file_name = &swap_id.to_string(); - if monero_wallet + let generated_wallet_file_name = swap_id.to_string(); + if let Err(e) = monero_wallet .create_from_and_load( - generated_wallet_file_name, + generated_wallet_file_name.clone(), spend_key, view_key, state.monero_wallet_restore_blockheight, ) .await - .is_err() { // In case we failed to refresh/sweep, when resuming the wallet might already // exist! This is a very unlikely scenario, but if we don't take care of it we // might not be able to ever transfer the Monero. - tracing::warn!("Failed to generate monero wallet from keys, falling back to trying to open the the wallet if it already exists: {}", swap_id); + tracing::warn!("Failed to generate monero wallet from keys: {:#}", e); + tracing::info!( + "Falling back to trying to open the the wallet if it already exists: {}", + swap_id + ); monero_wallet.open(generated_wallet_file_name).await?; } diff --git a/swap/tests/alice_punishes_after_restart_punish_timelock_expired.rs b/swap/tests/alice_punishes_after_restart_punish_timelock_expired.rs index 0173b1d0..9157a19c 100644 --- a/swap/tests/alice_punishes_after_restart_punish_timelock_expired.rs +++ b/swap/tests/alice_punishes_after_restart_punish_timelock_expired.rs @@ -13,7 +13,7 @@ use swap::protocol::{alice, bob}; async fn alice_punishes_after_restart_if_punish_timelock_expired() { harness::setup_test(FastPunishConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; - let bob_swap_id = bob_swap.swap_id; + let bob_swap_id = bob_swap.id; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); let alice_swap = ctx.alice_next_swap().await; diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command.rs index 71a902a8..d218341e 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command.rs @@ -9,7 +9,7 @@ use swap::protocol::{alice, bob}; async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() { harness::setup_test(FastCancelConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; - let bob_swap_id = bob_swap.swap_id; + let bob_swap_id = bob_swap.id; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); let alice_swap = ctx.alice_next_swap().await; @@ -37,7 +37,7 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() { // Bob manually cancels bob_join_handle.abort(); let (_, state) = bob::cancel( - bob_swap.swap_id, + bob_swap.id, bob_swap.state, bob_swap.bitcoin_wallet, bob_swap.db, @@ -54,7 +54,7 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() { // Bob manually refunds bob_join_handle.abort(); let bob_state = bob::refund( - bob_swap.swap_id, + bob_swap.id, bob_swap.state, bob_swap.bitcoin_wallet, bob_swap.db, diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs index 49d73a58..5c8e0f20 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs @@ -10,7 +10,7 @@ use swap::protocol::{alice, bob}; async fn given_bob_manually_cancels_when_timelock_not_expired_errors() { harness::setup_test(SlowCancelConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; - let bob_swap_id = bob_swap.swap_id; + let bob_swap_id = bob_swap.id; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); let alice_swap = ctx.alice_next_swap().await; @@ -26,7 +26,7 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() { // Bob tries but fails to manually cancel let result = bob::cancel( - bob_swap.swap_id, + bob_swap.id, bob_swap.state, bob_swap.bitcoin_wallet, bob_swap.db, @@ -45,7 +45,7 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() { // Bob tries but fails to manually refund bob::refund( - bob_swap.swap_id, + bob_swap.id, bob_swap.state, bob_swap.bitcoin_wallet, bob_swap.db, diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs index 057db955..df06416d 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs @@ -9,7 +9,7 @@ use swap::protocol::{alice, bob}; async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() { harness::setup_test(SlowCancelConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; - let bob_swap_id = bob_swap.swap_id; + let bob_swap_id = bob_swap.id; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); let alice_swap = ctx.alice_next_swap().await; @@ -25,7 +25,7 @@ async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() { // Bob forces a cancel that will fail let is_error = bob::cancel( - bob_swap.swap_id, + bob_swap.id, bob_swap.state, bob_swap.bitcoin_wallet, bob_swap.db, @@ -43,7 +43,7 @@ async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() { // Bob forces a refund that will fail let is_error = bob::refund( - bob_swap.swap_id, + bob_swap.id, bob_swap.state, bob_swap.bitcoin_wallet, bob_swap.db, diff --git a/swap/tests/concurrent_bobs_after_xmr_lock_proof_sent.rs b/swap/tests/concurrent_bobs_after_xmr_lock_proof_sent.rs index 11ae81b9..a580a6be 100644 --- a/swap/tests/concurrent_bobs_after_xmr_lock_proof_sent.rs +++ b/swap/tests/concurrent_bobs_after_xmr_lock_proof_sent.rs @@ -11,7 +11,7 @@ async fn concurrent_bobs_after_xmr_lock_proof_sent() { harness::setup_test(SlowCancelConfig, |mut ctx| async move { let (bob_swap_1, bob_join_handle_1) = ctx.bob_swap().await; - let swap_id = bob_swap_1.swap_id; + let swap_id = bob_swap_1.id; let bob_swap_1 = tokio::spawn(bob::run_until(bob_swap_1, is_xmr_locked)); diff --git a/swap/tests/concurrent_bobs_before_xmr_lock_proof_sent.rs b/swap/tests/concurrent_bobs_before_xmr_lock_proof_sent.rs index ed3efafe..a15d6cae 100644 --- a/swap/tests/concurrent_bobs_before_xmr_lock_proof_sent.rs +++ b/swap/tests/concurrent_bobs_before_xmr_lock_proof_sent.rs @@ -11,7 +11,7 @@ async fn concurrent_bobs_before_xmr_lock_proof_sent() { harness::setup_test(SlowCancelConfig, |mut ctx| async move { let (bob_swap_1, bob_join_handle_1) = ctx.bob_swap().await; - let swap_id = bob_swap_1.swap_id; + let swap_id = bob_swap_1.id; let bob_swap_1 = tokio::spawn(bob::run_until(bob_swap_1, is_btc_locked)); diff --git a/swap/tests/ensure_same_swap_id.rs b/swap/tests/ensure_same_swap_id.rs index 1dbc7046..48758ecd 100644 --- a/swap/tests/ensure_same_swap_id.rs +++ b/swap/tests/ensure_same_swap_id.rs @@ -7,7 +7,7 @@ use swap::protocol::bob; async fn ensure_same_swap_id_for_alice_and_bob() { harness::setup_test(SlowCancelConfig, |mut ctx| async move { let (bob_swap, _) = ctx.bob_swap().await; - let bob_swap_id = bob_swap.swap_id; + let bob_swap_id = bob_swap.id; let _ = tokio::spawn(bob::run(bob_swap)); // once Bob's swap is spawned we can retrieve Alice's swap and assert on the diff --git a/swap/tests/happy_path.rs b/swap/tests/happy_path.rs index 5f1ac5b3..2f50ff14 100644 --- a/swap/tests/happy_path.rs +++ b/swap/tests/happy_path.rs @@ -4,8 +4,6 @@ use harness::SlowCancelConfig; use swap::protocol::{alice, bob}; use tokio::join; -/// Run the following tests with RUST_MIN_STACK=10000000 - #[tokio::test] async fn happy_path() { harness::setup_test(SlowCancelConfig, |mut ctx| async move { diff --git a/swap/tests/happy_path_restart_bob_after_xmr_locked.rs b/swap/tests/happy_path_restart_bob_after_xmr_locked.rs index 8e18f5ed..a07fb2f2 100644 --- a/swap/tests/happy_path_restart_bob_after_xmr_locked.rs +++ b/swap/tests/happy_path_restart_bob_after_xmr_locked.rs @@ -9,7 +9,7 @@ use swap::protocol::{alice, bob}; async fn given_bob_restarts_after_xmr_is_locked_resume_swap() { harness::setup_test(SlowCancelConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; - let bob_swap_id = bob_swap.swap_id; + let bob_swap_id = bob_swap.id; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_xmr_locked)); let alice_swap = ctx.alice_next_swap().await; diff --git a/swap/tests/happy_path_restart_bob_before_xmr_locked.rs b/swap/tests/happy_path_restart_bob_before_xmr_locked.rs index 8e18f5ed..a07fb2f2 100644 --- a/swap/tests/happy_path_restart_bob_before_xmr_locked.rs +++ b/swap/tests/happy_path_restart_bob_before_xmr_locked.rs @@ -9,7 +9,7 @@ use swap::protocol::{alice, bob}; async fn given_bob_restarts_after_xmr_is_locked_resume_swap() { harness::setup_test(SlowCancelConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; - let bob_swap_id = bob_swap.swap_id; + let bob_swap_id = bob_swap.id; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_xmr_locked)); let alice_swap = ctx.alice_next_swap().await; diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index e23801b4..09ef421a 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -1,7 +1,6 @@ mod bitcoind; mod electrs; -use crate::harness; use anyhow::{bail, Context, Result}; use async_trait::async_trait; use bitcoin_harness::{BitcoindRpcApi, Client}; @@ -36,6 +35,293 @@ use tracing_subscriber::util::SubscriberInitExt; use url::Url; use uuid::Uuid; +pub async fn setup_test(_config: C, testfn: T) +where + T: Fn(TestContext) -> F, + F: Future>, + C: GetConfig, +{ + let cli = Cli::default(); + + let _guard = tracing_subscriber::fmt() + .with_env_filter("warn,swap=debug,monero_harness=debug,monero_rpc=debug,bitcoin_harness=info,testcontainers=info") // add `reqwest::connect::verbose=trace` if you want to logs of the RPC clients + .with_test_writer() + .set_default(); + + let env_config = C::get_config(); + + let (monero, containers) = init_containers(&cli).await; + monero.init_miner().await.unwrap(); + + let btc_amount = bitcoin::Amount::from_sat(1_000_000); + let xmr_amount = monero::Amount::from_monero(btc_amount.as_btc() / FixedRate::RATE).unwrap(); + + let alice_starting_balances = + StartingBalances::new(bitcoin::Amount::ZERO, xmr_amount, Some(10)); + + let electrs_rpc_port = containers + .electrs + .get_host_port(electrs::RPC_PORT) + .expect("Could not map electrs rpc port"); + + let alice_seed = Seed::random().unwrap(); + let (alice_bitcoin_wallet, alice_monero_wallet) = init_test_wallets( + MONERO_WALLET_NAME_ALICE, + containers.bitcoind_url.clone(), + &monero, + alice_starting_balances.clone(), + tempdir().unwrap().path(), + electrs_rpc_port, + &alice_seed, + env_config, + ) + .await; + + let alice_listen_port = get_port().expect("Failed to find a free port"); + let alice_listen_address: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", alice_listen_port) + .parse() + .expect("failed to parse Alice's address"); + + let alice_db_path = tempdir().unwrap().into_path(); + let (alice_handle, alice_swap_handle) = start_alice( + &alice_seed, + alice_db_path.clone(), + alice_listen_address.clone(), + env_config, + alice_bitcoin_wallet.clone(), + alice_monero_wallet.clone(), + ); + + let bob_seed = Seed::random().unwrap(); + let bob_starting_balances = StartingBalances::new(btc_amount * 10, monero::Amount::ZERO, None); + + let (bob_bitcoin_wallet, bob_monero_wallet) = init_test_wallets( + MONERO_WALLET_NAME_BOB, + containers.bitcoind_url, + &monero, + bob_starting_balances.clone(), + tempdir().unwrap().path(), + electrs_rpc_port, + &bob_seed, + env_config, + ) + .await; + + let bob_params = BobParams { + seed: Seed::random().unwrap(), + db_path: tempdir().unwrap().path().to_path_buf(), + bitcoin_wallet: bob_bitcoin_wallet.clone(), + monero_wallet: bob_monero_wallet.clone(), + alice_address: alice_listen_address.clone(), + alice_peer_id: alice_handle.peer_id, + env_config, + }; + + monero.start_miner().await.unwrap(); + + let test = TestContext { + env_config, + btc_amount, + xmr_amount, + alice_seed, + alice_db_path, + alice_listen_address, + alice_starting_balances, + alice_bitcoin_wallet, + alice_monero_wallet, + alice_swap_handle, + alice_handle, + bob_params, + bob_starting_balances, + bob_bitcoin_wallet, + bob_monero_wallet, + }; + + testfn(test).await.unwrap() +} + +async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) { + let prefix = random_prefix(); + let bitcoind_name = format!("{}_{}", prefix, "bitcoind"); + let (bitcoind, bitcoind_url) = + init_bitcoind_container(&cli, prefix.clone(), bitcoind_name.clone(), prefix.clone()) + .await + .expect("could not init bitcoind"); + let electrs = init_electrs_container(&cli, prefix.clone(), bitcoind_name, prefix) + .await + .expect("could not init electrs"); + let (monero, monerod_container, monero_wallet_rpc_containers) = + Monero::new(&cli, vec![MONERO_WALLET_NAME_ALICE, MONERO_WALLET_NAME_BOB]) + .await + .unwrap(); + + (monero, Containers { + bitcoind_url, + bitcoind, + monerod_container, + monero_wallet_rpc_containers, + electrs, + }) +} + +async fn init_bitcoind_container( + cli: &Cli, + volume: String, + name: String, + network: String, +) -> Result<(Container<'_, Cli, bitcoind::Bitcoind>, Url)> { + let image = bitcoind::Bitcoind::default().with_volume(volume); + + let run_args = RunArgs::default().with_name(name).with_network(network); + + let docker = cli.run_with_args(image, run_args); + let a = docker + .get_host_port(bitcoind::RPC_PORT) + .context("Could not map bitcoind rpc port")?; + + let bitcoind_url = { + let input = format!( + "http://{}:{}@localhost:{}", + bitcoind::RPC_USER, + bitcoind::RPC_PASSWORD, + a + ); + Url::parse(&input).unwrap() + }; + + init_bitcoind(bitcoind_url.clone(), 5).await?; + + Ok((docker, bitcoind_url.clone())) +} + +pub async fn init_electrs_container( + cli: &Cli, + volume: String, + bitcoind_container_name: String, + network: String, +) -> Result> { + let bitcoind_rpc_addr = format!("{}:{}", bitcoind_container_name, bitcoind::RPC_PORT); + let image = electrs::Electrs::default() + .with_volume(volume) + .with_daemon_rpc_addr(bitcoind_rpc_addr) + .with_tag("latest"); + + let run_args = RunArgs::default().with_network(network); + + let docker = cli.run_with_args(image, run_args); + + Ok(docker) +} + +fn start_alice( + seed: &Seed, + db_path: PathBuf, + listen_address: Multiaddr, + env_config: Config, + bitcoin_wallet: Arc, + monero_wallet: Arc, +) -> (AliceApplicationHandle, Receiver) { + let db = Arc::new(Database::open(db_path.as_path()).unwrap()); + + let mut swarm = swarm::alice(&seed).unwrap(); + swarm.listen_on(listen_address).unwrap(); + + let (event_loop, swap_handle) = alice::EventLoop::new( + swarm, + env_config, + bitcoin_wallet, + monero_wallet, + db, + FixedRate::default(), + bitcoin::Amount::ONE_BTC, + ) + .unwrap(); + + let peer_id = event_loop.peer_id(); + let handle = tokio::spawn(event_loop.run()); + + (AliceApplicationHandle { handle, peer_id }, swap_handle) +} + +#[allow(clippy::too_many_arguments)] +async fn init_test_wallets( + name: &str, + bitcoind_url: Url, + monero: &Monero, + starting_balances: StartingBalances, + datadir: &Path, + electrum_rpc_port: u16, + seed: &Seed, + env_config: Config, +) -> (Arc, Arc) { + monero + .init_wallet( + name, + starting_balances + .xmr_outputs + .into_iter() + .map(|amount| amount.as_piconero()) + .collect(), + ) + .await + .unwrap(); + + let xmr_wallet = swap::monero::Wallet::connect( + monero.wallet(name).unwrap().client().clone(), + name.to_string(), + env_config, + ) + .await + .unwrap(); + + let electrum_rpc_url = { + let input = format!("tcp://@localhost:{}", electrum_rpc_port); + Url::parse(&input).unwrap() + }; + + let btc_wallet = swap::bitcoin::Wallet::new( + electrum_rpc_url, + datadir, + seed.derive_extended_private_key(env_config.bitcoin_network) + .expect("Could not create extended private key from seed"), + env_config, + ) + .await + .expect("could not init btc wallet"); + + if starting_balances.btc != bitcoin::Amount::ZERO { + mint( + bitcoind_url, + btc_wallet.new_address().await.unwrap(), + starting_balances.btc, + ) + .await + .expect("could not mint btc starting balance"); + + let mut interval = interval(Duration::from_secs(1u64)); + let mut retries = 0u8; + let max_retries = 30u8; + loop { + retries += 1; + btc_wallet.sync().await.unwrap(); + + let btc_balance = btc_wallet.balance().await.unwrap(); + + if btc_balance == starting_balances.btc { + break; + } else if retries == max_retries { + panic!( + "Bitcoin wallet initialization failed, reached max retries upon balance sync" + ) + } + + interval.tick().await; + } + } + + (Arc::new(btc_wallet), Arc::new(xmr_wallet)) +} + const MONERO_WALLET_NAME_BOB: &str = "bob"; const MONERO_WALLET_NAME_ALICE: &str = "alice"; const BITCOIN_TEST_WALLET_NAME: &str = "testwallet"; @@ -97,22 +383,41 @@ struct BobParams { } impl BobParams { - pub async fn builder( - &self, - event_loop_handle: bob::EventLoopHandle, - swap_id: Uuid, - ) -> Result { - let receive_address = self.monero_wallet.get_main_address(); + pub fn new_swap_from_db(&self, swap_id: Uuid) -> Result<(bob::Swap, bob::EventLoop)> { + let (event_loop, handle) = self.new_eventloop(swap_id)?; + let db = Database::open(&self.db_path)?; - Ok(bob::Builder::new( - Database::open(&self.db_path.clone().as_path()).unwrap(), + let swap = bob::Swap::from_db( + db, swap_id, self.bitcoin_wallet.clone(), self.monero_wallet.clone(), self.env_config, - event_loop_handle, - receive_address, - )) + handle, + self.monero_wallet.get_main_address(), + )?; + + Ok((swap, event_loop)) + } + + pub fn new_swap(&self, btc_amount: bitcoin::Amount) -> Result<(bob::Swap, bob::EventLoop)> { + let swap_id = Uuid::new_v4(); + + let (event_loop, handle) = self.new_eventloop(swap_id)?; + let db = Database::open(&self.db_path)?; + + let swap = bob::Swap::new( + db, + swap_id, + self.bitcoin_wallet.clone(), + self.monero_wallet.clone(), + self.env_config, + handle, + self.monero_wallet.get_main_address(), + btc_amount, + ); + + Ok((swap, event_loop)) } pub fn new_eventloop(&self, swap_id: Uuid) -> Result<(bob::EventLoop, bob::EventLoopHandle)> { @@ -196,17 +501,7 @@ impl TestContext { } pub async fn bob_swap(&mut self) -> (bob::Swap, BobApplicationHandle) { - let swap_id = Uuid::new_v4(); - let (event_loop, event_loop_handle) = self.bob_params.new_eventloop(swap_id).unwrap(); - - let swap = self - .bob_params - .builder(event_loop_handle, swap_id) - .await - .unwrap() - .with_init_params(self.btc_amount) - .build() - .unwrap(); + let (swap, event_loop) = self.bob_params.new_swap(self.btc_amount).unwrap(); // ensure the wallet is up to date for concurrent swap tests swap.bitcoin_wallet.sync().await.unwrap(); @@ -223,15 +518,7 @@ impl TestContext { ) -> (bob::Swap, BobApplicationHandle) { join_handle.abort(); - let (event_loop, event_loop_handle) = self.bob_params.new_eventloop(swap_id).unwrap(); - - let swap = self - .bob_params - .builder(event_loop_handle, swap_id) - .await - .unwrap() - .build() - .unwrap(); + let (swap, event_loop) = self.bob_params.new_swap_from_db(swap_id).unwrap(); let join_handle = tokio::spawn(event_loop.run()); @@ -528,141 +815,6 @@ impl Wallet for bitcoin::Wallet { } } -pub async fn setup_test(_config: C, testfn: T) -where - T: Fn(TestContext) -> F, - F: Future>, - C: GetConfig, -{ - let cli = Cli::default(); - - let _guard = tracing_subscriber::fmt() - .with_env_filter("warn,swap=debug,monero_harness=debug,monero_rpc=debug,bitcoin_harness=info,testcontainers=info") - .with_test_writer() - .set_default(); - - let env_config = C::get_config(); - - let (monero, containers) = harness::init_containers(&cli).await; - monero.init_miner().await.unwrap(); - - let btc_amount = bitcoin::Amount::from_sat(1_000_000); - let xmr_amount = monero::Amount::from_monero(btc_amount.as_btc() / FixedRate::RATE).unwrap(); - - let alice_starting_balances = - StartingBalances::new(bitcoin::Amount::ZERO, xmr_amount, Some(10)); - - let electrs_rpc_port = containers - .electrs - .get_host_port(harness::electrs::RPC_PORT) - .expect("Could not map electrs rpc port"); - - let alice_seed = Seed::random().unwrap(); - let (alice_bitcoin_wallet, alice_monero_wallet) = init_test_wallets( - MONERO_WALLET_NAME_ALICE, - containers.bitcoind_url.clone(), - &monero, - alice_starting_balances.clone(), - tempdir().unwrap().path(), - electrs_rpc_port, - &alice_seed, - env_config, - ) - .await; - - let alice_listen_port = get_port().expect("Failed to find a free port"); - let alice_listen_address: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", alice_listen_port) - .parse() - .expect("failed to parse Alice's address"); - - let alice_db_path = tempdir().unwrap().into_path(); - let (alice_handle, alice_swap_handle) = start_alice( - &alice_seed, - alice_db_path.clone(), - alice_listen_address.clone(), - env_config, - alice_bitcoin_wallet.clone(), - alice_monero_wallet.clone(), - ); - - let bob_seed = Seed::random().unwrap(); - let bob_starting_balances = StartingBalances::new(btc_amount * 10, monero::Amount::ZERO, None); - - let (bob_bitcoin_wallet, bob_monero_wallet) = init_test_wallets( - MONERO_WALLET_NAME_BOB, - containers.bitcoind_url, - &monero, - bob_starting_balances.clone(), - tempdir().unwrap().path(), - electrs_rpc_port, - &bob_seed, - env_config, - ) - .await; - - let bob_params = BobParams { - seed: Seed::random().unwrap(), - db_path: tempdir().unwrap().path().to_path_buf(), - bitcoin_wallet: bob_bitcoin_wallet.clone(), - monero_wallet: bob_monero_wallet.clone(), - alice_address: alice_listen_address.clone(), - alice_peer_id: alice_handle.peer_id, - env_config, - }; - - monero.start_miner().await.unwrap(); - - let test = TestContext { - env_config, - btc_amount, - xmr_amount, - alice_seed, - alice_db_path, - alice_listen_address, - alice_starting_balances, - alice_bitcoin_wallet, - alice_monero_wallet, - alice_swap_handle, - alice_handle, - bob_params, - bob_starting_balances, - bob_bitcoin_wallet, - bob_monero_wallet, - }; - - testfn(test).await.unwrap() -} - -fn start_alice( - seed: &Seed, - db_path: PathBuf, - listen_address: Multiaddr, - env_config: Config, - bitcoin_wallet: Arc, - monero_wallet: Arc, -) -> (AliceApplicationHandle, Receiver) { - let db = Arc::new(Database::open(db_path.as_path()).unwrap()); - - let mut swarm = swarm::alice(&seed).unwrap(); - swarm.listen_on(listen_address).unwrap(); - - let (event_loop, swap_handle) = alice::EventLoop::new( - swarm, - env_config, - bitcoin_wallet, - monero_wallet, - db, - FixedRate::default(), - bitcoin::Amount::ONE_BTC, - ) - .unwrap(); - - let peer_id = event_loop.peer_id(); - let handle = tokio::spawn(event_loop.run()); - - (AliceApplicationHandle { handle, peer_id }, swap_handle) -} - fn random_prefix() -> String { use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; @@ -677,78 +829,6 @@ fn random_prefix() -> String { chars } -async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) { - let prefix = random_prefix(); - let bitcoind_name = format!("{}_{}", prefix, "bitcoind"); - let (bitcoind, bitcoind_url) = - init_bitcoind_container(&cli, prefix.clone(), bitcoind_name.clone(), prefix.clone()) - .await - .expect("could not init bitcoind"); - let electrs = init_electrs_container(&cli, prefix.clone(), bitcoind_name, prefix) - .await - .expect("could not init electrs"); - let (monero, monerods) = init_monero_container(&cli).await; - (monero, Containers { - bitcoind_url, - bitcoind, - monerods, - electrs, - }) -} - -async fn init_bitcoind_container( - cli: &Cli, - volume: String, - name: String, - network: String, -) -> Result<(Container<'_, Cli, bitcoind::Bitcoind>, Url)> { - let image = bitcoind::Bitcoind::default().with_volume(volume); - - let run_args = RunArgs::default().with_name(name).with_network(network); - - let docker = cli.run_with_args(image, run_args); - let a = docker - .get_host_port(harness::bitcoind::RPC_PORT) - .context("Could not map bitcoind rpc port")?; - - let bitcoind_url = { - let input = format!( - "http://{}:{}@localhost:{}", - bitcoind::RPC_USER, - bitcoind::RPC_PASSWORD, - a - ); - Url::parse(&input).unwrap() - }; - - init_bitcoind(bitcoind_url.clone(), 5).await?; - - Ok((docker, bitcoind_url.clone())) -} - -pub async fn init_electrs_container( - cli: &Cli, - volume: String, - bitcoind_container_name: String, - network: String, -) -> Result> { - let bitcoind_rpc_addr = format!( - "{}:{}", - bitcoind_container_name, - harness::bitcoind::RPC_PORT - ); - let image = electrs::Electrs::default() - .with_volume(volume) - .with_daemon_rpc_addr(bitcoind_rpc_addr) - .with_tag("latest"); - - let run_args = RunArgs::default().with_network(network); - - let docker = cli.run_with_args(image, run_args); - - Ok(docker) -} - async fn mine(bitcoind_client: Client, reward_address: bitcoin::Address) -> Result<()> { loop { tokio::time::sleep(Duration::from_secs(1)).await; @@ -798,107 +878,13 @@ pub async fn mint(node_url: Url, address: bitcoin::Address, amount: bitcoin::Amo Ok(()) } -async fn init_monero_container( - cli: &Cli, -) -> ( - Monero, - Vec>, -) { - let (monero, monerods) = Monero::new(&cli, vec![ - MONERO_WALLET_NAME_ALICE.to_string(), - MONERO_WALLET_NAME_BOB.to_string(), - ]) - .await - .unwrap(); - - (monero, monerods) -} - -#[allow(clippy::too_many_arguments)] -async fn init_test_wallets( - name: &str, - bitcoind_url: Url, - monero: &Monero, - starting_balances: StartingBalances, - datadir: &Path, - electrum_rpc_port: u16, - seed: &Seed, - env_config: Config, -) -> (Arc, Arc) { - monero - .init_wallet( - name, - starting_balances - .xmr_outputs - .into_iter() - .map(|amount| amount.as_piconero()) - .collect(), - ) - .await - .unwrap(); - - let xmr_wallet = swap::monero::Wallet::connect( - monero.wallet(name).unwrap().client(), - name.to_string(), - env_config, - ) - .await - .unwrap(); - - let electrum_rpc_url = { - let input = format!("tcp://@localhost:{}", electrum_rpc_port); - Url::parse(&input).unwrap() - }; - - let btc_wallet = swap::bitcoin::Wallet::new( - electrum_rpc_url, - datadir, - seed.derive_extended_private_key(env_config.bitcoin_network) - .expect("Could not create extended private key from seed"), - env_config, - ) - .await - .expect("could not init btc wallet"); - - if starting_balances.btc != bitcoin::Amount::ZERO { - mint( - bitcoind_url, - btc_wallet.new_address().await.unwrap(), - starting_balances.btc, - ) - .await - .expect("could not mint btc starting balance"); - - let mut interval = interval(Duration::from_secs(1u64)); - let mut retries = 0u8; - let max_retries = 30u8; - loop { - retries += 1; - btc_wallet.sync().await.unwrap(); - - let btc_balance = btc_wallet.balance().await.unwrap(); - - if btc_balance == starting_balances.btc { - break; - } else if retries == max_retries { - panic!( - "Bitcoin wallet initialization failed, reached max retries upon balance sync" - ) - } - - interval.tick().await; - } - } - - (Arc::new(btc_wallet), Arc::new(xmr_wallet)) -} - // This is just to keep the containers alive #[allow(dead_code)] struct Containers<'a> { bitcoind_url: Url, bitcoind: Container<'a, Cli, bitcoind::Bitcoind>, - monerods: Vec>, + monerod_container: Container<'a, Cli, image::Monerod>, + monero_wallet_rpc_containers: Vec>, electrs: Container<'a, Cli, electrs::Electrs>, } diff --git a/swap/tests/punish.rs b/swap/tests/punish.rs index bf053f5b..ef498d10 100644 --- a/swap/tests/punish.rs +++ b/swap/tests/punish.rs @@ -11,7 +11,7 @@ use swap::protocol::{alice, bob}; async fn alice_punishes_if_bob_never_acts_after_fund() { harness::setup_test(FastPunishConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; - let bob_swap_id = bob_swap.swap_id; + let bob_swap_id = bob_swap.id; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); let alice_swap = ctx.alice_next_swap().await;