490: Mainnet switch r=da-kami a=da-kami

Fixes  #446
Fixes #360 
Fixes #506 
Fixes #478 

To be precise: It is actually a testnet switch, because I think mainnet should be default.

I took several assumptions on the way (e.g. network support, ...).

At this stage any feedback welcome :)

TODO:

- [ ] successful mainnet swap with this code base before merging :)

Co-authored-by: Daniel Karzel <daniel@comit.network>
pull/518/head
bors[bot] 3 years ago committed by GitHub
commit bdb88f89cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -43,11 +43,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1. Balance of ASB too low
2. Buy amount sent by CLI exceeds maximum buy amount accepted by ASB
3. ASB is running in resume-only mode and does not accept incoming swap requests
- An issue where the monero daemon port used by the `monero-wallet-rpc` could not be specified.
The CLI parameter `--monero-daemon-host` was changed to `--monero-daemon-address` where host and port have to be specified.
### Changed
- The ASB's `--max-buy` and `ask-spread` parameter were removed in favour of entries in the config file.
The initial setup includes setting these two values now.
- From this version on the CLI and ASB run on **mainnet** by default!
When running either application with `--testnet` Monero network defaults to `stagenet` and Bitcoin network to `testnet3`.
This is a breaking change.
It is recommended to run the applications with `--testnet` first and not just run the application on `mainnet` without experience.
## [0.5.0] - 2021-04-17

@ -10,6 +10,9 @@ use uuid::Uuid;
author
)]
pub struct Arguments {
#[structopt(long, help = "Swap on testnet")]
pub testnet: bool,
#[structopt(
short,
long = "json",

@ -1,3 +1,4 @@
use crate::env::{Mainnet, Testnet};
use crate::fs::{ensure_directory_exists, system_config_dir, system_data_dir};
use crate::tor::{DEFAULT_CONTROL_PORT, DEFAULT_SOCKS5_PORT};
use anyhow::{bail, Context, Result};
@ -11,14 +12,71 @@ use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use tracing::info;
use url::Url;
const DEFAULT_LISTEN_ADDRESS_TCP: &str = "/ip4/0.0.0.0/tcp/9939";
const DEFAULT_LISTEN_ADDRESS_WS: &str = "/ip4/0.0.0.0/tcp/9940/ws";
const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002";
const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc";
const DEFAULT_BITCOIN_CONFIRMATION_TARGET: usize = 3;
pub trait GetDefaults {
fn getConfigFileDefaults() -> Result<Defaults>;
}
pub struct Defaults {
pub config_path: PathBuf,
data_dir: PathBuf,
listen_address_tcp: Multiaddr,
listen_address_ws: Multiaddr,
electrum_rpc_url: Url,
monero_wallet_rpc_url: Url,
bitcoin_confirmation_target: usize,
}
impl GetDefaults for Testnet {
fn getConfigFileDefaults() -> Result<Defaults> {
let defaults = Defaults {
config_path: default_asb_config_dir()?
.join("testnet")
.join("config.toml"),
data_dir: default_asb_data_dir()?.join("testnet"),
listen_address_tcp: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9939")?,
listen_address_ws: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9940/ws")?,
electrum_rpc_url: Url::parse("ssl://electrum.blockstream.info:60002")?,
monero_wallet_rpc_url: Url::parse("http://127.0.0.1:38083/json_rpc")?,
bitcoin_confirmation_target: 1,
};
Ok(defaults)
}
}
impl GetDefaults for Mainnet {
fn getConfigFileDefaults() -> Result<Defaults> {
let defaults = Defaults {
config_path: default_asb_config_dir()?
.join("mainnet")
.join("config.toml"),
data_dir: default_asb_data_dir()?.join("mainnet"),
listen_address_tcp: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9939")?,
listen_address_ws: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9940/ws")?,
electrum_rpc_url: Url::parse("ssl://electrum.blockstream.info:50002")?,
monero_wallet_rpc_url: Url::parse("http://127.0.0.1:18083/json_rpc")?,
bitcoin_confirmation_target: 3,
};
Ok(defaults)
}
}
fn default_asb_config_dir() -> Result<PathBuf> {
system_config_dir()
.map(|dir| Path::join(&dir, "asb"))
.context("Could not generate default config file path")
}
fn default_asb_data_dir() -> Result<PathBuf> {
system_data_dir()
.map(|dir| Path::join(&dir, "asb"))
.context("Could not generate default config file path")
}
const DEFAULT_MIN_BUY_AMOUNT: f64 = 0.002f64;
const DEFAULT_MAX_BUY_AMOUNT: f64 = 0.02f64;
@ -64,12 +122,18 @@ pub struct Network {
pub struct Bitcoin {
pub electrum_rpc_url: Url,
pub target_block: usize,
pub finality_confirmations: Option<u32>,
#[serde(with = "crate::bitcoin::network")]
pub network: bitcoin::Network,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Monero {
pub wallet_rpc_url: Url,
pub finality_confirmations: Option<u64>,
#[serde(with = "crate::monero::network")]
pub network: monero::Network,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
@ -118,31 +182,8 @@ pub fn read_config(config_path: PathBuf) -> Result<Result<Config, ConfigNotIniti
Ok(Ok(file))
}
/// Default location for storing the config file for the ASB
// Takes the default system config-dir and adds a `/asb/config.toml`
pub fn default_config_path() -> Result<PathBuf> {
system_config_dir()
.map(|dir| Path::join(&dir, "asb"))
.map(|dir| Path::join(&dir, "config.toml"))
.context("Could not generate default config file path")
}
/// Default location for storing data for the CLI
// Takes the default system data-dir and adds a `/asb`
fn default_data_dir() -> Result<PathBuf> {
system_data_dir()
.map(|proj_dir| Path::join(&proj_dir, "asb"))
.context("Could not generate default data dir")
}
pub fn initial_setup<F>(config_path: PathBuf, config_file: F) -> Result<()>
where
F: Fn() -> Result<Config>,
{
info!("Config file not found, running initial setup...");
let initial_config = config_file()?;
let toml = toml::to_string(&initial_config)?;
pub fn initial_setup(config_path: PathBuf, config: Config) -> Result<()> {
let toml = toml::to_string(&config)?;
ensure_directory_exists(config_path.as_path())?;
fs::write(&config_path, toml)?;
@ -154,13 +195,30 @@ where
Ok(())
}
pub fn query_user_for_initial_testnet_config() -> Result<Config> {
pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
let (bitcoin_network, monero_network, defaults) = if testnet {
tracing::info!("Running initial setup for testnet");
let bitcoin_network = bitcoin::Network::Testnet;
let monero_network = monero::Network::Stagenet;
let defaults = Testnet::getConfigFileDefaults()?;
(bitcoin_network, monero_network, defaults)
} else {
tracing::info!("Running initial setup for mainnet");
let bitcoin_network = bitcoin::Network::Bitcoin;
let monero_network = monero::Network::Mainnet;
let defaults = Mainnet::getConfigFileDefaults()?;
(bitcoin_network, monero_network, defaults)
};
println!();
let data_dir = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter data directory for asb or hit return to use default")
.default(
default_data_dir()
.context("No default data dir value for this system")?
defaults
.data_dir
.to_str()
.context("Unsupported characters in default path")?
.to_string(),
@ -170,28 +228,27 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
let target_block = Input::with_theme(&ColorfulTheme::default())
.with_prompt("How fast should your Bitcoin transactions be confirmed? Your transaction fee will be calculated based on this target. Hit return to use default")
.default(DEFAULT_BITCOIN_CONFIRMATION_TARGET)
.default(defaults.bitcoin_confirmation_target)
.interact_text()?;
let listen_addresses = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter multiaddresses (comma separated) on which asb should list for peer-to-peer communications or hit return to use default")
.default( format!("{},{}", DEFAULT_LISTEN_ADDRESS_TCP, DEFAULT_LISTEN_ADDRESS_WS))
.default( format!("{},{}", defaults.listen_address_tcp, defaults.listen_address_ws))
.interact_text()?;
let listen_addresses = listen_addresses
.split(',')
.map(|str| str.parse())
.collect::<Result<Vec<Multiaddr>, _>>()?;
let electrum_rpc_url: String = Input::with_theme(&ColorfulTheme::default())
let electrum_rpc_url: Url = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter Electrum RPC URL or hit return to use default")
.default(DEFAULT_ELECTRUM_RPC_URL.to_owned())
.default(defaults.electrum_rpc_url)
.interact_text()?;
let electrum_rpc_url = Url::parse(electrum_rpc_url.as_str())?;
let monero_wallet_rpc_url = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter Monero Wallet RPC URL or hit enter to use default")
.default(DEFAULT_MONERO_WALLET_RPC_TESTNET_URL.to_owned())
.default(defaults.monero_wallet_rpc_url)
.interact_text()?;
let monero_wallet_rpc_url = monero_wallet_rpc_url.as_str().parse()?;
let tor_control_port = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter Tor control port or hit enter to use default. If Tor is not running on your machine, no hidden service will be created.")
@ -234,9 +291,13 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
bitcoin: Bitcoin {
electrum_rpc_url,
target_block,
finality_confirmations: None,
network: bitcoin_network,
},
monero: Monero {
wallet_rpc_url: monero_wallet_rpc_url,
finality_confirmations: None,
network: monero_network,
},
tor: TorConf {
control_port: tor_control_port,
@ -253,31 +314,73 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
use tempfile::tempdir;
#[test]
fn config_roundtrip() {
fn config_roundtrip_testnet() {
let temp_dir = tempdir().unwrap().path().to_path_buf();
let config_path = Path::join(&temp_dir, "config.toml");
let defaults = Testnet::getConfigFileDefaults().unwrap();
let expected = Config {
data: Data {
dir: Default::default(),
},
bitcoin: Bitcoin {
electrum_rpc_url: defaults.electrum_rpc_url,
target_block: defaults.bitcoin_confirmation_target,
finality_confirmations: None,
network: bitcoin::Network::Testnet,
},
network: Network {
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws],
},
monero: Monero {
wallet_rpc_url: defaults.monero_wallet_rpc_url,
finality_confirmations: None,
network: monero::Network::Stagenet,
},
tor: Default::default(),
maker: Maker {
min_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MIN_BUY_AMOUNT).unwrap(),
max_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MAX_BUY_AMOUNT).unwrap(),
ask_spread: Decimal::from_f64(DEFAULT_SPREAD).unwrap(),
},
};
initial_setup(config_path.clone(), expected.clone()).unwrap();
let actual = read_config(config_path).unwrap().unwrap();
assert_eq!(expected, actual);
}
#[test]
fn config_roundtrip_mainnet() {
let temp_dir = tempdir().unwrap().path().to_path_buf();
let config_path = Path::join(&temp_dir, "config.toml");
let defaults = Mainnet::getConfigFileDefaults().unwrap();
let expected = Config {
data: Data {
dir: Default::default(),
},
bitcoin: Bitcoin {
electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(),
target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET,
electrum_rpc_url: defaults.electrum_rpc_url,
target_block: defaults.bitcoin_confirmation_target,
finality_confirmations: None,
network: bitcoin::Network::Bitcoin,
},
network: Network {
listen: vec![
DEFAULT_LISTEN_ADDRESS_TCP.parse().unwrap(),
DEFAULT_LISTEN_ADDRESS_WS.parse().unwrap(),
],
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws],
},
monero: Monero {
wallet_rpc_url: Url::from_str(DEFAULT_MONERO_WALLET_RPC_TESTNET_URL).unwrap(),
wallet_rpc_url: defaults.monero_wallet_rpc_url,
finality_confirmations: None,
network: monero::Network::Mainnet,
},
tor: Default::default(),
maker: Maker {
@ -287,7 +390,7 @@ mod tests {
},
};
initial_setup(config_path.clone(), || Ok(expected.clone())).unwrap();
initial_setup(config_path.clone(), expected.clone()).unwrap();
let actual = read_config(config_path).unwrap().unwrap();
assert_eq!(expected, actual);

@ -12,7 +12,7 @@
#![forbid(unsafe_code)]
#![allow(non_snake_case)]
use anyhow::{Context, Result};
use anyhow::{bail, Context, Result};
use libp2p::core::multiaddr::Protocol;
use libp2p::core::Multiaddr;
use libp2p::Swarm;
@ -22,11 +22,10 @@ use std::sync::Arc;
use structopt::StructOpt;
use swap::asb::command::{Arguments, Command, ManualRecovery, RecoverCommandParams};
use swap::asb::config::{
default_config_path, initial_setup, query_user_for_initial_testnet_config, read_config, Config,
ConfigNotInitialized,
initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized,
GetDefaults,
};
use swap::database::Database;
use swap::env::GetConfig;
use swap::monero::Amount;
use swap::network::swarm;
use swap::protocol::alice;
@ -45,23 +44,45 @@ const DEFAULT_WALLET_NAME: &str = "asb-wallet";
#[tokio::main]
async fn main() -> Result<()> {
let opt = Arguments::from_args();
asb::tracing::init(LevelFilter::DEBUG, opt.json).expect("initialize tracing");
let config_path = if let Some(config_path) = opt.config {
let Arguments {
testnet,
json,
config,
cmd,
} = Arguments::from_args();
asb::tracing::init(LevelFilter::DEBUG, json).expect("initialize tracing");
let config_path = if let Some(config_path) = config {
config_path
} else if testnet {
env::Testnet::getConfigFileDefaults()?.config_path
} else {
default_config_path()?
env::Mainnet::getConfigFileDefaults()?.config_path
};
let config = match read_config(config_path.clone())? {
Ok(config) => config,
Err(ConfigNotInitialized {}) => {
initial_setup(config_path.clone(), query_user_for_initial_testnet_config)?;
initial_setup(config_path.clone(), query_user_for_initial_config(testnet)?)?;
read_config(config_path)?.expect("after initial setup config can be read")
}
};
let env_config = env::new(testnet, &config);
if config.monero.network != env_config.monero_network {
bail!(format!(
"Expected monero network in config file to be {:?} but was {:?}",
env_config.monero_network, config.monero.network
));
}
if config.bitcoin.network != env_config.bitcoin_network {
bail!(format!(
"Expected bitcoin network in config file to be {:?} but was {:?}",
env_config.bitcoin_network, config.bitcoin.network
));
}
info!(
db_folder = %config.data.dir.display(),
"Database and Seed will be stored in",
@ -75,9 +96,7 @@ async fn main() -> Result<()> {
let seed =
Seed::from_file_or_generate(&config.data.dir).expect("Could not retrieve/initialize seed");
let env_config = env::Testnet::get_config();
match opt.cmd {
match cmd {
Command::Start { resume_only } => {
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
@ -127,6 +146,7 @@ async fn main() -> Result<()> {
config.maker.max_buy_btc,
kraken_rate.clone(),
resume_only,
env_config,
)?;
for listen in config.network.listen {

@ -15,21 +15,21 @@
use anyhow::{bail, Context, Result};
use prettytable::{row, Table};
use std::cmp::min;
use std::env;
use std::future::Future;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use structopt::StructOpt;
use swap::bitcoin::TxLock;
use swap::cli::command::{Arguments, Command, MoneroParams};
use swap::cli::command::{parse_args_and_apply_defaults, Arguments, Command};
use swap::database::Database;
use swap::env::{Config, GetConfig};
use swap::env::Config;
use swap::network::quote::BidQuote;
use swap::network::swarm;
use swap::protocol::bob;
use swap::protocol::bob::{EventLoop, Swap};
use swap::seed::Seed;
use swap::{bitcoin, cli, env, monero};
use swap::{bitcoin, cli, monero};
use tracing::{debug, error, info, warn};
use url::Url;
use uuid::Uuid;
@ -39,41 +39,33 @@ extern crate prettytable;
#[tokio::main]
async fn main() -> Result<()> {
let Arguments { data, debug, cmd } = Arguments::from_args();
let Arguments {
env_config,
data_dir,
debug,
cmd,
} = parse_args_and_apply_defaults(env::args_os())?;
match cmd {
Command::BuyXmr {
alice_peer_id,
alice_multiaddr,
monero_params:
MoneroParams {
receive_monero_address,
monero_daemon_host,
},
electrum_rpc_url,
tor_socks5_port,
seller_peer_id,
seller_addr,
bitcoin_electrum_rpc_url,
bitcoin_target_block,
monero_receive_address,
monero_daemon_address,
tor_socks5_port,
} => {
let swap_id = Uuid::new_v4();
let data_dir = data.0;
cli::tracing::init(debug, data_dir.join("logs"), swap_id)?;
let db = Database::open(data_dir.join("database").as_path())
.context("Failed to open database")?;
let seed = Seed::from_file_or_generate(data_dir.as_path())
.context("Failed to read in seed file")?;
let env_config = env::Testnet::get_config();
if receive_monero_address.network != env_config.monero_network {
bail!(
"Given monero address is on network {:?}, expected address on network {:?}",
receive_monero_address.network,
env_config.monero_network
)
}
let bitcoin_wallet = init_bitcoin_wallet(
electrum_rpc_url,
bitcoin_electrum_rpc_url,
&seed,
data_dir.clone(),
env_config,
@ -81,17 +73,22 @@ async fn main() -> Result<()> {
)
.await?;
let (monero_wallet, _process) =
init_monero_wallet(data_dir, monero_daemon_host, env_config).await?;
init_monero_wallet(data_dir, monero_daemon_address, env_config).await?;
let bitcoin_wallet = Arc::new(bitcoin_wallet);
let mut swarm = swarm::bob(&seed, alice_peer_id, tor_socks5_port).await?;
let mut swarm = swarm::bob(&seed, seller_peer_id, tor_socks5_port).await?;
swarm
.behaviour_mut()
.add_address(alice_peer_id, alice_multiaddr);
.add_address(seller_peer_id, seller_addr);
let swap_id = Uuid::new_v4();
let (event_loop, mut event_loop_handle) =
EventLoop::new(swap_id, swarm, alice_peer_id, bitcoin_wallet.clone())?;
let (event_loop, mut event_loop_handle) = EventLoop::new(
swap_id,
swarm,
seller_peer_id,
bitcoin_wallet.clone(),
env_config,
)?;
let event_loop = tokio::spawn(event_loop.run());
let max_givable = || bitcoin_wallet.max_giveable(TxLock::script_size());
@ -106,7 +103,7 @@ async fn main() -> Result<()> {
info!("Swapping {} with {} fees", send_bitcoin, fees);
db.insert_peer_id(swap_id, alice_peer_id).await?;
db.insert_peer_id(swap_id, seller_peer_id).await?;
let swap = Swap::new(
db,
@ -115,7 +112,7 @@ async fn main() -> Result<()> {
Arc::new(monero_wallet),
env_config,
event_loop_handle,
receive_monero_address,
monero_receive_address,
send_bitcoin,
);
@ -130,8 +127,6 @@ async fn main() -> Result<()> {
}
}
Command::History => {
let data_dir = data.0;
let db = Database::open(data_dir.join("database").as_path())
.context("Failed to open database")?;
@ -148,30 +143,25 @@ async fn main() -> Result<()> {
}
Command::Resume {
swap_id,
alice_multiaddr,
monero_params:
MoneroParams {
receive_monero_address,
monero_daemon_host,
},
electrum_rpc_url,
tor_socks5_port,
seller_addr,
bitcoin_electrum_rpc_url,
bitcoin_target_block,
monero_receive_address,
monero_daemon_address,
tor_socks5_port,
} => {
let data_dir = data.0;
cli::tracing::init(debug, data_dir.join("logs"), swap_id)?;
let db = Database::open(data_dir.join("database").as_path())
.context("Failed to open database")?;
let seed = Seed::from_file_or_generate(data_dir.as_path())
.context("Failed to read in seed file")?;
let env_config = env::Testnet::get_config();
if receive_monero_address.network != env_config.monero_network {
bail!("The given monero address is on network {:?}, expected address of network {:?}.", receive_monero_address.network, env_config.monero_network)
if monero_receive_address.network != env_config.monero_network {
bail!("The given monero address is on network {:?}, expected address of network {:?}.", monero_receive_address.network, env_config.monero_network)
}
let bitcoin_wallet = init_bitcoin_wallet(
electrum_rpc_url,
bitcoin_electrum_rpc_url,
&seed,
data_dir.clone(),
env_config,
@ -179,20 +169,25 @@ async fn main() -> Result<()> {
)
.await?;
let (monero_wallet, _process) =
init_monero_wallet(data_dir, monero_daemon_host, env_config).await?;
init_monero_wallet(data_dir, monero_daemon_address, env_config).await?;
let bitcoin_wallet = Arc::new(bitcoin_wallet);
let alice_peer_id = db.get_peer_id(swap_id)?;
let seller_peer_id = db.get_peer_id(swap_id)?;
let mut swarm = swarm::bob(&seed, alice_peer_id, tor_socks5_port).await?;
let mut swarm = swarm::bob(&seed, seller_peer_id, tor_socks5_port).await?;
let bob_peer_id = swarm.local_peer_id();
tracing::debug!("Our peer-id: {}", bob_peer_id);
swarm
.behaviour_mut()
.add_address(alice_peer_id, alice_multiaddr);
.add_address(seller_peer_id, seller_addr);
let (event_loop, event_loop_handle) =
EventLoop::new(swap_id, swarm, alice_peer_id, bitcoin_wallet.clone())?;
let (event_loop, event_loop_handle) = EventLoop::new(
swap_id,
swarm,
seller_peer_id,
bitcoin_wallet.clone(),
env_config,
)?;
let handle = tokio::spawn(event_loop.run());
let swap = Swap::from_db(
@ -202,7 +197,7 @@ async fn main() -> Result<()> {
Arc::new(monero_wallet),
env_config,
event_loop_handle,
receive_monero_address,
monero_receive_address,
)?;
tokio::select! {
@ -217,19 +212,17 @@ async fn main() -> Result<()> {
Command::Cancel {
swap_id,
force,
electrum_rpc_url,
bitcoin_electrum_rpc_url,
bitcoin_target_block,
} => {
let data_dir = data.0;
cli::tracing::init(debug, data_dir.join("logs"), swap_id)?;
let db = Database::open(data_dir.join("database").as_path())
.context("Failed to open database")?;
let seed = Seed::from_file_or_generate(data_dir.as_path())
.context("Failed to read in seed file")?;
let env_config = env::Testnet::get_config();
let bitcoin_wallet = init_bitcoin_wallet(
electrum_rpc_url,
bitcoin_electrum_rpc_url,
&seed,
data_dir,
env_config,
@ -251,19 +244,17 @@ async fn main() -> Result<()> {
Command::Refund {
swap_id,
force,
electrum_rpc_url,
bitcoin_electrum_rpc_url,
bitcoin_target_block,
} => {
let data_dir = data.0;
cli::tracing::init(debug, data_dir.join("logs"), swap_id)?;
let db = Database::open(data_dir.join("database").as_path())
.context("Failed to open database")?;
let seed = Seed::from_file_or_generate(data_dir.as_path())
.context("Failed to read in seed file")?;
let env_config = env::Testnet::get_config();
let bitcoin_wallet = init_bitcoin_wallet(
electrum_rpc_url,
bitcoin_electrum_rpc_url,
&seed,
data_dir,
env_config,
@ -303,7 +294,7 @@ async fn init_bitcoin_wallet(
async fn init_monero_wallet(
data_dir: PathBuf,
monero_daemon_host: String,
monero_daemon_address: String,
env_config: Config,
) -> Result<(monero::Wallet, monero::WalletRpcProcess)> {
let network = env_config.monero_network;
@ -313,7 +304,7 @@ async fn init_monero_wallet(
let monero_wallet_rpc = monero::WalletRpc::new(data_dir.join("monero")).await?;
let monero_wallet_rpc_process = monero_wallet_rpc
.run(network, monero_daemon_host.as_str())
.run(network, monero_daemon_address.as_str())
.await?;
let monero_wallet = monero::Wallet::open_or_create(

@ -37,6 +37,17 @@ use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::str::FromStr;
#[derive(Serialize, Deserialize)]
#[serde(remote = "Network")]
#[allow(non_camel_case_types)]
pub enum network {
#[serde(rename = "Mainnet")]
Bitcoin,
Testnet,
Signet,
Regtest,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct SecretKey {
inner: Scalar,

@ -16,6 +16,7 @@ use reqwest::Url;
use rust_decimal::prelude::*;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap};
use std::convert::TryFrom;
use std::fmt;
@ -108,18 +109,6 @@ impl Wallet {
Ok((txid, subscription))
}
pub async fn sign_and_finalize(&self, psbt: PartiallySignedTransaction) -> Result<Transaction> {
let (signed_psbt, finalized) = self.wallet.lock().await.sign(psbt, None)?;
if !finalized {
bail!("PSBT is not finalized")
}
let tx = signed_psbt.extract_tx();
Ok(tx)
}
pub async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
self.get_tx(txid)
.await?
@ -257,6 +246,18 @@ where
C: EstimateFeeRate,
D: BatchDatabase,
{
pub async fn sign_and_finalize(&self, psbt: PartiallySignedTransaction) -> Result<Transaction> {
let (signed_psbt, finalized) = self.wallet.lock().await.sign(psbt, None)?;
if !finalized {
bail!("PSBT is not finalized")
}
let tx = signed_psbt.extract_tx();
Ok(tx)
}
pub async fn balance(&self) -> Result<Amount> {
let balance = self
.wallet
@ -293,6 +294,10 @@ where
Ok(Amount::from_sat(fees))
}
/// Builds a partially signed transaction
///
/// Ensures that the address script is at output index `0`
/// for the partially signed transaction.
pub async fn send_to_address(
&self,
address: Address,
@ -301,11 +306,30 @@ where
let wallet = self.wallet.lock().await;
let client = self.client.lock().await;
let fee_rate = client.estimate_feerate(self.target_block)?;
let script = address.script_pubkey();
let mut tx_builder = wallet.build_tx();
tx_builder.add_recipient(address.script_pubkey(), amount.as_sat());
tx_builder.add_recipient(script.clone(), amount.as_sat());
tx_builder.fee_rate(fee_rate);
let (psbt, _details) = tx_builder.finish()?;
let mut psbt: PartiallySignedTransaction = psbt;
// When subscribing to transactions we depend on the relevant script being at
// output index 0, thus we ensure the relevant output to be at index `0`.
psbt.outputs.sort_by(|a, _| {
if a.witness_script.as_ref() == Some(&script) {
Ordering::Less
} else {
Ordering::Greater
}
});
psbt.global.unsigned_tx.output.sort_by(|a, _| {
if a.script_pubkey == script {
Ordering::Less
} else {
Ordering::Greater
}
});
Ok(psbt)
}
@ -480,7 +504,7 @@ where
use bitcoin::OutPoint;
use testutils::testutils;
let descriptors = testutils!(@descriptors ("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"));
let descriptors = testutils!(@descriptors ("wpkh(tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m/*)"));
let mut database = MemoryDatabase::new();
bdk::populate_test_db!(
@ -527,59 +551,39 @@ impl Watchable for (Txid, Script) {
pub struct Client {
electrum: bdk::electrum_client::Client,
latest_block: BlockHeight,
last_ping: Instant,
interval: Duration,
latest_block_height: BlockHeight,
last_sync: Instant,
sync_interval: Duration,
script_history: BTreeMap<Script, Vec<GetHistoryRes>>,
subscriptions: HashMap<(Txid, Script), Subscription>,
}
impl Client {
fn new(electrum: bdk::electrum_client::Client, interval: Duration) -> Result<Self> {
// Initially fetch the latest block for storing the height.
// We do not act on this subscription after this call.
let latest_block = electrum
.block_headers_subscribe()
.context("Failed to subscribe to header notifications")?;
Ok(Self {
electrum,
latest_block: BlockHeight::try_from(latest_block)?,
last_ping: Instant::now(),
interval,
latest_block_height: BlockHeight::try_from(latest_block)?,
last_sync: Instant::now(),
sync_interval: interval,
script_history: Default::default(),
subscriptions: Default::default(),
})
}
/// Ping the electrum server unless we already did within the set interval.
///
/// Returns a boolean indicating whether we actually pinged the server.
fn ping(&mut self) -> bool {
if self.last_ping.elapsed() <= self.interval {
return false;
}
match self.electrum.ping() {
Ok(()) => {
self.last_ping = Instant::now();
true
}
Err(error) => {
tracing::debug!(?error, "Failed to ping electrum server");
false
}
}
}
fn drain_notifications(&mut self) -> Result<()> {
let pinged = self.ping();
if !pinged {
fn update_state(&mut self) -> Result<()> {
let now = Instant::now();
if now < self.last_sync + self.sync_interval {
return Ok(());
}
self.drain_blockheight_notifications()?;
self.last_sync = now;
self.update_latest_block()?;
self.update_script_histories()?;
Ok(())
@ -596,7 +600,7 @@ impl Client {
self.script_history.insert(script.clone(), vec![]);
}
self.drain_notifications()?;
self.update_state()?;
let history = self.script_history.entry(script).or_default();
@ -618,7 +622,7 @@ impl Client {
Ok(ScriptStatus::Confirmed(
Confirmed::from_inclusion_and_latest_block(
u32::try_from(last.height)?,
u32::from(self.latest_block),
u32::from(self.latest_block_height),
),
))
}
@ -626,18 +630,24 @@ impl Client {
}
}
fn drain_blockheight_notifications(&mut self) -> Result<()> {
let latest_block = std::iter::from_fn(|| self.electrum.block_headers_pop().transpose())
.last()
.transpose()
.context("Failed to pop header notification")?;
fn update_latest_block(&mut self) -> Result<()> {
// Fetch the latest block for storing the height.
// We do not act on this subscription after this call, as we cannot rely on
// subscription push notifications because eventually the Electrum server will
// close the connection and subscriptions are not automatically renewed
// upon renewing the connection.
let latest_block = self
.electrum
.block_headers_subscribe()
.context("Failed to subscribe to header notifications")?;
let latest_block_height = BlockHeight::try_from(latest_block)?;
if let Some(new_block) = latest_block {
if latest_block_height > self.latest_block_height {
tracing::debug!(
block_height = new_block.height,
block_height = u32::from(latest_block_height),
"Got notification for new block"
);
self.latest_block = BlockHeight::try_from(new_block)?;
self.latest_block_height = latest_block_height;
}
Ok(())
@ -774,7 +784,7 @@ impl fmt::Display for ScriptStatus {
#[cfg(test)]
mod tests {
use super::*;
use crate::bitcoin::TxLock;
use crate::bitcoin::{PublicKey, TxLock};
use proptest::prelude::*;
#[test]
@ -1004,4 +1014,51 @@ mod tests {
assert!(amount.as_sat() > 0);
}
/// This test ensures that the relevant script output of the transaction
/// created out of the PSBT is at index 0. This is important because
/// subscriptions to the transaction are on index `0` when broadcasting the
/// transaction.
#[tokio::test]
async fn given_amounts_with_change_outputs_when_signing_tx_then_output_index_0_is_ensured_for_script(
) {
// We don't care about fees in this test, thus use a zero fee rate
struct NoFeeRate();
impl EstimateFeeRate for NoFeeRate {
fn estimate_feerate(&self, _target_block: usize) -> Result<FeeRate> {
Ok(FeeRate::from_sat_per_vb(0.0))
}
fn min_relay_fee(&self) -> Result<bitcoin::Amount> {
Ok(bitcoin::Amount::from_sat(0))
}
}
// This value is somewhat arbitrary but the indexation problem usually occurred
// on the first or second value (i.e. 547, 548) We keep the test
// iterations relatively low because these tests are expensive.
let above_dust = 547;
let balance = 2000;
let wallet = Wallet::new_funded(balance, NoFeeRate());
// sorting is only relevant for amounts that have a change output
// if the change output is below dust it will be dropped by the BDK
for amount in above_dust..(balance - (above_dust - 1)) {
let (A, B) = (PublicKey::random(), PublicKey::random());
let txlock = TxLock::new(&wallet, bitcoin::Amount::from_sat(amount), A, B)
.await
.unwrap();
let txlock_output = txlock.script_pubkey();
let tx = wallet.sign_and_finalize(txlock.into()).await.unwrap();
let tx_output = tx.output[0].script_pubkey.clone();
assert_eq!(
tx_output, txlock_output,
"Output {:?} index mismatch for amount {} and balance {}",
tx.output, amount, balance
);
}
}
}

File diff suppressed because it is too large Load Diff

@ -1,9 +1,10 @@
use crate::asb;
use crate::bitcoin::{CancelTimelock, PunishTimelock};
use std::cmp::max;
use std::time::Duration;
use time::NumericalStdDurationShort;
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Config {
pub bitcoin_lock_confirmed_timeout: Duration,
pub bitcoin_finality_confirmations: u32,
@ -43,13 +44,13 @@ impl GetConfig for Mainnet {
fn get_config() -> Config {
Config {
bitcoin_lock_confirmed_timeout: 24.hours(),
bitcoin_finality_confirmations: 3,
bitcoin_finality_confirmations: 2,
bitcoin_avg_block_time: 10.minutes(),
bitcoin_cancel_timelock: CancelTimelock::new(72),
bitcoin_punish_timelock: PunishTimelock::new(72),
bitcoin_network: bitcoin::Network::Bitcoin,
monero_avg_block_time: 2.minutes(),
monero_finality_confirmations: 15,
monero_finality_confirmations: 10,
monero_network: monero::Network::Mainnet,
}
}
@ -59,8 +60,8 @@ impl GetConfig for Testnet {
fn get_config() -> Config {
Config {
bitcoin_lock_confirmed_timeout: 12.hours(),
bitcoin_finality_confirmations: 1,
bitcoin_avg_block_time: 5.minutes(),
bitcoin_finality_confirmations: 2,
bitcoin_avg_block_time: 10.minutes(),
bitcoin_cancel_timelock: CancelTimelock::new(12),
bitcoin_punish_timelock: PunishTimelock::new(6),
bitcoin_network: bitcoin::Network::Testnet,
@ -91,6 +92,33 @@ fn sync_interval(avg_block_time: Duration) -> Duration {
max(avg_block_time / 10, Duration::from_secs(1))
}
pub fn new(is_testnet: bool, asb_config: &asb::config::Config) -> Config {
let env_config = if is_testnet {
Testnet::get_config()
} else {
Mainnet::get_config()
};
let env_config =
if let Some(bitcoin_finality_confirmations) = asb_config.bitcoin.finality_confirmations {
Config {
bitcoin_finality_confirmations,
..env_config
}
} else {
env_config
};
if let Some(monero_finality_confirmations) = asb_config.monero.finality_confirmations {
Config {
monero_finality_confirmations,
..env_config
}
} else {
env_config
}
}
#[cfg(test)]
mod tests {
use super::*;

@ -1,7 +1,8 @@
pub mod wallet;
mod wallet_rpc;
pub use ::monero::{Address, Network, PrivateKey, PublicKey};
pub use ::monero::network::Network;
pub use ::monero::{Address, PrivateKey, PublicKey};
pub use curve25519_dalek::scalar::Scalar;
pub use wallet::Wallet;
pub use wallet_rpc::{WalletRpc, WalletRpcProcess};
@ -19,6 +20,15 @@ use std::str::FromStr;
pub const PICONERO_OFFSET: u64 = 1_000_000_000_000;
#[derive(Serialize, Deserialize)]
#[serde(remote = "Network")]
#[allow(non_camel_case_types)]
pub enum network {
Mainnet,
Stagenet,
Testnet,
}
pub fn private_key_from_secp256k1_scalar(scalar: bitcoin::Scalar) -> PrivateKey {
let mut bytes = scalar.to_bytes();

@ -115,7 +115,7 @@ impl WalletRpc {
Ok(monero_wallet_rpc)
}
pub async fn run(&self, network: Network, daemon_host: &str) -> Result<WalletRpcProcess> {
pub async fn run(&self, network: Network, daemon_address: &str) -> Result<WalletRpcProcess> {
let port = tokio::net::TcpListener::bind("127.0.0.1:0")
.await?
.local_addr()?
@ -126,17 +126,25 @@ impl WalletRpc {
"Starting monero-wallet-rpc on"
);
let network_flag = match network {
Network::Mainnet => {
vec![]
}
Network::Stagenet => {
vec!["--stagenet"]
}
Network::Testnet => {
vec!["--testnet"]
}
};
let mut child = Command::new(self.exec_path())
.env("LANG", "en_AU.UTF-8")
.stdout(Stdio::piped())
.kill_on_drop(true)
.arg(match network {
Network::Mainnet => "--mainnet",
Network::Stagenet => "--stagenet",
Network::Testnet => "--testnet",
})
.arg("--daemon-host")
.arg(daemon_host)
.args(network_flag)
.arg("--daemon-address")
.arg(daemon_address)
.arg("--rpc-bind-port")
.arg(format!("{}", port))
.arg("--disable-rpc-login")

@ -32,6 +32,7 @@ impl ProtocolName for SpotPriceProtocol {
pub struct Request {
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
pub btc: bitcoin::Amount,
pub blockchain_network: BlockchainNetwork,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@ -59,11 +60,23 @@ pub enum Error {
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
buy: bitcoin::Amount,
},
BlockchainNetworkMismatch {
cli: BlockchainNetwork,
asb: BlockchainNetwork,
},
/// To be used for errors that cannot be explained on the CLI side (e.g.
/// rate update problems on the seller side)
Other,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct BlockchainNetwork {
#[serde(with = "crate::bitcoin::network")]
pub bitcoin: bitcoin::Network,
#[serde(with = "crate::monero::network")]
pub monero: monero::Network,
}
#[cfg(test)]
mod tests {
use super::*;
@ -103,6 +116,21 @@ mod tests {
.unwrap();
assert_eq!(error, serialized);
let error = r#"{"Error":{"BlockchainNetworkMismatch":{"cli":{"bitcoin":"Mainnet","monero":"Mainnet"},"asb":{"bitcoin":"Testnet","monero":"Stagenet"}}}}"#.to_string();
let serialized =
serde_json::to_string(&Response::Error(Error::BlockchainNetworkMismatch {
cli: BlockchainNetwork {
bitcoin: bitcoin::Network::Bitcoin,
monero: monero::Network::Mainnet,
},
asb: BlockchainNetwork {
bitcoin: bitcoin::Network::Testnet,
monero: monero::Network::Stagenet,
},
}))
.unwrap();
assert_eq!(error, serialized);
let error = r#"{"Error":"Other"}"#.to_string();
let serialized = serde_json::to_string(&Response::Error(Error::Other)).unwrap();
assert_eq!(error, serialized);

@ -2,12 +2,13 @@ use crate::network::transport;
use crate::protocol::alice::event_loop::LatestRate;
use crate::protocol::{alice, bob};
use crate::seed::Seed;
use crate::{monero, tor};
use crate::{env, monero, tor};
use anyhow::Result;
use libp2p::swarm::{NetworkBehaviour, SwarmBuilder};
use libp2p::{PeerId, Swarm};
use std::fmt::Debug;
#[allow(clippy::too_many_arguments)]
pub fn alice<LR>(
seed: &Seed,
balance: monero::Amount,
@ -16,6 +17,7 @@ pub fn alice<LR>(
max_buy: bitcoin::Amount,
latest_rate: LR,
resume_only: bool,
env_config: env::Config,
) -> Result<Swarm<alice::Behaviour<LR>>>
where
LR: LatestRate + Send + 'static + Debug,
@ -29,6 +31,7 @@ where
max_buy,
latest_rate,
resume_only,
env_config,
),
)
}

@ -1,8 +1,8 @@
use crate::monero;
use crate::network::quote::BidQuote;
use crate::network::{encrypted_signature, quote, transfer_proof};
use crate::protocol::alice::event_loop::LatestRate;
use crate::protocol::alice::{execution_setup, spot_price, State3};
use crate::{env, monero};
use anyhow::{anyhow, Error};
use libp2p::request_response::{RequestId, ResponseChannel};
use libp2p::{NetworkBehaviour, PeerId};
@ -88,6 +88,7 @@ where
max_buy: bitcoin::Amount,
latest_rate: LR,
resume_only: bool,
env_config: env::Config,
) -> Self {
Self {
quote: quote::alice(),
@ -96,6 +97,7 @@ where
lock_fee,
min_buy,
max_buy,
env_config,
latest_rate,
resume_only,
),

@ -211,7 +211,8 @@ where
match error {
Error::ResumeOnlyMode
| Error::AmountBelowMinimum { .. }
| Error::AmountAboveMaximum { .. } => {
| Error::AmountAboveMaximum { .. }
| Error::BlockchainNetworkMismatch { .. } => {
tracing::warn!(%peer, "Ignoring spot price request because: {}", error);
}
Error::BalanceTooLow { .. }

@ -1,9 +1,9 @@
use crate::monero;
use crate::network::cbor_request_response::CborCodec;
use crate::network::spot_price;
use crate::network::spot_price::SpotPriceProtocol;
use crate::network::spot_price::{BlockchainNetwork, SpotPriceProtocol};
use crate::protocol::alice;
use crate::protocol::alice::event_loop::LatestRate;
use crate::{env, monero};
use libp2p::request_response::{
ProtocolSupport, RequestResponseConfig, RequestResponseEvent, RequestResponseMessage,
ResponseChannel,
@ -48,6 +48,8 @@ where
#[behaviour(ignore)]
max_buy: bitcoin::Amount,
#[behaviour(ignore)]
env_config: env::Config,
#[behaviour(ignore)]
latest_rate: LR,
#[behaviour(ignore)]
resume_only: bool,
@ -66,6 +68,7 @@ where
lock_fee: monero::Amount,
min_buy: bitcoin::Amount,
max_buy: bitcoin::Amount,
env_config: env::Config,
latest_rate: LR,
resume_only: bool,
) -> Self {
@ -80,6 +83,7 @@ where
lock_fee,
min_buy,
max_buy,
env_config,
latest_rate,
resume_only,
}
@ -154,6 +158,19 @@ where
}
};
let blockchain_network = BlockchainNetwork {
bitcoin: self.env_config.bitcoin_network,
monero: self.env_config.monero_network,
};
if request.blockchain_network != blockchain_network {
self.decline(peer, channel, Error::BlockchainNetworkMismatch {
cli: request.blockchain_network,
asb: blockchain_network,
});
return;
}
if self.resume_only {
self.decline(peer, channel, Error::ResumeOnlyMode);
return;
@ -246,12 +263,15 @@ pub enum Error {
balance: monero::Amount,
buy: bitcoin::Amount,
},
#[error("Failed to fetch latest rate")]
LatestRateFetchFailed(#[source] Box<dyn std::error::Error + Send + 'static>),
#[error("Failed to calculate quote: {0}")]
SellQuoteCalculationFailed(#[source] anyhow::Error),
#[error("Blockchain networks did not match, we are on {asb:?}, but request from {cli:?}")]
BlockchainNetworkMismatch {
cli: spot_price::BlockchainNetwork,
asb: spot_price::BlockchainNetwork,
},
}
impl Error {
@ -267,6 +287,12 @@ impl Error {
buy: *buy,
},
Error::BalanceTooLow { buy, .. } => spot_price::Error::BalanceTooLow { buy: *buy },
Error::BlockchainNetworkMismatch { cli, asb } => {
spot_price::Error::BlockchainNetworkMismatch {
cli: *cli,
asb: *asb,
}
}
Error::LatestRateFetchFailed(_) | Error::SellQuoteCalculationFailed(_) => {
spot_price::Error::Other
}
@ -278,6 +304,7 @@ impl Error {
mod tests {
use super::*;
use crate::asb::Rate;
use crate::env::GetConfig;
use crate::monero;
use crate::network::test::{await_events_or_timeout, connect, new_swarm};
use crate::protocol::{alice, bob};
@ -294,6 +321,7 @@ mod tests {
max_buy: bitcoin::Amount::from_btc(0.01).unwrap(),
rate: TestRate::default(), // 0.01
resume_only: false,
env_config: env::Testnet::get_config(),
}
}
}
@ -305,9 +333,7 @@ mod tests {
let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap();
let expected_xmr = monero::Amount::from_monero(1.0).unwrap();
let request = spot_price::Request { btc: btc_to_swap };
test.send_request(request);
test.construct_and_send_request(btc_to_swap);
test.assert_price((btc_to_swap, expected_xmr), expected_xmr)
.await;
}
@ -321,9 +347,7 @@ mod tests {
let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap();
let request = spot_price::Request { btc: btc_to_swap };
test.send_request(request);
test.construct_and_send_request(btc_to_swap);
test.assert_error(
alice::spot_price::Error::BalanceTooLow {
balance: monero::Amount::ZERO,
@ -341,9 +365,7 @@ mod tests {
let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap();
let expected_xmr = monero::Amount::from_monero(1.0).unwrap();
let request = spot_price::Request { btc: btc_to_swap };
test.send_request(request);
test.construct_and_send_request(btc_to_swap);
test.assert_price((btc_to_swap, expected_xmr), expected_xmr)
.await;
@ -351,9 +373,7 @@ mod tests {
.behaviour_mut()
.update_balance(monero::Amount::ZERO);
let request = spot_price::Request { btc: btc_to_swap };
test.send_request(request);
test.construct_and_send_request(btc_to_swap);
test.assert_error(
alice::spot_price::Error::BalanceTooLow {
balance: monero::Amount::ZERO,
@ -376,10 +396,7 @@ mod tests {
.await;
let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap();
let request = spot_price::Request { btc: btc_to_swap };
test.send_request(request);
test.construct_and_send_request(btc_to_swap);
test.assert_error(
alice::spot_price::Error::BalanceTooLow {
balance,
@ -398,10 +415,7 @@ mod tests {
SpotPriceTest::setup(AliceBehaviourValues::default().with_min_buy(min_buy)).await;
let btc_to_swap = bitcoin::Amount::from_btc(0.0001).unwrap();
let request = spot_price::Request { btc: btc_to_swap };
test.send_request(request);
test.construct_and_send_request(btc_to_swap);
test.assert_error(
alice::spot_price::Error::AmountBelowMinimum {
buy: btc_to_swap,
@ -424,9 +438,7 @@ mod tests {
let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap();
let request = spot_price::Request { btc: btc_to_swap };
test.send_request(request);
test.construct_and_send_request(btc_to_swap);
test.assert_error(
alice::spot_price::Error::AmountAboveMaximum {
buy: btc_to_swap,
@ -446,10 +458,7 @@ mod tests {
SpotPriceTest::setup(AliceBehaviourValues::default().with_resume_only(true)).await;
let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap();
let request = spot_price::Request { btc: btc_to_swap };
test.send_request(request);
test.construct_and_send_request(btc_to_swap);
test.assert_error(
alice::spot_price::Error::ResumeOnlyMode,
bob::spot_price::Error::NoSwapsAccepted,
@ -464,10 +473,7 @@ mod tests {
.await;
let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap();
let request = spot_price::Request { btc: btc_to_swap };
test.send_request(request);
test.construct_and_send_request(btc_to_swap);
test.assert_error(
alice::spot_price::Error::LatestRateFetchFailed(Box::new(TestRateError {})),
bob::spot_price::Error::Other,
@ -484,9 +490,7 @@ mod tests {
let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap();
let request = spot_price::Request { btc: btc_to_swap };
test.send_request(request);
test.construct_and_send_request(btc_to_swap);
test.assert_error(
alice::spot_price::Error::SellQuoteCalculationFailed(anyhow!(
"Error text irrelevant, won't be checked here"
@ -496,6 +500,79 @@ mod tests {
.await;
}
#[tokio::test]
async fn given_alice_mainnnet_bob_testnet_then_network_mismatch_error() {
let mut test = SpotPriceTest::setup(
AliceBehaviourValues::default().with_env_config(env::Mainnet::get_config()),
)
.await;
let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap();
test.construct_and_send_request(btc_to_swap);
test.assert_error(
alice::spot_price::Error::BlockchainNetworkMismatch {
cli: BlockchainNetwork {
bitcoin: bitcoin::Network::Testnet,
monero: monero::Network::Stagenet,
},
asb: BlockchainNetwork {
bitcoin: bitcoin::Network::Bitcoin,
monero: monero::Network::Mainnet,
},
},
bob::spot_price::Error::BlockchainNetworkMismatch {
cli: BlockchainNetwork {
bitcoin: bitcoin::Network::Testnet,
monero: monero::Network::Stagenet,
},
asb: BlockchainNetwork {
bitcoin: bitcoin::Network::Bitcoin,
monero: monero::Network::Mainnet,
},
},
)
.await;
}
#[tokio::test]
async fn given_alice_testnet_bob_mainnet_then_network_mismatch_error() {
let mut test = SpotPriceTest::setup(AliceBehaviourValues::default()).await;
let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap();
let request = spot_price::Request {
btc: btc_to_swap,
blockchain_network: BlockchainNetwork {
bitcoin: bitcoin::Network::Bitcoin,
monero: monero::Network::Mainnet,
},
};
test.send_request(request);
test.assert_error(
alice::spot_price::Error::BlockchainNetworkMismatch {
cli: BlockchainNetwork {
bitcoin: bitcoin::Network::Bitcoin,
monero: monero::Network::Mainnet,
},
asb: BlockchainNetwork {
bitcoin: bitcoin::Network::Testnet,
monero: monero::Network::Stagenet,
},
},
bob::spot_price::Error::BlockchainNetworkMismatch {
cli: BlockchainNetwork {
bitcoin: bitcoin::Network::Bitcoin,
monero: monero::Network::Mainnet,
},
asb: BlockchainNetwork {
bitcoin: bitcoin::Network::Testnet,
monero: monero::Network::Stagenet,
},
},
)
.await;
}
struct SpotPriceTest {
alice_swarm: Swarm<alice::spot_price::Behaviour<TestRate>>,
bob_swarm: Swarm<spot_price::Behaviour>,
@ -511,6 +588,7 @@ mod tests {
values.lock_fee,
values.min_buy,
values.max_buy,
values.env_config,
values.rate.clone(),
values.resume_only,
)
@ -526,6 +604,17 @@ mod tests {
}
}
pub fn construct_and_send_request(&mut self, btc_to_swap: bitcoin::Amount) {
let request = spot_price::Request {
btc: btc_to_swap,
blockchain_network: BlockchainNetwork {
bitcoin: bitcoin::Network::Testnet,
monero: monero::Network::Stagenet,
},
};
self.send_request(request);
}
pub fn send_request(&mut self, spot_price_request: spot_price::Request) {
self.bob_swarm
.behaviour_mut()
@ -588,6 +677,19 @@ mod tests {
assert_eq!(balance1, balance2);
assert_eq!(buy1, buy2);
}
(
alice::spot_price::Error::BlockchainNetworkMismatch {
cli: cli1,
asb: asb1,
},
alice::spot_price::Error::BlockchainNetworkMismatch {
cli: cli2,
asb: asb2,
},
) => {
assert_eq!(cli1, cli2);
assert_eq!(asb1, asb2);
}
(
alice::spot_price::Error::AmountBelowMinimum { .. },
alice::spot_price::Error::AmountBelowMinimum { .. },
@ -640,6 +742,7 @@ mod tests {
pub max_buy: bitcoin::Amount,
pub rate: TestRate, // 0.01
pub resume_only: bool,
pub env_config: env::Config,
}
impl AliceBehaviourValues {
@ -672,6 +775,11 @@ mod tests {
self.rate = rate;
self
}
pub fn with_env_config(mut self, env_config: env::Config) -> AliceBehaviourValues {
self.env_config = env_config;
self
}
}
#[derive(Clone, Debug)]

@ -1,10 +1,10 @@
use crate::bitcoin::EncryptedSignature;
use crate::network::quote::BidQuote;
use crate::network::spot_price::Response;
use crate::network::spot_price::{BlockchainNetwork, Response};
use crate::network::{encrypted_signature, spot_price};
use crate::protocol::bob;
use crate::protocol::bob::{Behaviour, OutEvent, State0, State2};
use crate::{bitcoin, monero};
use crate::{bitcoin, env, monero};
use anyhow::{bail, Context, Result};
use futures::future::{BoxFuture, OptionFuture};
use futures::{FutureExt, StreamExt};
@ -55,6 +55,7 @@ impl EventLoop {
swarm: Swarm<Behaviour>,
alice_peer_id: PeerId,
bitcoin_wallet: Arc<bitcoin::Wallet>,
env_config: env::Config,
) -> Result<(Self, EventLoopHandle)> {
let execution_setup = bmrng::channel_with_timeout(1, Duration::from_secs(30));
let transfer_proof = bmrng::channel_with_timeout(1, Duration::from_secs(30));
@ -85,6 +86,7 @@ impl EventLoop {
encrypted_signature: encrypted_signature.0,
spot_price: spot_price.0,
quote: quote.0,
env_config,
};
Ok((event_loop, handle))
@ -242,6 +244,7 @@ pub struct EventLoopHandle {
encrypted_signature: bmrng::RequestSender<EncryptedSignature, ()>,
spot_price: bmrng::RequestSender<spot_price::Request, spot_price::Response>,
quote: bmrng::RequestSender<(), BidQuote>,
env_config: env::Config,
}
impl EventLoopHandle {
@ -265,7 +268,13 @@ impl EventLoopHandle {
pub async fn request_spot_price(&mut self, btc: bitcoin::Amount) -> Result<monero::Amount> {
let response = self
.spot_price
.send_receive(spot_price::Request { btc })
.send_receive(spot_price::Request {
btc,
blockchain_network: BlockchainNetwork {
bitcoin: self.env_config.bitcoin_network,
monero: self.env_config.monero_network,
},
})
.await?;
match response {

@ -54,6 +54,12 @@ pub enum Error {
#[error("Seller's XMR balance is currently too low to fulfill the swap request to buy {buy}, please try again later")]
BalanceTooLow { buy: bitcoin::Amount },
#[error("Seller blockchain network {asb:?} setup did not match your blockchain network setup {cli:?}")]
BlockchainNetworkMismatch {
cli: spot_price::BlockchainNetwork,
asb: spot_price::BlockchainNetwork,
},
/// To be used for errors that cannot be explained on the CLI side (e.g.
/// rate update problems on the seller side)
#[error("Seller encountered a problem, please try again later.")]
@ -71,6 +77,9 @@ impl From<spot_price::Error> for Error {
Error::AmountAboveMaximum { max, buy }
}
spot_price::Error::BalanceTooLow { buy } => Error::BalanceTooLow { buy },
spot_price::Error::BlockchainNetworkMismatch { cli, asb } => {
Error::BlockchainNetworkMismatch { cli, asb }
}
spot_price::Error::Other => Error::Other,
}
}

@ -239,6 +239,7 @@ async fn start_alice(
max_buy,
latest_rate,
resume_only,
env_config,
)
.unwrap();
swarm.listen_on(listen_address).unwrap();
@ -458,6 +459,7 @@ impl BobParams {
swarm,
self.alice_peer_id,
self.bitcoin_wallet.clone(),
self.env_config,
)
}
}

Loading…
Cancel
Save