diff --git a/monero-harness/src/lib.rs b/monero-harness/src/lib.rs index ab528564..f635da63 100644 --- a/monero-harness/src/lib.rs +++ b/monero-harness/src/lib.rs @@ -44,52 +44,42 @@ const WAIT_WALLET_SYNC_MILLIS: u64 = 1000; pub struct Monero { monerod: Monerod, wallets: Vec, - prefix: String, } impl<'c> Monero { /// Starts a new regtest monero container setup consisting out of 1 monerod - /// node and n wallets. The containers and network will be prefixed, either - /// randomly generated or as defined in `prefix` if provided. There will - /// be 1 miner wallet started automatically. Default monerod container - /// name will be: `prefix`_`monerod` Default miner wallet container name - /// will be: `prefix`_`miner` Default network will be: `prefix`_`monero` + /// node and n wallets. The docker container and network will be prefixed + /// with a randomly generated `prefix`. One miner wallet is started + /// automatically. + /// monerod container name is: `prefix`_`monerod` + /// network is: `prefix`_`monero` + /// miner wallet container name is: `miner` pub async fn new( cli: &'c Cli, - prefix: Option, additional_wallets: Vec, ) -> Result<(Self, Vec>)> { - let prefix = format!("{}_", prefix.unwrap_or_else(random_prefix)); - + 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); + tracing::info!("Starting monerod: {}", monerod_name); let (monerod, monerod_container) = Monerod::new(cli, monerod_name, network)?; let mut containers = vec![monerod_container]; let mut wallets = vec![]; - let miner = format!("{}{}", prefix, "miner"); - tracing::info!("Starting miner wallet... {}", miner); + let miner = "miner"; + tracing::info!("Starting miner wallet: {}", miner); let (miner_wallet, miner_container) = MoneroWalletRpc::new(cli, &miner, &monerod).await?; wallets.push(miner_wallet); containers.push(miner_container); for wallet in additional_wallets.iter() { - tracing::info!("Starting wallet: {}...", wallet); - let wallet = format!("{}{}", prefix, wallet); + tracing::info!("Starting wallet: {}", wallet); let (wallet, container) = MoneroWalletRpc::new(cli, &wallet, &monerod).await?; wallets.push(wallet); containers.push(container); } - Ok(( - Self { - monerod, - wallets, - prefix, - }, - containers, - )) + Ok((Self { monerod, wallets }, containers)) } pub fn monerod(&self) -> &Monerod { @@ -97,7 +87,6 @@ impl<'c> Monero { } pub fn wallet(&self, name: &str) -> Result<&MoneroWalletRpc> { - let name = format!("{}{}", self.prefix, name); let wallet = self .wallets .iter() diff --git a/monero-harness/tests/monerod.rs b/monero-harness/tests/monerod.rs index e48e3b87..7e816264 100644 --- a/monero-harness/tests/monerod.rs +++ b/monero-harness/tests/monerod.rs @@ -12,7 +12,7 @@ async fn init_miner_and_mine_to_miner_address() { let _guard = init_tracing(); let tc = Cli::default(); - let (monero, _monerod_container) = Monero::new(&tc, None, vec![]).await.unwrap(); + let (monero, _monerod_container) = Monero::new(&tc, vec![]).await.unwrap(); monero.init(vec![]).await.unwrap(); diff --git a/monero-harness/tests/wallet.rs b/monero-harness/tests/wallet.rs index a571a35e..3314341a 100644 --- a/monero-harness/tests/wallet.rs +++ b/monero-harness/tests/wallet.rs @@ -16,12 +16,9 @@ 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, Some("test_".to_string()), vec![ - "alice".to_string(), - "bob".to_string(), - ]) - .await - .unwrap(); + let (monero, _containers) = Monero::new(&tc, vec!["alice".to_string(), "bob".to_string()]) + .await + .unwrap(); let alice_wallet = monero.wallet("alice").unwrap(); let bob_wallet = monero.wallet("bob").unwrap(); diff --git a/monero-rpc/src/rpc/wallet.rs b/monero-rpc/src/rpc/wallet.rs index 67298b61..db106cbc 100644 --- a/monero-rpc/src/rpc/wallet.rs +++ b/monero-rpc/src/rpc/wallet.rs @@ -44,7 +44,7 @@ impl Client { debug!("get address RPC response: {}", response); - let r: Response = serde_json::from_str(&response)?; + let r = serde_json::from_str::>(&response)?; Ok(r.result) } @@ -69,9 +69,9 @@ impl Client { index, response ); - let res: Response = serde_json::from_str(&response)?; + let r = serde_json::from_str::>(&response)?; - let balance = res.result.balance; + let balance = r.result.balance; Ok(balance) } @@ -93,7 +93,7 @@ impl Client { debug!("create account RPC response: {}", response); - let r: Response = serde_json::from_str(&response)?; + let r = serde_json::from_str::>(&response)?; Ok(r.result) } @@ -115,7 +115,7 @@ impl Client { debug!("get accounts RPC response: {}", response); - let r: Response = serde_json::from_str(&response)?; + let r = serde_json::from_str::>(&response)?; Ok(r.result) } @@ -147,6 +147,28 @@ impl Client { 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 { @@ -211,7 +233,7 @@ impl Client { debug!("transfer RPC response: {}", response); - let r: Response = serde_json::from_str(&response)?; + let r = serde_json::from_str::>(&response)?; Ok(r.result) } @@ -230,7 +252,7 @@ impl Client { debug!("wallet height RPC response: {}", response); - let r: Response = serde_json::from_str(&response)?; + let r = serde_json::from_str::>(&response)?; Ok(r.result) } @@ -259,7 +281,7 @@ impl Client { debug!("transfer RPC response: {}", response); - let r: Response = serde_json::from_str(&response)?; + let r = serde_json::from_str::>(&response)?; Ok(r.result) } @@ -292,7 +314,7 @@ impl Client { debug!("generate_from_keys RPC response: {}", response); - let r: Response = serde_json::from_str(&response)?; + let r = serde_json::from_str::>(&response)?; Ok(r.result) } @@ -310,7 +332,29 @@ impl Client { debug!("refresh RPC response: {}", response); - let r: Response = serde_json::from_str(&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) } } @@ -453,3 +497,41 @@ 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, +} + +#[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/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 90451fdd..2e50c81a 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -32,7 +32,7 @@ use swap::{ execution_params::GetExecutionParams, fs::default_config_path, monero, - monero::{Amount, CreateWallet, OpenWallet}, + monero::Amount, protocol::alice::EventLoop, seed::Seed, trace::init_tracing, @@ -165,21 +165,7 @@ async fn init_wallets( ); // Setup the Monero wallet - let open_wallet_response = monero_wallet.open_wallet(DEFAULT_WALLET_NAME).await; - if open_wallet_response.is_err() { - monero_wallet - .create_wallet(DEFAULT_WALLET_NAME) - .await - .context(format!( - "Unable to create Monero wallet.\ - Please ensure that the monero-wallet-rpc is available at {}", - config.monero.wallet_rpc_url - ))?; - - info!("Created Monero wallet {}", DEFAULT_WALLET_NAME); - } else { - info!("Opened Monero wallet {}", DEFAULT_WALLET_NAME); - } + monero_wallet.open_or_create().await?; let balance = monero_wallet.get_balance().await?; if balance == Amount::ZERO { diff --git a/swap/src/bin/swap_cli.rs b/swap/src/bin/swap_cli.rs index ad6c74dd..36f1326a 100644 --- a/swap/src/bin/swap_cli.rs +++ b/swap/src/bin/swap_cli.rs @@ -28,7 +28,6 @@ use swap::{ execution_params, execution_params::GetExecutionParams, monero, - monero::{CreateWallet, OpenWallet}, protocol::{ bob, bob::{cancel::CancelError, Builder, EventLoop}, @@ -102,8 +101,9 @@ async fn main() -> Result<()> { .run(monero_network, "stagenet.community.xmr.to") .await?; - match args.cmd.unwrap_or_default() { + match args.cmd { Command::BuyXmr { + receive_monero_address, alice_peer_id, alice_addr, } => { @@ -153,6 +153,7 @@ async fn main() -> Result<()> { Arc::new(monero_wallet), execution_params, event_loop_handle, + receive_monero_address, ) .with_init_params(send_bitcoin) .build()?; @@ -180,6 +181,7 @@ async fn main() -> Result<()> { table.printstd(); } Command::Resume { + receive_monero_address, swap_id, alice_peer_id, alice_addr, @@ -205,6 +207,7 @@ async fn main() -> Result<()> { Arc::new(monero_wallet), execution_params, event_loop_handle, + receive_monero_address, ) .build()?; @@ -292,25 +295,7 @@ async fn init_monero_wallet( MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME.to_string(), ); - // Setup the temporary Monero wallet necessary for monitoring the blockchain - let open_monitoring_wallet_response = monero_wallet - .open_wallet(MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME) - .await; - if open_monitoring_wallet_response.is_err() { - monero_wallet - .create_wallet(MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME) - .await - .context(format!( - "Unable to create Monero wallet for blockchain monitoring.\ - Please ensure that the monero-wallet-rpc is available at {}", - monero_wallet_rpc_url - ))?; - - debug!( - "Created Monero wallet for blockchain monitoring with name {}", - MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME - ); - } + monero_wallet.open_or_create().await?; let _test_wallet_connection = monero_wallet .block_height() diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index 1dc97fb9..0231f033 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -18,13 +18,16 @@ pub struct Arguments { pub debug: bool, #[structopt(subcommand)] - pub cmd: Option, + pub cmd: Command, } #[derive(structopt::StructOpt, Debug)] #[structopt(name = "xmr_btc-swap", about = "XMR BTC atomic swap")] pub enum Command { BuyXmr { + #[structopt(long = "receive-address")] + receive_monero_address: monero::Address, + #[structopt(long = "connect-peer-id", default_value = DEFAULT_ALICE_PEER_ID)] alice_peer_id: PeerId, @@ -36,6 +39,9 @@ pub enum Command { }, History, Resume { + #[structopt(long = "receive-address")] + receive_monero_address: monero::Address, + #[structopt(long = "swap-id")] swap_id: Uuid, @@ -66,22 +72,9 @@ pub enum Command { }, } -impl Default for Command { - fn default() -> Self { - Self::BuyXmr { - alice_peer_id: DEFAULT_ALICE_PEER_ID - .parse() - .expect("default alice peer id str is a valid Multiaddr>"), - alice_addr: DEFAULT_ALICE_MULTIADDR - .parse() - .expect("default alice multiaddr str is a valid PeerId"), - } - } -} - #[cfg(test)] mod tests { - use crate::cli::command::{Command, DEFAULT_ALICE_MULTIADDR, DEFAULT_ALICE_PEER_ID}; + use crate::cli::command::{DEFAULT_ALICE_MULTIADDR, DEFAULT_ALICE_PEER_ID}; use libp2p::{core::Multiaddr, PeerId}; #[test] @@ -97,9 +90,4 @@ mod tests { .parse::() .expect("default alice multiaddr str is a valid Multiaddr>"); } - - #[test] - fn default_command_success() { - Command::default(); - } } diff --git a/swap/src/execution_params.rs b/swap/src/execution_params.rs index ba3ae21e..c9e25c9e 100644 --- a/swap/src/execution_params.rs +++ b/swap/src/execution_params.rs @@ -91,8 +91,7 @@ mod testnet { pub static BITCOIN_AVG_BLOCK_TIME: Lazy = Lazy::new(|| Duration::from_secs(5 * 60)); - // This does not reflect recommended values for mainnet! - pub static MONERO_FINALITY_CONFIRMATIONS: u32 = 5; + pub static MONERO_FINALITY_CONFIRMATIONS: u32 = 10; // This does not reflect recommended values for mainnet! pub static BITCOIN_CANCEL_TIMELOCK: CancelTimelock = CancelTimelock::new(12); @@ -109,7 +108,7 @@ mod regtest { pub static BITCOIN_AVG_BLOCK_TIME: Lazy = Lazy::new(|| Duration::from_secs(5)); - pub static MONERO_FINALITY_CONFIRMATIONS: u32 = 1; + pub static MONERO_FINALITY_CONFIRMATIONS: u32 = 10; pub static BITCOIN_CANCEL_TIMELOCK: CancelTimelock = CancelTimelock::new(100); diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 90224948..613e2095 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -1,7 +1,7 @@ pub mod wallet; mod wallet_rpc; -pub use ::monero::{Network, PrivateKey, PublicKey}; +pub use ::monero::{Address, Network, PrivateKey, PublicKey}; pub use curve25519_dalek::scalar::Scalar; pub use wallet::Wallet; pub use wallet_rpc::{WalletRpc, WalletRpcProcess}; @@ -9,9 +9,6 @@ pub use wallet_rpc::{WalletRpc, WalletRpcProcess}; use crate::bitcoin; use ::bitcoin::hashes::core::fmt::Formatter; use anyhow::Result; -use async_trait::async_trait; -use monero::Address; -use monero_rpc::wallet::{BlockHeight, Refreshed}; use rand::{CryptoRng, RngCore}; use rust_decimal::{ prelude::{FromPrimitive, ToPrimitive}, @@ -183,28 +180,6 @@ impl From for String { } } -#[async_trait] -pub trait Transfer { - async fn transfer( - &self, - public_spend_key: PublicKey, - public_view_key: PublicViewKey, - amount: Amount, - ) -> Result; -} - -#[async_trait] -pub trait WatchForTransfer { - async fn watch_for_transfer( - &self, - public_spend_key: PublicKey, - public_view_key: PublicViewKey, - transfer_proof: TransferProof, - amount: Amount, - expected_confirmations: u32, - ) -> Result<(), InsufficientFunds>; -} - #[derive(Debug, Clone, Copy, thiserror::Error)] #[error("transaction does not pay enough: expected {expected}, got {actual}")] pub struct InsufficientFunds { @@ -218,51 +193,6 @@ pub struct BalanceTooLow { pub balance: Amount, } -#[async_trait] -pub trait CreateWalletForOutput { - async fn create_and_load_wallet_for_output( - &self, - private_spend_key: PrivateKey, - private_view_key: PrivateViewKey, - restore_height: BlockHeight, - ) -> Result<()>; -} - -#[async_trait] -pub trait CreateWalletForOutputThenLoadDefaultWallet { - async fn create_and_load_wallet_for_output_then_load_default_wallet( - &self, - private_spend_key: PrivateKey, - private_view_key: PrivateViewKey, - restore_height: BlockHeight, - ) -> Result<()>; -} - -#[async_trait] -pub trait OpenWallet { - async fn open_wallet(&self, file_name: &str) -> Result<()>; -} - -#[async_trait] -pub trait CreateWallet { - async fn create_wallet(&self, file_name: &str) -> Result<()>; -} - -#[async_trait] -pub trait WalletBlockHeight { - async fn block_height(&self) -> Result; -} - -#[async_trait] -pub trait GetAddress { - async fn get_main_address(&self) -> Result
; -} - -#[async_trait] -pub trait Refresh { - async fn refresh(&self) -> Result; -} - #[derive(thiserror::Error, Debug, Clone, PartialEq)] #[error("Overflow, cannot convert {0} to u64")] pub struct OverflowError(pub String); diff --git a/swap/src/monero/wallet.rs b/swap/src/monero/wallet.rs index b79ced60..04fe14dd 100644 --- a/swap/src/monero/wallet.rs +++ b/swap/src/monero/wallet.rs @@ -1,11 +1,8 @@ use crate::monero::{ - Amount, CreateWallet, CreateWalletForOutput, CreateWalletForOutputThenLoadDefaultWallet, - InsufficientFunds, OpenWallet, PrivateViewKey, PublicViewKey, Transfer, TransferProof, TxHash, - WatchForTransfer, + Amount, InsufficientFunds, PrivateViewKey, PublicViewKey, TransferProof, TxHash, }; use ::monero::{Address, Network, PrivateKey, PublicKey}; -use anyhow::Result; -use async_trait::async_trait; +use anyhow::{Context, Result}; use backoff::{backoff::Constant as ConstantBackoff, future::retry}; use bitcoin::hashes::core::sync::atomic::AtomicU32; use monero_rpc::{ @@ -14,66 +11,118 @@ use monero_rpc::{ }; use std::{str::FromStr, sync::atomic::Ordering, time::Duration}; use tokio::sync::Mutex; -use tracing::info; +use tracing::{debug, info}; use url::Url; #[derive(Debug)] pub struct Wallet { inner: Mutex, network: Network, - default_wallet_name: String, + name: String, } impl Wallet { - pub fn new(url: Url, network: Network, default_wallet_name: String) -> Self { + pub fn new(url: Url, network: Network, name: String) -> Self { Self { inner: Mutex::new(wallet::Client::new(url)), network, - default_wallet_name, + name, } } - pub fn new_with_client( - client: wallet::Client, - network: Network, - default_wallet_name: String, - ) -> Self { + pub fn new_with_client(client: wallet::Client, network: Network, name: String) -> Self { Self { inner: Mutex::new(client), network, - default_wallet_name, + name, } } - /// Get the balance of the primary account. - pub async fn get_balance(&self) -> Result { - let amount = self.inner.lock().await.get_balance(0).await?; - - Ok(Amount::from_piconero(amount)) + pub async fn open(&self) -> Result<()> { + self.inner + .lock() + .await + .open_wallet(self.name.as_str()) + .await?; + Ok(()) } - pub async fn block_height(&self) -> Result { - self.inner.lock().await.block_height().await + pub async fn open_or_create(&self) -> Result<()> { + let open_wallet_response = self.open().await; + if open_wallet_response.is_err() { + self.inner.lock().await.create_wallet(self.name.as_str()).await.context( + "Unable to create Monero wallet, please ensure that the monero-wallet-rpc is available", + )?; + + debug!("Created Monero wallet {}", self.name); + } else { + debug!("Opened Monero wallet {}", self.name); + } + + Ok(()) } - pub async fn get_main_address(&self) -> Result
{ - let address = self.inner.lock().await.get_address(0).await?; - Ok(Address::from_str(address.address.as_str())?) + pub async fn create_from_and_load( + &self, + private_spend_key: PrivateKey, + private_view_key: PrivateViewKey, + restore_height: BlockHeight, + ) -> Result<()> { + let public_spend_key = PublicKey::from_private_key(&private_spend_key); + let public_view_key = PublicKey::from_private_key(&private_view_key.into()); + + let address = Address::standard(self.network, public_spend_key, public_view_key); + + let wallet = self.inner.lock().await; + + // Properly close the wallet before generating the other wallet to ensure that + // it saves its state correctly + let _ = wallet.close_wallet().await?; + + let _ = wallet + .generate_from_keys( + &address.to_string(), + &private_spend_key.to_string(), + &PrivateKey::from(private_view_key).to_string(), + restore_height.height, + ) + .await?; + + Ok(()) } - pub async fn refresh(&self) -> Result { - self.inner.lock().await.refresh().await + pub async fn create_from( + &self, + private_spend_key: PrivateKey, + private_view_key: PrivateViewKey, + restore_height: BlockHeight, + ) -> Result<()> { + let public_spend_key = PublicKey::from_private_key(&private_spend_key); + let public_view_key = PublicKey::from_private_key(&private_view_key.into()); + + let address = Address::standard(self.network, public_spend_key, public_view_key); + + let wallet = self.inner.lock().await; + + // Properly close the wallet before generating the other wallet to ensure that + // it saves its state correctly + let _ = wallet.close_wallet().await?; + + let _ = wallet + .generate_from_keys( + &address.to_string(), + &private_spend_key.to_string(), + &PrivateKey::from(private_view_key).to_string(), + restore_height.height, + ) + .await?; + + let _ = wallet.open_wallet(self.name.as_str()).await?; + + Ok(()) } - pub fn static_tx_fee_estimate(&self) -> Amount { - // Median tx fees on Monero as found here: https://www.monero.how/monero-transaction-fees, 0.000_015 * 2 (to be on the safe side) - Amount::from_monero(0.000_03f64).expect("static fee to be convertible without problems") - } -} - -#[async_trait] -impl Transfer for Wallet { - async fn transfer( + pub async fn transfer( &self, public_spend_key: PublicKey, public_view_key: PublicViewKey, @@ -101,90 +150,8 @@ impl Transfer for Wallet { PrivateKey::from_str(&res.tx_key)?, )) } -} -#[async_trait] -impl CreateWalletForOutput for Wallet { - async fn create_and_load_wallet_for_output( - &self, - private_spend_key: PrivateKey, - private_view_key: PrivateViewKey, - restore_height: BlockHeight, - ) -> Result<()> { - let public_spend_key = PublicKey::from_private_key(&private_spend_key); - let public_view_key = PublicKey::from_private_key(&private_view_key.into()); - - let address = Address::standard(self.network, public_spend_key, public_view_key); - - let _ = self - .inner - .lock() - .await - .generate_from_keys( - &address.to_string(), - &private_spend_key.to_string(), - &PrivateKey::from(private_view_key).to_string(), - restore_height.height, - ) - .await?; - - Ok(()) - } -} - -#[async_trait] -impl CreateWalletForOutputThenLoadDefaultWallet for Wallet { - async fn create_and_load_wallet_for_output_then_load_default_wallet( - &self, - private_spend_key: PrivateKey, - private_view_key: PrivateViewKey, - restore_height: BlockHeight, - ) -> Result<()> { - let public_spend_key = PublicKey::from_private_key(&private_spend_key); - let public_view_key = PublicKey::from_private_key(&private_view_key.into()); - - let address = Address::standard(self.network, public_spend_key, public_view_key); - - let wallet = self.inner.lock().await; - - let _ = wallet - .generate_from_keys( - &address.to_string(), - &private_spend_key.to_string(), - &PrivateKey::from(private_view_key).to_string(), - restore_height.height, - ) - .await?; - - let _ = wallet - .open_wallet(self.default_wallet_name.as_str()) - .await?; - - Ok(()) - } -} - -#[async_trait] -impl OpenWallet for Wallet { - async fn open_wallet(&self, file_name: &str) -> Result<()> { - self.inner.lock().await.open_wallet(file_name).await?; - Ok(()) - } -} - -#[async_trait] -impl CreateWallet for Wallet { - async fn create_wallet(&self, file_name: &str) -> Result<()> { - self.inner.lock().await.create_wallet(file_name).await?; - Ok(()) - } -} - -// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed -// to `ConstantBackoff`. -#[async_trait] -impl WatchForTransfer for Wallet { - async fn watch_for_transfer( + pub async fn watch_for_transfer( &self, public_spend_key: PublicKey, public_view_key: PublicViewKey, @@ -247,4 +214,41 @@ impl WatchForTransfer for Wallet { Ok(()) } + + pub async fn sweep_all(&self, address: Address) -> Result> { + let sweep_all = self + .inner + .lock() + .await + .sweep_all(address.to_string().as_str()) + .await?; + + let tx_hashes = sweep_all.tx_hash_list.into_iter().map(TxHash).collect(); + Ok(tx_hashes) + } + + /// Get the balance of the primary account. + pub async fn get_balance(&self) -> Result { + let amount = self.inner.lock().await.get_balance(0).await?; + + Ok(Amount::from_piconero(amount)) + } + + pub async fn block_height(&self) -> Result { + self.inner.lock().await.block_height().await + } + + pub async fn get_main_address(&self) -> Result
{ + let address = self.inner.lock().await.get_address(0).await?; + Ok(Address::from_str(address.address.as_str())?) + } + + pub async fn refresh(&self) -> Result { + self.inner.lock().await.refresh().await + } + + pub fn static_tx_fee_estimate(&self) -> Amount { + // Median tx fees on Monero as found here: https://www.monero.how/monero-transaction-fees, 0.000_015 * 2 (to be on the safe side) + Amount::from_monero(0.000_03f64).expect("static fee to be convertible without problems") + } } diff --git a/swap/src/protocol/alice/steps.rs b/swap/src/protocol/alice/steps.rs index e65567d1..aab80944 100644 --- a/swap/src/protocol/alice/steps.rs +++ b/swap/src/protocol/alice/steps.rs @@ -6,7 +6,6 @@ use crate::{ }, execution_params::ExecutionParams, monero, - monero::Transfer, protocol::{ alice, alice::{event_loop::EventLoopHandle, TransferProof}, @@ -23,7 +22,6 @@ use futures::{ }; use libp2p::PeerId; use sha2::Sha256; -use std::sync::Arc; use tokio::time::timeout; // TODO(Franck): Use helper functions from xmr-btc instead of re-writing them @@ -49,15 +47,12 @@ pub async fn wait_for_locked_bitcoin( Ok(()) } -pub async fn lock_xmr( +pub async fn lock_xmr( bob_peer_id: PeerId, state3: alice::State3, event_loop_handle: &mut EventLoopHandle, - monero_wallet: Arc, -) -> Result<()> -where - W: Transfer, -{ + monero_wallet: &monero::Wallet, +) -> Result<()> { let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: state3.s_a }); let public_spend_key = S_a + state3.S_b_monero; @@ -130,14 +125,13 @@ pub async fn publish_cancel_transaction( B: bitcoin::PublicKey, cancel_timelock: CancelTimelock, tx_cancel_sig_bob: bitcoin::Signature, - bitcoin_wallet: Arc, + bitcoin_wallet: &bitcoin::Wallet, ) -> Result { // First wait for cancel timelock to expire let tx_lock_height = bitcoin_wallet .transaction_block_height(tx_lock.txid()) .await?; - poll_until_block_height_is_gte(bitcoin_wallet.as_ref(), tx_lock_height + cancel_timelock) - .await?; + poll_until_block_height_is_gte(&bitcoin_wallet, tx_lock_height + cancel_timelock).await?; let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B); diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index addde0a4..c9bd7341 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -7,7 +7,6 @@ use crate::{ database::Database, execution_params::ExecutionParams, monero, - monero::CreateWalletForOutputThenLoadDefaultWallet, monero_ext::ScalarExt, protocol::{ alice, @@ -131,7 +130,7 @@ async fn run_until_internal( bob_peer_id, *state3.clone(), &mut event_loop_handle, - monero_wallet.clone(), + &monero_wallet, ) .await?; @@ -287,7 +286,7 @@ async fn run_until_internal( state3.B, state3.cancel_timelock, state3.tx_cancel_sig_bob.clone(), - bitcoin_wallet.clone(), + &bitcoin_wallet, ) .await?; @@ -392,11 +391,7 @@ async fn run_until_internal( let view_key = state3.v; monero_wallet - .create_and_load_wallet_for_output_then_load_default_wallet( - spend_key, - view_key, - monero_wallet_restore_blockheight, - ) + .create_from(spend_key, view_key, monero_wallet_restore_blockheight) .await?; let state = AliceState::XmrRefunded; diff --git a/swap/src/protocol/bob.rs b/swap/src/protocol/bob.rs index 70ecc7d6..d69fbb83 100644 --- a/swap/src/protocol/bob.rs +++ b/swap/src/protocol/bob.rs @@ -44,6 +44,7 @@ pub struct Swap { pub monero_wallet: Arc, pub execution_params: ExecutionParams, pub swap_id: Uuid, + pub receive_monero_address: ::monero::Address, } pub struct Builder { @@ -57,6 +58,8 @@ pub struct Builder { execution_params: ExecutionParams, event_loop_handle: bob::EventLoopHandle, + + receive_monero_address: ::monero::Address, } enum InitParams { @@ -73,6 +76,7 @@ impl Builder { monero_wallet: Arc, execution_params: ExecutionParams, event_loop_handle: bob::EventLoopHandle, + receive_monero_address: ::monero::Address, ) -> Self { Self { swap_id, @@ -82,6 +86,7 @@ impl Builder { init_params: InitParams::None, execution_params, event_loop_handle, + receive_monero_address, } } @@ -106,6 +111,7 @@ impl Builder { monero_wallet: self.monero_wallet.clone(), swap_id: self.swap_id, execution_params: self.execution_params, + receive_monero_address: self.receive_monero_address, }) } } diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index f7e562ac..3f407f0f 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -316,15 +316,12 @@ pub struct State3 { } impl State3 { - pub async fn watch_for_lock_xmr( + pub async fn watch_for_lock_xmr( self, - xmr_wallet: &W, + xmr_wallet: &monero::Wallet, transfer_proof: TransferProof, monero_wallet_restore_blockheight: BlockHeight, - ) -> Result> - where - W: monero::WatchForTransfer, - { + ) -> Result> { let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(self.s_b)); let S = self.S_a_monero + S_b_monero; @@ -572,10 +569,7 @@ pub struct State5 { } impl State5 { - pub async fn claim_xmr(&self, monero_wallet: &W) -> Result<()> - where - W: monero::CreateWalletForOutput, - { + pub async fn claim_xmr(&self, monero_wallet: &monero::Wallet) -> Result<()> { let s_b = monero::PrivateKey { scalar: self.s_b }; let s = self.s_a + s_b; @@ -583,7 +577,7 @@ impl State5 { // NOTE: This actually generates and opens a new wallet, closing the currently // open one. monero_wallet - .create_and_load_wallet_for_output(s, self.v, self.monero_wallet_restore_blockheight) + .create_from_and_load(s, self.v, self.monero_wallet_restore_blockheight) .await?; Ok(()) diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index cb412c2a..35b495de 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -43,6 +43,7 @@ pub async fn run_until( swap.monero_wallet, swap.swap_id, swap.execution_params, + swap.receive_monero_address, ) .await } @@ -59,6 +60,7 @@ async fn run_until_internal( monero_wallet: Arc, swap_id: Uuid, execution_params: ExecutionParams, + receive_monero_address: monero::Address, ) -> Result { trace!("Current state: {}", state); if is_target_state(&state) { @@ -90,6 +92,7 @@ async fn run_until_internal( monero_wallet, swap_id, execution_params, + receive_monero_address, ) .await } @@ -111,6 +114,7 @@ async fn run_until_internal( monero_wallet, swap_id, execution_params, + receive_monero_address, ) .await } @@ -159,6 +163,7 @@ async fn run_until_internal( monero_wallet, swap_id, execution_params, + receive_monero_address, ) .await } @@ -213,6 +218,7 @@ async fn run_until_internal( monero_wallet, swap_id, execution_params, + receive_monero_address, ) .await } @@ -255,6 +261,7 @@ async fn run_until_internal( monero_wallet, swap_id, execution_params, + receive_monero_address, ) .await } @@ -290,6 +297,7 @@ async fn run_until_internal( monero_wallet, swap_id, execution_params, + receive_monero_address, ) .await } @@ -297,6 +305,11 @@ async fn run_until_internal( // Bob redeems XMR using revealed s_a state.claim_xmr(monero_wallet.as_ref()).await?; + // Ensure that the generated wallet is synced so we have a proper balance + monero_wallet.refresh().await?; + // Sweep (transfer all funds) to the given address + monero_wallet.sweep_all(receive_monero_address).await?; + let state = BobState::XmrRedeemed { tx_lock_id: state.tx_lock_id(), }; @@ -311,6 +324,7 @@ async fn run_until_internal( monero_wallet, swap_id, execution_params, + receive_monero_address, ) .await } @@ -336,6 +350,7 @@ async fn run_until_internal( monero_wallet, swap_id, execution_params, + receive_monero_address, ) .await } @@ -367,6 +382,7 @@ async fn run_until_internal( monero_wallet, swap_id, execution_params, + receive_monero_address, ) .await } diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index 058cf5d4..e80094be 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -33,7 +33,9 @@ use tracing_log::LogTracer; use url::Url; use uuid::Uuid; -const TEST_WALLET_NAME: &str = "testwallet"; +const MONERO_WALLET_NAME_BOB: &str = "bob"; +const MONERO_WALLET_NAME_ALICE: &str = "alice"; +const BITCOIN_TEST_WALLET_NAME: &str = "testwallet"; #[derive(Debug, Clone)] pub struct StartingBalances { @@ -54,15 +56,18 @@ struct BobParams { } impl BobParams { - pub fn builder(&self, event_loop_handle: bob::EventLoopHandle) -> bob::Builder { - bob::Builder::new( + pub async fn builder(&self, event_loop_handle: bob::EventLoopHandle) -> Result { + let receive_address = self.monero_wallet.get_main_address().await?; + + Ok(bob::Builder::new( Database::open(&self.db_path.clone().as_path()).unwrap(), self.swap_id, self.bitcoin_wallet.clone(), self.monero_wallet.clone(), self.execution_params, event_loop_handle, - ) + receive_address, + )) } pub fn new_eventloop(&self) -> Result<(bob::EventLoop, bob::EventLoopHandle)> { @@ -107,6 +112,8 @@ impl TestContext { let swap = self .bob_params .builder(event_loop_handle) + .await + .unwrap() .with_init_params(self.btc_amount) .build() .unwrap(); @@ -124,7 +131,13 @@ impl TestContext { let (event_loop, event_loop_handle) = self.bob_params.new_eventloop().unwrap(); - let swap = self.bob_params.builder(event_loop_handle).build().unwrap(); + let swap = self + .bob_params + .builder(event_loop_handle) + .await + .unwrap() + .build() + .unwrap(); let join_handle = tokio::spawn(event_loop.run()); @@ -237,13 +250,15 @@ impl TestContext { self.bob_starting_balances.btc - self.btc_amount - lock_tx_bitcoin_fee ); + // unload the generated wallet by opening the original wallet + self.bob_monero_wallet.open().await.unwrap(); + // refresh the original wallet to make sure the balance is caught up + self.bob_monero_wallet.refresh().await.unwrap(); + // Ensure that Bob's balance is refreshed as we use a newly created wallet self.bob_monero_wallet.as_ref().refresh().await.unwrap(); let xmr_balance_after_swap = self.bob_monero_wallet.as_ref().get_balance().await.unwrap(); - assert_eq!( - xmr_balance_after_swap, - self.bob_starting_balances.xmr + self.xmr_amount - ); + assert!(xmr_balance_after_swap > self.bob_starting_balances.xmr); } pub async fn assert_bob_refunded(&self, state: BobState) { @@ -353,7 +368,7 @@ where let bob_seed = Seed::random().unwrap(); let (alice_bitcoin_wallet, alice_monero_wallet) = init_test_wallets( - "alice", + MONERO_WALLET_NAME_ALICE, containers.bitcoind_url.clone(), &monero, alice_starting_balances.clone(), @@ -375,7 +390,7 @@ where }; let (bob_bitcoin_wallet, bob_monero_wallet) = init_test_wallets( - "bob", + MONERO_WALLET_NAME_BOB, containers.bitcoind_url, &monero, bob_starting_balances.clone(), @@ -529,11 +544,11 @@ async fn init_bitcoind(node_url: Url, spendable_quantity: u32) -> Result let bitcoind_client = Client::new(node_url.clone()); bitcoind_client - .createwallet(TEST_WALLET_NAME, None, None, None, None) + .createwallet(BITCOIN_TEST_WALLET_NAME, None, None, None, None) .await?; let reward_address = bitcoind_client - .with_wallet(TEST_WALLET_NAME)? + .with_wallet(BITCOIN_TEST_WALLET_NAME)? .getnewaddress(None, None) .await?; @@ -550,12 +565,12 @@ pub async fn mint(node_url: Url, address: bitcoin::Address, amount: bitcoin::Amo let bitcoind_client = Client::new(node_url.clone()); bitcoind_client - .send_to_address(TEST_WALLET_NAME, address.clone(), amount) + .send_to_address(BITCOIN_TEST_WALLET_NAME, address.clone(), amount) .await?; // Confirm the transaction let reward_address = bitcoind_client - .with_wallet(TEST_WALLET_NAME)? + .with_wallet(BITCOIN_TEST_WALLET_NAME)? .getnewaddress(None, None) .await?; bitcoind_client @@ -571,9 +586,12 @@ async fn init_monero_container( Monero, Vec>, ) { - let (monero, monerods) = Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) - .await - .unwrap(); + let (monero, monerods) = Monero::new(&cli, vec![ + MONERO_WALLET_NAME_ALICE.to_string(), + MONERO_WALLET_NAME_BOB.to_string(), + ]) + .await + .unwrap(); (monero, monerods) } @@ -597,7 +615,7 @@ async fn init_test_wallets( let xmr_wallet = swap::monero::Wallet::new_with_client( monero.wallet(name).unwrap().client(), monero::Network::default(), - "irrelevant_for_tests".to_string(), + name.to_string(), ); let electrum_rpc_url = { @@ -680,18 +698,20 @@ pub fn init_tracing() -> DefaultGuard { let global_filter = tracing::Level::WARN; let swap_filter = tracing::Level::DEBUG; let xmr_btc_filter = tracing::Level::DEBUG; - let monero_harness_filter = tracing::Level::INFO; + let monero_rpc_filter = tracing::Level::DEBUG; + let monero_harness_filter = tracing::Level::DEBUG; let bitcoin_harness_filter = tracing::Level::INFO; let testcontainers_filter = tracing::Level::DEBUG; use tracing_subscriber::util::SubscriberInitExt as _; tracing_subscriber::fmt() .with_env_filter(format!( - "{},swap={},xmr_btc={},monero_harness={},bitcoin_harness={},testcontainers={}", + "{},swap={},xmr_btc={},monero_harness={},monero_rpc={},bitcoin_harness={},testcontainers={}", global_filter, swap_filter, xmr_btc_filter, monero_harness_filter, + monero_rpc_filter, bitcoin_harness_filter, testcontainers_filter ))