Repository: farcaster-project/farcaster-node
Pull request #231 (2021-11-23 14:33:15)

Swap functional test
Before running the test, the docker-containers have to be started again. in /tests do `docker-compose up`, then `sudo chown -R $USER data_dir`.

I currently run the test with:
```
cargo test -- --nocapture --ignored swap_test
```

The processes don't exit cleanly yet, for cleanup I run `rm -rf tests/.farcaster_node_0 tests/.farcaster_node_1  && sudo killall peerd swapd farcasterd walletd syncerd` from the project's root directory.

TheCharlatan: I marked this ready for review, but it seems like we again have issues with the containers. This also includes hacky work to get the Local network type accepted, which conflicts with the work in #230. This should not be merged before that.
codecov-commenter: # [Codecov](https://codecov.io/gh/farcaster-project/farcaster-node/pull/231?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#231](https://codecov.io/gh/farcaster-project/farcaster-node/pull/231?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (62118f6) into [main](https://codecov.io/gh/farcaster-project/farcaster-node/commit/a09ddc24b19d6c6c28ba6d6a5b1e6a3c5d3160d1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (a09ddc2) will **increase** coverage by `0.0%`.
> The diff coverage is `20.0%`.

> :exclamation: Current head 62118f6 differs from pull request most recent head 3943167. Consider uploading reports for the commit 3943167 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/farcaster-project/farcaster-node/pull/231/graphs/tree.svg?width=650&height=150&src=pr&token=ZZ9RnFhgFC&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)](https://codecov.io/gh/farcaster-project/farcaster-node/pull/231?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@          Coverage Diff          @@
##            main    #231   +/-   ##
=====================================
  Coverage   15.1%   15.1%           
=====================================
  Files         27      27           
  Lines       6326    6325    -1     
=====================================
  Hits         954     954           
+ Misses      5372    5371    -1     
```


| [Impacted Files](https://codecov.io/gh/farcaster-project/farcaster-node/pull/231?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [src/syncerd/bitcoin\_syncer.rs](https://codecov.io/gh/farcaster-project/farcaster-node/pull/231/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3N5bmNlcmQvYml0Y29pbl9zeW5jZXIucnM=) | `0.0% <0.0%> (ø)` | |
| [src/syncerd/monero\_syncer.rs](https://codecov.io/gh/farcaster-project/farcaster-node/pull/231/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3N5bmNlcmQvbW9uZXJvX3N5bmNlci5ycw==) | `0.0% <0.0%> (ø)` | |
| [src/syncerd/runtime.rs](https://codecov.io/gh/farcaster-project/farcaster-node/pull/231/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3N5bmNlcmQvcnVudGltZS5ycw==) | `0.0% <ø> (ø)` | |
| [src/cli/command.rs](https://codecov.io/gh/farcaster-project/farcaster-node/pull/231/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NsaS9jb21tYW5kLnJz) | `40.5% <100.0%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/farcaster-project/farcaster-node/pull/231?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/farcaster-project/farcaster-node/pull/231?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [a09ddc2...3943167](https://codecov.io/gh/farcaster-project/farcaster-node/pull/231?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index d43afaca..d0467f80 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -979,6 +979,7 @@ dependencies = [
  "slip132 0.3.3",
  "strict_encoding",
  "strict_encoding_derive",
+ "strip-ansi-escapes",
  "sysinfo",
  "tokio",
  "toml 0.5.8",
@@ -2910,6 +2911,15 @@ dependencies = [
  "serde_str_helpers",
 ]
 
+[[package]]
+name = "strip-ansi-escapes"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8"
+dependencies = [
+ "vte",
+]
+
 [[package]]
 name = "strsim"
 version = "0.10.0"
@@ -3269,6 +3279,12 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "utf8parse"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
+
 [[package]]
 name = "uuid"
 version = "0.8.2"
@@ -3302,6 +3318,27 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
 
+[[package]]
+name = "vte"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983"
+dependencies = [
+ "arrayvec",
+ "utf8parse",
+ "vte_generate_state_changes",
+]
+
+[[package]]
+name = "vte_generate_state_changes"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
 [[package]]
 name = "want"
 version = "0.3.0"
diff --git a/Cargo.toml b/Cargo.toml
index c3f67af2..adf3de98 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -131,6 +131,9 @@ features = ['peer']
 [dev-dependencies]
 ntest = "0.7.3"
 
+# Test
+strip-ansi-escapes = "0.1.1"
+
 [build-dependencies.microservices]
 # path = '../ext/rust-internet2'
 version = "0.5.0-alpha.1"
diff --git a/src/cli/command.rs b/src/cli/command.rs
index fbe9c335..308075e4 100644
--- a/src/cli/command.rs
+++ b/src/cli/command.rs
@@ -127,9 +127,9 @@ impl Exec for Command {
                 port,
                 overlay,
             } => {
-                if network != Network::Testnet {
+                if network != Network::Testnet && network != Network::Local {
                     eprintln!(
-                        "Error: {} not yet supported. Only Testnet currently enabled, for your funds safety",
+                        "Error: {} not yet supported. Only Testnet and Local currently enabled, for your funds safety",
                         network
                     );
                     return Ok(());
diff --git a/src/syncerd/bitcoin_syncer.rs b/src/syncerd/bitcoin_syncer.rs
index b764657d..424e0275 100644
--- a/src/syncerd/bitcoin_syncer.rs
+++ b/src/syncerd/bitcoin_syncer.rs
@@ -443,7 +443,7 @@ async fn run_syncerd_task_receiver(
                         }
                         Task::BroadcastTransaction(task) => {
                             // TODO: match error and emit event with fail code
-                            trace!("trying to broadcast tx: {:?}", task.tx.to_hex());
+                            debug!("trying to broadcast tx: {:?}", task.tx.to_hex());
                             match Client::new(&electrum_server).and_then(|broadcast_client| {
                                 broadcast_client.transaction_broadcast_raw(&task.tx.clone())
                             }) {


Test patch:
diff --git a/tests/.farcasterd_1.toml b/tests/.farcasterd_1.toml
new file mode 100644
index 00000000..82360b1b
--- /dev/null
+++ b/tests/.farcasterd_1.toml
@@ -0,0 +1,5 @@
+[syncers.local]
+electrum_server = "tcp://localhost:50001"
+monero_daemon = "http://localhost:18081"
+monero_rpc_wallet = "http://localhost:18084"
+
diff --git a/tests/.farcasterd_2.toml b/tests/.farcasterd_2.toml
new file mode 100644
index 00000000..31874831
--- /dev/null
+++ b/tests/.farcasterd_2.toml
@@ -0,0 +1,5 @@
+[syncers.local]
+electrum_server = "tcp://localhost:50001"
+monero_daemon = "http://localhost:18081"
+monero_rpc_wallet = "http://localhost:18085"
+
diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml
index 9f4ae3e1..f52f92dc 100644
--- a/tests/docker-compose.yml
+++ b/tests/docker-compose.yml
@@ -45,4 +45,11 @@ services:
             - "monerod"
         ports:
             - 18084:18083
-
+    monero-wallet-rpc-3:
+        image: ghcr.io/farcaster-project/containers/monero-wallet-rpc:0.17.2.3
+        environment:
+            MONEROD_ADDRESS: monerod:18081
+        depends_on:
+            - "monerod"
+        ports:
+            - 18085:18083
diff --git a/tests/functional-swap.rs b/tests/functional-swap.rs
new file mode 100644
index 00000000..675e9a56
--- /dev/null
+++ b/tests/functional-swap.rs
@@ -0,0 +1,603 @@
+#[macro_use]
+extern crate log;
+
+use bitcoincore_rpc::{Auth, Client, RpcApi};
+use std::ffi::OsStr;
+use std::io;
+use std::process;
+use std::{thread, time};
+use sysinfo::{ProcessExt, System, SystemExt};
+
+use std::collections::HashMap;
+use std::str;
+use std::str::FromStr;
+
+const ALLOWED_RETRIES: u32 = 120;
+
+#[tokio::test]
+#[ignore]
+async fn swap_test_bob_maker() {
+    let bitcoin_rpc = bitcoin_setup();
+    let (monero_regtest, monero_wallet) = monero_setup().await;
+
+    let (farcasterd_maker, data_dir_maker, farcasterd_taker, data_dir_taker) =
+        setup_farcaster_clients().await;
+
+    let (_, bitcoin_address, swap_ids) = make_and_take_offer(
+        data_dir_maker.clone(),
+        data_dir_taker.clone(),
+        "Bob".to_string(),
+        &bitcoin_rpc,
+        &monero_wallet,
+    )
+    .await;
+
+    run_swap(
+        swap_ids[0].clone(),
+        data_dir_taker,
+        data_dir_maker,
+        bitcoin_rpc,
+        bitcoin_address,
+        monero_regtest,
+        monero_wallet,
+    )
+    .await;
+
+    cleanup_processes(farcasterd_maker, farcasterd_taker);
+}
+
+#[tokio::test]
+#[ignore]
+async fn swap_test_alice_maker() {
+    let bitcoin_rpc = bitcoin_setup();
+    let (monero_regtest, monero_wallet) = monero_setup().await;
+
+    let (farcasterd_maker, data_dir_maker, farcasterd_taker, data_dir_taker) =
+        setup_farcaster_clients().await;
+
+    let (_, bitcoin_address, swap_ids) = make_and_take_offer(
+        data_dir_maker.clone(),
+        data_dir_taker.clone(),
+        "Alice".to_string(),
+        &bitcoin_rpc,
+        &monero_wallet,
+    )
+    .await;
+
+    run_swap(
+        swap_ids[0].clone(),
+        data_dir_maker,
+        data_dir_taker,
+        bitcoin_rpc,
+        bitcoin_address,
+        monero_regtest,
+        monero_wallet,
+    )
+    .await;
+
+    cleanup_processes(farcasterd_maker, farcasterd_taker);
+}
+
+async fn setup_farcaster_clients() -> (process::Child, Vec<String>, process::Child, Vec<String>) {
+    // data directories
+    let data_dir_maker = vec!["-d".to_string(), "tests/.farcaster_node_0".to_string()];
+    let data_dir_taker = vec!["-d".to_string(), "tests/.farcaster_node_1".to_string()];
+
+    let server_args_maker = vec!["-vvv", "--config", "tests/.farcasterd_1.toml"]
+        .iter()
+        .map(|i| i.to_string())
+        .collect();
+
+    let server_args_taker = vec!["-vvv", "--config", "tests/.farcasterd_2.toml"]
+        .iter()
+        .map(|i| i.to_string())
+        .collect();
+
+    let farcasterd_maker_args = farcasterd_args(data_dir_maker.clone(), server_args_maker);
+    let farcasterd_taker_args = farcasterd_args(data_dir_taker.clone(), server_args_taker);
+
+    let farcasterd_maker = launch("../farcasterd", farcasterd_maker_args).unwrap();
+    let farcasterd_taker = launch("../farcasterd", farcasterd_taker_args).unwrap();
+    (
+        farcasterd_maker,
+        data_dir_maker,
+        farcasterd_taker,
+        data_dir_taker,
+    )
+}
+
+async fn make_and_take_offer(
+    data_dir_maker: Vec<String>,
+    data_dir_taker: Vec<String>,
+    role: String,
+    bitcoin_rpc: &bitcoincore_rpc::Client,
+    monero_wallet: &monero_rpc::WalletClient,
+) -> (monero::Address, bitcoin::Address, Vec<String>) {
+    let maker_info_args = info_args(data_dir_maker.clone());
+
+    // test connection to farcasterd and check that swap-cli is in the correct place
+    run("../swap-cli", maker_info_args.clone()).unwrap();
+
+    let xmr_address = monero_wallet.get_address(0, None).await.unwrap().address;
+    let btc_address = bitcoin_rpc.get_new_address(None, None).unwrap();
+
+    let btc_addr = btc_address.to_string();
+    let xmr_addr = xmr_address.to_string();
+
+    let cli_make_args = make_offer_args(
+        data_dir_maker.clone(),
+        role,
+        btc_addr.clone(),
+        xmr_addr.clone(),
+    );
+    let (_stdout, _stderr) = run("../swap-cli", cli_make_args).unwrap();
+
+    // get offer strings
+    let offers = retry_until_offer(maker_info_args.clone());
+
+    let cli_take_args = take_offer_args(
+        data_dir_taker.clone(),
+        btc_addr,
+        xmr_addr,
+        offers[0].clone(),
+    );
+    run("../swap-cli", cli_take_args).unwrap();
+
+    let swap_ids = retry_until_swap_ids(maker_info_args.clone());
+
+    (xmr_address, btc_address, swap_ids)
+}
+
+async fn run_swap(
+    swap_id: String,
+    data_dir_alice: Vec<String>,
+    data_dir_bob: Vec<String>,
+    bitcoin_rpc: bitcoincore_rpc::Client,
+    funding_btc_address: bitcoin::Address,
+    monero_regtest: monero_rpc::RegtestDaemonClient,
+    monero_wallet: monero_rpc::WalletClient,
+) {
+    // let btc_address = bitcoin_rpc.get_new_address(None, None).unwrap();
+    let cli_alice_progress_args: Vec<String> = progress_args(data_dir_alice, swap_id.clone());
+    let cli_bob_progress_args: Vec<String> = progress_args(data_dir_bob, swap_id.clone());
+
+    bitcoin_rpc
+        .generate_to_address(1, &reusable_btc_address())
+        .unwrap();
+
+    // run until bob has the btc funding address
+    let address = retry_until_bitcoin_funding_address(cli_bob_progress_args.clone());
+
+    // this seems to improve reliability, probably there is a race condition somewhere with the address being funded too early
+    // FIXME: change this once https://github.com/farcaster-project/farcaster-node/issues/225 is fixed
+    thread::sleep(time::Duration::from_secs(10));
+
+    // fund the bitcoin address
+    let amount = bitcoin::Amount::ONE_SAT * 100000150;
+    bitcoin_rpc
+        .send_to_address(&address, amount, None, None, None, None, None, None)
+        .unwrap();
+    monero_regtest
+        .generate_blocks(11, reusable_xmr_address())
+        .await
+        .unwrap();
+
+    // run until the alice has the monero funding address
+    let monero_address = retry_until_monero_funding_address(cli_alice_progress_args.clone());
+    send_monero(&monero_wallet, monero_address, 1000000000000).await;
+
+    // generate some bitcoin blocks for confirmations
+    bitcoin_rpc
+        .generate_to_address(5, &reusable_btc_address())
+        .unwrap();
+
+    // run until the AliceState(Finish) is received
+    retry_until_finish_state_transition(
+        cli_alice_progress_args.clone(),
+        "AliceState(Finish)".to_string(),
+    );
+
+    // generate some blocks on bitcoin's side
+    bitcoin_rpc
+        .generate_to_address(1, &reusable_btc_address())
+        .unwrap();
+
+    let (_stdout, _stderr) = run("../swap-cli", cli_bob_progress_args.clone()).unwrap();
+
+    // check that btc was received in the destination address
+    let balance = bitcoin_rpc
+        .get_received_by_address(&funding_btc_address, None)
+        .unwrap();
+    println!("balance: {:?}", balance.as_sat());
+    assert!(balance.as_sat() > 90000000);
+
+    // cache the monero balance before sweeping
+    let before_balance = monero_wallet.get_balance(0, None).await.unwrap();
+
+    // generate some blocks on monero's side
+    monero_regtest
+        .generate_blocks(11, reusable_xmr_address())
+        .await
+        .unwrap();
+
+    // run until the BobState(Finish) is received
+    retry_until_finish_state_transition(
+        cli_bob_progress_args.clone(),
+        "BobState(Finish)".to_string(),
+    );
+
+    monero_regtest
+        .generate_blocks(1, reusable_xmr_address())
+        .await
+        .unwrap();
+
+    monero_wallet.refresh(Some(1)).await.unwrap();
+    let after_balance = monero_wallet.get_balance(0, None).await.unwrap();
+    let delta_balance = after_balance.balance - before_balance.balance;
+    assert!(delta_balance > 999660000000);
+}
+
+fn cleanup_processes(mut farcasterd_maker: process::Child, mut farcasterd_taker: process::Child) {
+    // clean up processes
+    farcasterd_maker
+        .kill()
+        .expect("Couldn't kill farcasterd maker");
+    farcasterd_taker
+        .kill()
+        .expect("Couldn't kill farcasterd taker");
+
+    let _procs: Vec<_> = System::new_all()
+        .get_processes()
+        .iter()
+        .filter(|(_pid, process)| {
+            ["peerd", "swapd", "walletd", "syncerd"].contains(&process.name())
+            // && [farcasterd_maker.id(), farcasterd_taker.id()]
+            // .contains(&(process.parent().unwrap() as u32))
+        })
+        .map(|(pid, _process)| {
+            nix::sys::signal::kill(
+                nix::unistd::Pid::from_raw(*pid as i32),
+                nix::sys::signal::Signal::SIGINT,
+            )
+            .expect("Sending CTRL-C failed")
+        })
+        .collect();
+}
+
+fn reusable_btc_address() -> bitcoin::Address {
+    bitcoin::Address::from_str("bcrt1q3rc4sm3w9fr6a46n08znfjt7eu2yhhel6j8rsa").unwrap()
+}
+
+fn reusable_xmr_address() -> monero::Address {
+    monero::Address::from_str("44CpGC77Kn6exUWYCUwfaUYmDeKn7MyRcNPikgeHBCz8M6LXUC3fGCWNMW7UACHyTL6QxzqKxvJbu5o2VESLzCaeNHNUkwv").unwrap()
+}
+
+fn info_args(data_dir: Vec<String>) -> Vec<String> {
+    data_dir
+        .into_iter()
+        .chain(vec!["info".to_string()])
+        .collect()
+}
+
+fn farcasterd_args(data_dir: Vec<String>, server_args: Vec<String>) -> Vec<String> {
+    data_dir.into_iter().chain(server_args).collect()
+}
+
+fn make_offer_args(
+    data_dir: Vec<String>,
+    role: String,
+    btc_addr: String,
+    xmr_addr: String,
+) -> Vec<String> {
+    data_dir
+        .into_iter()
+        .chain(vec![
+            "make".to_string(),
+            btc_addr,
+            xmr_addr,
+            "Local".to_string(),
+            "ECDSA".to_string(),
+            "Monero".to_string(),
+            "1 BTC".to_string(),
+            "1 XMR".to_string(),
+            role,
+            "10".to_string(),
+            "30".to_string(),
+            "1 satoshi/vByte".to_string(),
+            "127.0.0.1".to_string(),
+            "0.0.0.0".to_string(),
+            "9376".to_string(),
+        ])
+        .collect()
+}
+
+fn take_offer_args(
+    data_dir: Vec<String>,
+    btc_addr: String,
+    xmr_addr: String,
+    offer: String,
+) -> Vec<String> {
+    data_dir
+        .into_iter()
+        .chain(vec![
+            "take".to_string(),
+            btc_addr,
+            xmr_addr,
+            offer,
+            "--without-validation".to_string(),
+        ])
+        .collect()
+}
+
+fn progress_args(data_dir: Vec<String>, swap_id: String) -> Vec<String> {
+    data_dir
+        .into_iter()
+        .chain(vec!["progress".to_string(), swap_id])
+        .collect()
+}
+
+fn retry_until_offer(args: Vec<String>) -> Vec<String> {
+    for _ in 0..ALLOWED_RETRIES {
+        let (stdout, _stderr) = run("../swap-cli", args.clone()).unwrap();
+        let offers: Vec<String> = stdout
+            .iter()
+            .filter_map(|element| {
+                if let Some(pos) = element.find("Offer") {
+                    let len = element.to_string().len() - 1;
+                    let offer = element[pos..len].to_string();
+                    Some(offer)
+                } else {
+                    None
+                }
+            })
+            .collect();
+        if !offers.is_empty() {
+            return offers;
+        }
+        thread::sleep(time::Duration::from_secs(1));
+    }
+    panic!("timeout before any offer could be retrieved");
+}
+
+fn retry_until_swap_ids(args: Vec<String>) -> Vec<String> {
+    for _ in 0..ALLOWED_RETRIES {
+        let (stdout, _stderr) = run("../swap-cli", args.clone()).unwrap();
+        let swap_ids: Vec<String> = stdout
+            .iter()
+            .filter_map(|element| {
+                if let Some(pos) = element.find("\"0x") {
+                    let len = element.to_string().len() - 1;
+                    let swap_id = element[pos + 1..len].to_string();
+                    Some(swap_id)
+                } else {
+                    None
+                }
+            })
+            .collect();
+        if !swap_ids.is_empty() {
+            return swap_ids;
+        }
+        thread::sleep(time::Duration::from_secs(1));
+    }
+    panic!("timeout before any swapid could be retrieved");
+}
+
+fn retry_until_bitcoin_funding_address(args: Vec<String>) -> bitcoin::Address {
+    for _ in 0..ALLOWED_RETRIES {
+        let (stdout, _stderr) = run("../swap-cli", args.clone()).unwrap();
+        // get the btc funding address
+        let bitcoin_address: Vec<String> = stdout
+            .iter()
+            .filter_map(|element| {
+                if let Some(pos) = element.find("bcr") {
+                    let len = element.to_string().len();
+                    let swap_id = element[pos..len].to_string();
+                    Some(swap_id)
+                } else {
+                    None
+                }
+            })
+            .collect();
+
+        if !bitcoin_address.is_empty() {
+            return bitcoin::Address::from_str(&bitcoin_address[0]).unwrap();
+        }
+        thread::sleep(time::Duration::from_secs(1));
+    }
+    panic!("timeout before any bitcoin funding address could be retrieved");
+}
+
+fn retry_until_monero_funding_address(args: Vec<String>) -> monero::Address {
+    for _ in 0..ALLOWED_RETRIES {
+        let (stdout, _stderr) = run("../swap-cli", args.clone()).unwrap();
+
+        // get the monero funding address
+        let monero_addresses: Vec<String> = stdout
+            .iter()
+            .filter_map(|element| {
+                if element.contains("Success: 4") {
+                    let monero_address = element[9..].to_string();
+                    Some(monero_address)
+                } else {
+                    None
+                }
+            })
+            .collect();
+
+        if !monero_addresses.is_empty() {
+            return monero::Address::from_str(&monero_addresses[0]).unwrap();
+        }
+        thread::sleep(time::Duration::from_secs(1));
+    }
+    panic!("timeout before any monero funding address could be retrieved");
+}
+
+fn retry_until_finish_state_transition(args: Vec<String>, finish_state: String) -> Vec<String> {
+    for _ in 0..ALLOWED_RETRIES {
+        let (stdout, _stderr) = run("../swap-cli", args.clone()).unwrap();
+
+        let alice_finish: Vec<String> = stdout
+            .iter()
+            .filter_map(|element| {
+                if element.contains(&finish_state) {
+                    Some(element.to_string())
+                } else {
+                    None
+                }
+            })
+            .collect();
+
+        if !alice_finish.is_empty() {
+            return alice_finish;
+        }
+
+        thread::sleep(time::Duration::from_secs(1));
+    }
+    panic!(
+        "timeout before finish state {:?} could be retrieved",
+        finish_state
+    );
+}
+
+fn bitcoin_setup() -> bitcoincore_rpc::Client {
+    let path = std::path::PathBuf::from_str("tests/data_dir/regtest/.cookie").unwrap();
+    let bitcoin_rpc = Client::new("http://localhost:18443", Auth::CookieFile(path)).unwrap();
+
+    // make sure a wallet is created and loaded
+    if bitcoin_rpc
+        .create_wallet("wallet", None, None, None, None)
+        .is_err()
+    {
+        let _ = bitcoin_rpc.load_wallet("wallet");
+    }
+
+    let address = bitcoin_rpc.get_new_address(None, None).unwrap();
+    bitcoin_rpc.generate_to_address(200, &address).unwrap();
+    bitcoin_rpc
+}
+
+async fn monero_setup() -> (monero_rpc::RegtestDaemonClient, monero_rpc::WalletClient) {
+    let daemon_client = monero_rpc::RpcClient::new("http://localhost:18081".to_string());
+    let daemon = daemon_client.daemon();
+    let regtest = daemon.regtest();
+    let wallet_client = monero_rpc::RpcClient::new("http://localhost:18083".to_string());
+    let wallet = wallet_client.wallet();
+    if let Err(_e) = wallet.open_wallet("test".to_string(), None).await {
+        wallet
+            .create_wallet("test".to_string(), None, "English".to_string())
+            .await
+            .unwrap();
+        wallet.open_wallet("test".to_string(), None).await.unwrap();
+    }
+
+    let address = wallet.get_address(0, None).await.unwrap();
+    regtest.generate_blocks(200, address.address).await.unwrap();
+    regtest
+        .generate_blocks(200, reusable_xmr_address())
+        .await
+        .unwrap();
+
+    (regtest, wallet)
+}
+
+pub fn run(
+    name: &str,
+    args: impl IntoIterator<Item = impl AsRef<OsStr>>,
+) -> io::Result<(Vec<String>, Vec<String>)> {
+    let mut bin_path = std::env::current_exe().map_err(|err| {
+        error!("Unable to detect binary directory: {}", err);
+        err
+    })?;
+    bin_path.pop();
+
+    bin_path.push(name);
+    #[cfg(target_os = "windows")]
+    bin_path.set_extension("exe");
+
+    debug!(
+        "Launching {} as a separate process using `{}` as binary",
+        name,
+        bin_path.to_string_lossy()
+    );
+
+    let res = process::Command::new(bin_path)
+        .args(args)
+        .output()
+        .expect("failed to run command");
+
+    let stdout = String::from_utf8_lossy(&res.stdout)
+        .to_string()
+        .lines()
+        .map(|line| {
+            let plain_bytes = strip_ansi_escapes::strip(&line).unwrap();
+            str::from_utf8(&plain_bytes).unwrap().to_string()
+        })
+        .collect();
+    let stderr = String::from_utf8_lossy(&res.stderr)
+        .to_string()
+        .lines()
+        .map(|line| {
+            let plain_bytes = strip_ansi_escapes::strip(&line).unwrap();
+            str::from_utf8(&plain_bytes).unwrap().to_string()
+        })
+        .collect();
+    Ok((stdout, stderr))
+}
+
+pub fn launch(
+    name: &str,
+    args: impl IntoIterator<Item = impl AsRef<OsStr>>,
+) -> io::Result<process::Child> {
+    let mut bin_path = std::env::current_exe().map_err(|err| {
+        error!("Unable to detect binary directory: {}", err);
+        err
+    })?;
+    bin_path.pop();
+
+    bin_path.push(name);
+    #[cfg(target_os = "windows")]
+    bin_path.set_extension("exe");
+
+    debug!(
+        "Launching {} as a separate process using `{}` as binary",
+        name,
+        bin_path.to_string_lossy()
+    );
+
+    let mut cmd = process::Command::new(bin_path);
+
+    cmd.args(args);
+
+    trace!("Executing `{:?}`", cmd);
+    cmd.spawn().map_err(|err| {
+        error!("Error launching {}: {}", name, err);
+        err
+    })
+}
+
+async fn send_monero(
+    wallet: &monero_rpc::WalletClient,
+    address: monero::Address,
+    amount: u64,
+) -> Vec<u8> {
+    let mut destination: HashMap<monero::Address, u64> = HashMap::new();
+    destination.insert(address, amount);
+    let options = monero_rpc::TransferOptions {
+        account_index: None,
+        subaddr_indices: None,
+        mixin: None,
+        ring_size: None,
+        unlock_time: None,
+        payment_id: None,
+        do_not_relay: None,
+    };
+    let transaction = wallet
+        .transfer(
+            destination.clone(),
+            monero_rpc::TransferPriority::Default,
+            options.clone(),
+        )
+        .await
+        .unwrap();
+    hex::decode(transaction.tx_hash.to_string()).unwrap()
+}