Merge branch 'master' into document-wallet-export

pull/1280/head
Byron Hambly 3 months ago committed by GitHub
commit ffe4e48785
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -45,16 +45,16 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout tagged commit - name: Checkout tagged commit
uses: actions/checkout@v3.3.0 uses: actions/checkout@v4.1.1
with: with:
ref: ${{ github.event.release.target_commitish }} ref: ${{ github.event.release.target_commitish }}
token: ${{ secrets.BOTTY_GITHUB_TOKEN }} token: ${{ secrets.BOTTY_GITHUB_TOKEN }}
- uses: Swatinem/rust-cache@v2.2.0 - uses: Swatinem/rust-cache@v2.7.3
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: 1.63 toolchain: "1.70"
targets: armv7-unknown-linux-gnueabihf targets: armv7-unknown-linux-gnueabihf
- name: Build ${{ matrix.target }} ${{ matrix.bin }} release binary - name: Build ${{ matrix.target }} ${{ matrix.bin }} release binary
@ -69,7 +69,7 @@ jobs:
run: target/${{ matrix.target }}/release/${{ matrix.bin }} --help run: target/${{ matrix.target }}/release/${{ matrix.bin }} --help
# Remove once python 3 is the default # Remove once python 3 is the default
- uses: actions/setup-python@v4 - uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"

@ -4,8 +4,6 @@ on:
pull_request: # Need to run on pull-requests, otherwise PRs from forks don't run pull_request: # Need to run on pull-requests, otherwise PRs from forks don't run
push: push:
branches: branches:
- "staging" # Bors uses this branch
- "trying" # Bors uses this branch
- "master" # Always build head of master for the badge in the README - "master" # Always build head of master for the badge in the README
jobs: jobs:
@ -13,12 +11,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v3.3.0 uses: actions/checkout@v4.1.1
- uses: Swatinem/rust-cache@v2.2.0 - uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.70"
components: clippy,rustfmt
- uses: Swatinem/rust-cache@v2.7.3
- name: Check formatting - name: Check formatting
uses: dprint/check@v2.1 uses: dprint/check@v2.2
with:
dprint-version: 0.39.1
- name: Run clippy with default features - name: Run clippy with default features
run: cargo clippy --workspace --all-targets -- -D warnings run: cargo clippy --workspace --all-targets -- -D warnings
@ -30,9 +35,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v3.3.0 uses: actions/checkout@v4.1.1
- uses: Swatinem/rust-cache@v2.0.2 - uses: Swatinem/rust-cache@v2.7.3
- name: Build swap - name: Build swap
run: cargo build --bin swap run: cargo build --bin swap
@ -44,12 +49,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v3.3.0 uses: actions/checkout@v4.1.1
- uses: Swatinem/rust-cache@v2.0.2 - uses: Swatinem/rust-cache@v2.7.3
- name: Install sqlx-cli - name: Install sqlx-cli
run: cargo install sqlx-cli run: cargo install sqlx-cli --locked
- name: Run sqlite_dev_setup.sh script - name: Run sqlite_dev_setup.sh script
run: | run: |
@ -71,13 +76,13 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v3.3.0 uses: actions/checkout@v4.1.1
- uses: Swatinem/rust-cache@v2.2.0 - uses: Swatinem/rust-cache@v2.7.3
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: 1.63 toolchain: "1.70"
targets: armv7-unknown-linux-gnueabihf targets: armv7-unknown-linux-gnueabihf
- name: Build binary - name: Build binary
@ -93,13 +98,13 @@ jobs:
run: cross build -p swap --target ${{ matrix.target }} run: cross build -p swap --target ${{ matrix.target }}
- name: Upload swap binary - name: Upload swap binary
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: swap-${{ matrix.target }} name: swap-${{ matrix.target }}
path: target/${{ matrix.target }}/debug/swap path: target/${{ matrix.target }}/debug/swap
- name: Upload asb binary - name: Upload asb binary
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: asb-${{ matrix.target }} name: asb-${{ matrix.target }}
path: target/${{ matrix.target }}/debug/asb path: target/${{ matrix.target }}/debug/asb
@ -110,10 +115,23 @@ jobs:
os: [ubuntu-latest, macos-latest] os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: (Free disk space on Ubuntu)
if: matrix.os == 'ubuntu-latest'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# removing all of these takes ~10 mins, so just do as needed
android: true
dotnet: true
haskell: true
docker-images: false
large-packages: false
swap-storage: false
tool-cache: false
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v3.3.0 uses: actions/checkout@v4.1.1
- uses: Swatinem/rust-cache@v2.2.0 - uses: Swatinem/rust-cache@v2.7.3
- name: Build tests - name: Build tests
run: cargo build --tests --workspace --all-features run: cargo build --tests --workspace --all-features
@ -148,9 +166,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v3.3.0 uses: actions/checkout@v4.1.1
- uses: Swatinem/rust-cache@v2.2.0 - uses: Swatinem/rust-cache@v2.7.3
- name: Run test ${{ matrix.test_name }} - name: Run test ${{ matrix.test_name }}
run: cargo test --package swap --all-features --test ${{ matrix.test_name }} -- --nocapture run: cargo test --package swap --all-features --test ${{ matrix.test_name }} -- --nocapture

@ -11,7 +11,7 @@ jobs:
if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3.3.0 - uses: actions/checkout@v4.1.1
- name: Extract version from branch name - name: Extract version from branch name
id: extract-version id: extract-version

@ -12,7 +12,7 @@ jobs:
name: "Draft a new release" name: "Draft a new release"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3.3.0 - uses: actions/checkout@v4.1.1
with: with:
token: ${{ secrets.BOTTY_GITHUB_TOKEN }} token: ${{ secrets.BOTTY_GITHUB_TOKEN }}
@ -20,7 +20,7 @@ jobs:
run: git checkout -b release/${{ github.event.inputs.version }} run: git checkout -b release/${{ github.event.inputs.version }}
- name: Update changelog - name: Update changelog
uses: thomaseizinger/keep-a-changelog-new-release@1.3.0 uses: thomaseizinger/keep-a-changelog-new-release@2.0.0
with: with:
version: ${{ github.event.inputs.version }} version: ${{ github.event.inputs.version }}
changelogPath: CHANGELOG.md changelogPath: CHANGELOG.md
@ -41,8 +41,12 @@ jobs:
- name: Commit changelog and manifest files - name: Commit changelog and manifest files
id: make-commit id: make-commit
env:
DPRINT_VERSION: 0.39.1
RUST_TOOLCHAIN: 1.70
run: | run: |
curl -fsSL https://dprint.dev/install.sh | sh rustup component add rustfmt --toolchain "$RUST_TOOLCHAIN-x86_64-unknown-linux-gnu"
curl -fsSL https://dprint.dev/install.sh | sh -s $DPRINT_VERSION
/home/runner/.dprint/bin/dprint fmt /home/runner/.dprint/bin/dprint fmt
git add CHANGELOG.md Cargo.lock swap/Cargo.toml git add CHANGELOG.md Cargo.lock swap/Cargo.toml
@ -54,7 +58,7 @@ jobs:
run: git push origin release/${{ github.event.inputs.version }} --force run: git push origin release/${{ github.event.inputs.version }} --force
- name: Create pull request - name: Create pull request
uses: thomaseizinger/create-pull-request@1.3.0 uses: thomaseizinger/create-pull-request@1.3.1
with: with:
GITHUB_TOKEN: ${{ secrets.BOTTY_GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.BOTTY_GITHUB_TOKEN }}
head: release/${{ github.event.inputs.version }} head: release/${{ github.event.inputs.version }}

@ -10,7 +10,7 @@ jobs:
name: Create preview release name: Create preview release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3.3.0 - uses: actions/checkout@v4.1.1
- name: Delete 'preview' release - name: Delete 'preview' release
uses: larryjoelane/delete-release-action@v1.0.24 uses: larryjoelane/delete-release-action@v1.0.24

@ -7,9 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
- Minimum Supported Rust Version (MSRV) bumped to 1.70
## [0.12.3] - 2023-09-20
- Swap: If no Monero daemon is manually specified, we will automatically choose one from a list of public daemons by connecting to each and checking their availability.
## [0.12.2] - 2023-08-08
### Changed ### Changed
- Minimum Supported Rust Version (MSRV) bumped to 1.63 - Minimum Supported Rust Version (MSRV) bumped to 1.67
- ASB can now register with multiple rendezvous nodes. The `rendezvous_point` option in `config.toml` can be a string with comma separated addresses, or a toml array of address strings.
## [0.12.1] - 2023-01-09 ## [0.12.1] - 2023-01-09
@ -342,7 +351,9 @@ It is possible to migrate critical data from the old db to the sqlite but there
- Fixed an issue where Alice would not verify if Bob's Bitcoin lock transaction is semantically correct, i.e. pays the agreed upon amount to an output owned by both of them. - Fixed an issue where Alice would not verify if Bob's Bitcoin lock transaction is semantically correct, i.e. pays the agreed upon amount to an output owned by both of them.
Fixing this required a **breaking change** on the network layer and hence old versions are not compatible with this version. Fixing this required a **breaking change** on the network layer and hence old versions are not compatible with this version.
[unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.1...HEAD [Unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.3...HEAD
[0.12.3]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.2...0.12.3
[0.12.2]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.1...0.12.2
[0.12.1]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.0...0.12.1 [0.12.1]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.0...0.12.1
[0.12.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.11.0...0.12.0 [0.12.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.11.0...0.12.0
[0.11.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.10.2...0.11.0 [0.11.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.10.2...0.11.0

1537
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -44,13 +44,13 @@ It is not recommended to bump fees when swapping because it can have unpredictab
## Contributing ## Contributing
We are encourage community contributions whether it be a bug fix or an improvement to the documentation. We encourage community contributions whether it be a bug fix or an improvement to the documentation.
Please have a look at the [contribution guidelines](./CONTRIBUTING.md). Please have a look at the [contribution guidelines](./CONTRIBUTING.md).
## Rust Version Support ## Rust Version Support
Please note that only the latest stable Rust toolchain is supported. Please note that only the latest stable Rust toolchain is supported.
All stable toolchains since 1.63 _should_ work. All stable toolchains since 1.70 _should_ work.
## Contact ## Contact

@ -1,25 +0,0 @@
status = [
"static_analysis",
"bdk_test",
"sqlx_test",
"build (x86_64-unknown-linux-gnu, ubuntu-latest)",
"build (armv7-unknown-linux-gnueabihf, ubuntu-latest)",
"build (x86_64-apple-darwin, macos-latest)",
"build (x86_64-pc-windows-msvc, windows-latest)",
"test (ubuntu-latest)",
"test (macos-latest)",
"docker_tests (happy_path)",
"docker_tests (happy_path_restart_bob_after_xmr_locked)",
"docker_tests (happy_path_restart_alice_after_xmr_locked)",
"docker_tests (happy_path_restart_bob_before_xmr_locked)",
"docker_tests (alice_and_bob_refund_using_cancel_and_refund_command)",
"docker_tests (alice_and_bob_refund_using_cancel_then_refund_command)",
"docker_tests (alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired)",
"docker_tests (punish)",
"docker_tests (alice_punishes_after_restart_bob_dead)",
"docker_tests (alice_manually_punishes_after_bob_dead)",
"docker_tests (alice_refunds_after_restart_bob_refunded)",
"docker_tests (ensure_same_swap_id)",
"docker_tests (concurrent_bobs_before_xmr_lock_proof_sent)",
"docker_tests (alice_manually_redeems_after_enc_sig_learned)"
]

@ -42,13 +42,16 @@ Since the ASB is a long running task we specify the person running an ASB as ser
The ASB daemon supports the libp2p [rendezvous-protocol](https://github.com/libp2p/specs/tree/master/rendezvous). The ASB daemon supports the libp2p [rendezvous-protocol](https://github.com/libp2p/specs/tree/master/rendezvous).
Usage of the rendezvous functionality is entirely optional. Usage of the rendezvous functionality is entirely optional.
You can configure a rendezvous point in the `[network]` section of your config file. You can configure one or more rendezvous points in the `[network]` section of your config file.
For the registration to be successful, you also need to configure the externally reachable addresses within the `[network]` section. For the registration to be successful, you also need to configure the externally reachable addresses within the `[network]` section.
For example: For example:
```toml ```toml
[network] [network]
rendezvous_point = "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE" rendezvous_point = [
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
"/dns4/eratosthen.es/tcp/7798/p2p/12D3KooWAh7EXXa2ZyegzLGdjvj1W4G3EXrTGrf6trraoT1MEobs",
]
external_addresses = ["/dns4/example.com/tcp/9939"] external_addresses = ["/dns4/example.com/tcp/9939"]
``` ```

@ -3,22 +3,16 @@
"projectType": "openSource", "projectType": "openSource",
"incremental": true, "incremental": true,
"markdown": {}, "markdown": {},
"rustfmt": { "exec": {
"edition": 2021, "associations": "**/*.{rs}",
"condense_wildcard_suffixes": true, "rustfmt": "rustfmt --edition 2021",
"format_macro_matchers": true, "rustfmt.associations": "**/*.rs"
"imports_granularity": "Module",
"use_field_init_shorthand": true,
"format_code_in_doc_comments": true,
"normalize_comments": true,
"wrap_comments": true,
"overflow_delimited_expr": true
}, },
"includes": ["**/*.{md}", "**/*.{toml}", "**/*.{rs}"], "includes": ["**/*.{md}", "**/*.{toml}", "**/*.{rs}"],
"excludes": ["target/"], "excludes": ["target/"],
"plugins": [ "plugins": [
"https://plugins.dprint.dev/markdown-0.13.1.wasm", "https://plugins.dprint.dev/markdown-0.13.1.wasm",
"https://github.com/thomaseizinger/dprint-plugin-cargo-toml/releases/download/0.1.0/cargo-toml-0.1.0.wasm", "https://github.com/thomaseizinger/dprint-plugin-cargo-toml/releases/download/0.1.0/cargo-toml-0.1.0.wasm",
"https://plugins.dprint.dev/rustfmt-0.6.1.exe-plugin@99b89a0599fd3a63e597e03436862157901f3facae2f0c2fbd0b9f656cdbc2a5" "https://plugins.dprint.dev/exec-0.3.5.json@d687dda57be0fe9a0088ccdaefa5147649ff24127d8b3ea227536c68ee7abeab"
] ]
} }

@ -11,7 +11,7 @@ futures = "0.3"
monero-rpc = { path = "../monero-rpc" } monero-rpc = { path = "../monero-rpc" }
rand = "0.7" rand = "0.7"
spectral = "0.6" spectral = "0.6"
testcontainers = "0.12" testcontainers = "0.14"
tokio = { version = "1", default-features = false, features = [ "rt-multi-thread", "time", "macros" ] } tokio = { version = "1", default-features = false, features = [ "rt-multi-thread", "time", "macros" ] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.2", default-features = false, features = [ "fmt", "ansi", "env-filter", "tracing-log" ] } tracing-subscriber = { version = "0.2", default-features = false, features = [ "fmt", "ansi", "env-filter", "tracing-log" ] }

@ -1,6 +1,4 @@
use std::collections::HashMap; use testcontainers::{core::WaitFor, Image, ImageArgs};
use testcontainers::core::{Container, Docker, WaitForMessage};
use testcontainers::Image;
pub const MONEROD_DAEMON_CONTAINER_NAME: &str = "monerod"; pub const MONEROD_DAEMON_CONTAINER_NAME: &str = "monerod";
pub const MONEROD_DEFAULT_NETWORK: &str = "monero_network"; pub const MONEROD_DEFAULT_NETWORK: &str = "monero_network";
@ -13,43 +11,22 @@ pub const MONEROD_DEFAULT_NETWORK: &str = "monero_network";
/// this doesn't matter. /// this doesn't matter.
pub const RPC_PORT: u16 = 18081; pub const RPC_PORT: u16 = 18081;
#[derive(Debug, Default)] #[derive(Clone, Copy, Debug, Default)]
pub struct Monerod { pub struct Monerod;
args: MonerodArgs,
}
impl Image for Monerod { impl Image for Monerod {
type Args = MonerodArgs; type Args = MonerodArgs;
type EnvVars = HashMap<String, String>;
type Volumes = HashMap<String, String>;
type EntryPoint = str;
fn descriptor(&self) -> String {
"rinocommunity/monero:v0.18.1.2".to_owned()
}
fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
container
.logs()
.stdout
.wait_for_message("RPC server started ok")
.unwrap();
}
fn args(&self) -> <Self as Image>::Args {
self.args.clone()
}
fn volumes(&self) -> Self::Volumes { fn name(&self) -> String {
HashMap::new() "rinocommunity/monero".into()
} }
fn env_vars(&self) -> Self::EnvVars { fn tag(&self) -> String {
HashMap::new() "v0.18.1.2".into()
} }
fn with_args(self, args: <Self as Image>::Args) -> Self { fn ready_conditions(&self) -> Vec<WaitFor> {
Self { args } vec![WaitFor::message_on_stdout("RPC server started ok")]
} }
fn entrypoint(&self) -> Option<String> { fn entrypoint(&self) -> Option<String> {
@ -58,43 +35,22 @@ impl Image for Monerod {
} }
} }
#[derive(Debug, Default)] #[derive(Clone, Copy, Debug)]
pub struct MoneroWalletRpc { pub struct MoneroWalletRpc;
args: MoneroWalletRpcArgs,
}
impl Image for MoneroWalletRpc { impl Image for MoneroWalletRpc {
type Args = MoneroWalletRpcArgs; type Args = MoneroWalletRpcArgs;
type EnvVars = HashMap<String, String>;
type Volumes = HashMap<String, String>;
type EntryPoint = str;
fn descriptor(&self) -> String {
"rinocommunity/monero:v0.18.1.2".to_owned()
}
fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
container
.logs()
.stdout
.wait_for_message("Run server thread name: RPC")
.unwrap();
}
fn args(&self) -> <Self as Image>::Args {
self.args.clone()
}
fn volumes(&self) -> Self::Volumes { fn name(&self) -> String {
HashMap::new() "rinocommunity/monero".into()
} }
fn env_vars(&self) -> Self::EnvVars { fn tag(&self) -> String {
HashMap::new() "v0.18.1.2".into()
} }
fn with_args(self, args: <Self as Image>::Args) -> Self { fn ready_conditions(&self) -> Vec<WaitFor> {
Self { args } vec![WaitFor::message_on_stdout("Run server thread name: RPC")]
} }
fn entrypoint(&self) -> Option<String> { fn entrypoint(&self) -> Option<String> {
@ -104,10 +60,9 @@ impl Image for MoneroWalletRpc {
} }
impl MoneroWalletRpc { impl MoneroWalletRpc {
pub fn new(name: &str, daemon_address: String) -> Self { pub fn new(name: &str, daemon_address: String) -> (Self, MoneroWalletRpcArgs) {
Self { let args = MoneroWalletRpcArgs::new(name, daemon_address);
args: MoneroWalletRpcArgs::new(name, daemon_address), (Self, args)
}
} }
} }
@ -191,6 +146,12 @@ impl IntoIterator for MonerodArgs {
} }
} }
impl ImageArgs for MonerodArgs {
fn into_iterator(self) -> Box<dyn Iterator<Item = String>> {
Box::new(self.into_iter())
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MoneroWalletRpcArgs { pub struct MoneroWalletRpcArgs {
pub disable_rpc_login: bool, pub disable_rpc_login: bool,
@ -200,12 +161,6 @@ pub struct MoneroWalletRpcArgs {
pub daemon_address: String, pub daemon_address: String,
} }
impl Default for MoneroWalletRpcArgs {
fn default() -> Self {
unimplemented!("A default instance for `MoneroWalletRpc` doesn't make sense because we always need to connect to a node.")
}
}
impl MoneroWalletRpcArgs { impl MoneroWalletRpcArgs {
pub fn new(wallet_name: &str, daemon_address: String) -> Self { pub fn new(wallet_name: &str, daemon_address: String) -> Self {
Self { Self {
@ -247,3 +202,9 @@ impl IntoIterator for MoneroWalletRpcArgs {
args.into_iter() args.into_iter()
} }
} }
impl ImageArgs for MoneroWalletRpcArgs {
fn into_iterator(self) -> Box<dyn Iterator<Item = String>> {
Box::new(self.into_iter())
}
}

@ -20,17 +20,20 @@
//! every BLOCK_TIME_SECS seconds. //! every BLOCK_TIME_SECS seconds.
//! //!
//! Also provides standalone JSON RPC clients for monerod and monero-wallet-rpc. //! Also provides standalone JSON RPC clients for monerod and monero-wallet-rpc.
pub mod image; use std::time::Duration;
use crate::image::{MONEROD_DAEMON_CONTAINER_NAME, MONEROD_DEFAULT_NETWORK, RPC_PORT};
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use testcontainers::clients::Cli;
use testcontainers::{Container, RunnableImage};
use tokio::time;
use monero_rpc::monerod; use monero_rpc::monerod;
use monero_rpc::monerod::MonerodRpc as _; use monero_rpc::monerod::MonerodRpc as _;
use monero_rpc::wallet::{self, GetAddress, MoneroWalletRpc as _, Refreshed, Transfer}; use monero_rpc::wallet::{self, GetAddress, MoneroWalletRpc as _, Refreshed, Transfer};
use std::time::Duration;
use testcontainers::clients::Cli; use crate::image::{MONEROD_DAEMON_CONTAINER_NAME, MONEROD_DEFAULT_NETWORK, RPC_PORT};
use testcontainers::{Container, Docker, RunArgs};
use tokio::time; pub mod image;
/// How often we mine a block. /// How often we mine a block.
const BLOCK_TIME_SECS: u64 = 1; const BLOCK_TIME_SECS: u64 = 1;
@ -56,8 +59,8 @@ impl<'c> Monero {
additional_wallets: Vec<&'static str>, additional_wallets: Vec<&'static str>,
) -> Result<( ) -> Result<(
Self, Self,
Container<'c, Cli, image::Monerod>, Container<'c, image::Monerod>,
Vec<Container<'c, Cli, image::MoneroWalletRpc>>, Vec<Container<'c, image::MoneroWalletRpc>>,
)> { )> {
let prefix = format!("{}_", random_prefix()); let prefix = format!("{}_", random_prefix());
let monerod_name = format!("{}{}", prefix, MONEROD_DAEMON_CONTAINER_NAME); let monerod_name = format!("{}{}", prefix, MONEROD_DAEMON_CONTAINER_NAME);
@ -221,15 +224,14 @@ impl<'c> Monerod {
cli: &'c Cli, cli: &'c Cli,
name: String, name: String,
network: String, network: String,
) -> Result<(Self, Container<'c, Cli, image::Monerod>)> { ) -> Result<(Self, Container<'c, image::Monerod>)> {
let image = image::Monerod::default(); let image = image::Monerod::default();
let run_args = RunArgs::default() let image: RunnableImage<image::Monerod> = RunnableImage::from(image)
.with_name(name.clone()) .with_container_name(name.clone())
.with_network(network.clone()); .with_network(network.clone());
let container = cli.run_with_args(image, run_args);
let monerod_rpc_port = container let container = cli.run(image);
.get_host_port(RPC_PORT) let monerod_rpc_port = container.get_host_port_ipv4(RPC_PORT);
.context("port not exposed")?;
Ok(( Ok((
Self { Self {
@ -249,7 +251,7 @@ impl<'c> Monerod {
/// address /// address
pub async fn start_miner(&self, miner_wallet_address: &str) -> Result<()> { pub async fn start_miner(&self, miner_wallet_address: &str) -> Result<()> {
let monerod = self.client().clone(); let monerod = self.client().clone();
let _ = tokio::spawn(mine(monerod, miner_wallet_address.to_string())); tokio::spawn(mine(monerod, miner_wallet_address.to_string()));
Ok(()) Ok(())
} }
} }
@ -262,19 +264,15 @@ impl<'c> MoneroWalletRpc {
name: &str, name: &str,
monerod: &Monerod, monerod: &Monerod,
prefix: String, prefix: String,
) -> Result<(Self, Container<'c, Cli, image::MoneroWalletRpc>)> { ) -> Result<(Self, Container<'c, image::MoneroWalletRpc>)> {
let daemon_address = format!("{}:{}", monerod.name, RPC_PORT); let daemon_address = format!("{}:{}", monerod.name, RPC_PORT);
let image = image::MoneroWalletRpc::new(name, daemon_address); let (image, args) = image::MoneroWalletRpc::new(name, daemon_address);
let image = RunnableImage::from((image, args))
.with_container_name(format!("{}{}", prefix, name))
.with_network(monerod.network.clone());
let network = monerod.network.clone(); let container = cli.run(image);
let run_args = RunArgs::default() let wallet_rpc_port = container.get_host_port_ipv4(RPC_PORT);
// prefix the container name so we can run multiple tests
.with_name(format!("{}{}", prefix, name))
.with_network(network.clone());
let container = cli.run_with_args(image, run_args);
let wallet_rpc_port = container
.get_host_port(RPC_PORT)
.context("port not exposed")?;
let client = wallet::Client::localhost(wallet_rpc_port)?; let client = wallet::Client::localhost(wallet_rpc_port)?;

@ -19,5 +19,5 @@ serde_json = "1.0"
tracing = "0.1" tracing = "0.1"
[dev-dependencies] [dev-dependencies]
hex-literal = "0.3" hex-literal = "0.4"
tokio = { version = "1", features = [ "full" ] } tokio = { version = "1", features = [ "full" ] }

@ -47,9 +47,10 @@ impl Client {
} }
pub async fn get_o_indexes(&self, txid: Hash) -> Result<GetOIndexesResponse> { pub async fn get_o_indexes(&self, txid: Hash) -> Result<GetOIndexesResponse> {
self.binary_request(self.get_o_indexes_bin_url.clone(), GetOIndexesPayload { self.binary_request(
txid, self.get_o_indexes_bin_url.clone(),
}) GetOIndexesPayload { txid },
)
.await .await
} }
@ -194,7 +195,7 @@ mod monero_serde_hex_block {
{ {
let hex = String::deserialize(deserializer)?; let hex = String::deserialize(deserializer)?;
let bytes = hex::decode(&hex).map_err(D::Error::custom)?; let bytes = hex::decode(hex).map_err(D::Error::custom)?;
let mut cursor = Cursor::new(bytes); let mut cursor = Cursor::new(bytes);
let block = monero::Block::consensus_decode(&mut cursor).map_err(D::Error::custom)?; let block = monero::Block::consensus_decode(&mut cursor).map_err(D::Error::custom)?;

@ -14,6 +14,6 @@ rand = "0.7"
curve25519-dalek = "3" curve25519-dalek = "3"
monero-harness = { path = "../monero-harness" } monero-harness = { path = "../monero-harness" }
rand = "0.7" rand = "0.7"
testcontainers = "0.12" testcontainers = "0.14"
tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs" ] } tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs" ] }
tracing-subscriber = { version = "0.2", default-features = false, features = [ "fmt", "ansi", "env-filter", "chrono", "tracing-log" ] } tracing-subscriber = { version = "0.2", default-features = false, features = [ "fmt", "ansi", "env-filter", "chrono", "tracing-log" ] }

@ -61,13 +61,12 @@ mod tests {
use monero_harness::image::Monerod; use monero_harness::image::Monerod;
use monero_rpc::monerod::{Client, GetOutputsOut}; use monero_rpc::monerod::{Client, GetOutputsOut};
use testcontainers::clients::Cli; use testcontainers::clients::Cli;
use testcontainers::Docker;
#[tokio::test] #[tokio::test]
async fn get_outs_for_key_offsets() { async fn get_outs_for_key_offsets() {
let cli = Cli::default(); let cli = Cli::default();
let container = cli.run(Monerod::default()); let container = cli.run(Monerod::default());
let rpc_client = Client::localhost(container.get_host_port(18081).unwrap()).unwrap(); let rpc_client = Client::localhost(container.get_host_port_ipv4(18081)).unwrap();
rpc_client.generateblocks(150, "498AVruCDWgP9Az9LjMm89VWjrBrSZ2W2K3HFBiyzzrRjUJWUcCVxvY1iitfuKoek2FdX6MKGAD9Qb1G1P8QgR5jPmmt3Vj".to_owned()).await.unwrap(); rpc_client.generateblocks(150, "498AVruCDWgP9Az9LjMm89VWjrBrSZ2W2K3HFBiyzzrRjUJWUcCVxvY1iitfuKoek2FdX6MKGAD9Qb1G1P8QgR5jPmmt3Vj".to_owned()).await.unwrap();
let wallet = Wallet { let wallet = Wallet {
client: rpc_client.clone(), client: rpc_client.clone(),

@ -1,4 +1,4 @@
[toolchain] [toolchain]
channel = "1.63" # also update this in the readme, changelog, and github actions channel = "1.70" # also update this in the readme, changelog, and github actions
components = ["clippy"] components = ["clippy"]
targets = ["armv7-unknown-linux-gnueabihf"] targets = ["armv7-unknown-linux-gnueabihf"]

@ -1,6 +1,6 @@
[package] [package]
name = "swap" name = "swap"
version = "0.12.1" version = "0.12.3"
authors = [ "The COMIT guys <hello@comit.network>" ] authors = [ "The COMIT guys <hello@comit.network>" ]
edition = "2021" edition = "2021"
description = "XMR/BTC trustless atomic swaps." description = "XMR/BTC trustless atomic swaps."
@ -15,28 +15,28 @@ async-trait = "0.1"
atty = "0.2" atty = "0.2"
backoff = { version = "0.4", features = [ "tokio" ] } backoff = { version = "0.4", features = [ "tokio" ] }
base64 = "0.21" base64 = "0.21"
bdk = "0.26" bdk = "0.28"
big-bytes = "1" big-bytes = "1"
bitcoin = { version = "0.29", features = [ "rand", "serde" ] } bitcoin = { version = "0.29", features = [ "rand", "serde" ] }
bmrng = "0.5" bmrng = "0.5"
comfy-table = "6.1" comfy-table = "7.1"
config = { version = "0.13", default-features = false, features = [ "toml" ] } config = { version = "0.14", default-features = false, features = [ "toml" ] }
conquer-once = "0.3" conquer-once = "0.4"
curve25519-dalek = { package = "curve25519-dalek-ng", version = "4" } curve25519-dalek = { package = "curve25519-dalek-ng", version = "4" }
data-encoding = "2.3" data-encoding = "2.5"
dialoguer = "0.10" dialoguer = "0.11"
directories-next = "2" directories-next = "2"
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = false, features = [ "libsecp_compat", "serde", "adaptor" ] } ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = false, features = [ "libsecp_compat", "serde", "adaptor" ] }
ed25519-dalek = "1" ed25519-dalek = "1"
futures = { version = "0.3", default-features = false } futures = { version = "0.3", default-features = false }
hex = "0.4" hex = "0.4"
itertools = "0.10" itertools = "0.12"
libp2p = { version = "0.42.2", default-features = false, features = [ "tcp-tokio", "yamux", "mplex", "dns-tokio", "noise", "request-response", "websocket", "ping", "rendezvous", "identify" ] } libp2p = { version = "0.42.2", default-features = false, features = [ "tcp-tokio", "yamux", "mplex", "dns-tokio", "noise", "request-response", "websocket", "ping", "rendezvous", "identify" ] }
monero = { version = "0.12", features = [ "serde_support" ] } monero = { version = "0.12", features = [ "serde_support" ] }
monero-rpc = { path = "../monero-rpc" } monero-rpc = { path = "../monero-rpc" }
pem = "1.1" pem = "3.0"
proptest = "1" proptest = "1"
qrcode = "0.12" qrcode = "0.13"
rand = "0.8" rand = "0.8"
rand_chacha = "0.3" rand_chacha = "0.3"
reqwest = { version = "0.11", features = [ "rustls-tls", "stream", "socks" ], default-features = false } reqwest = { version = "0.11", features = [ "rustls-tls", "stream", "socks" ], default-features = false }
@ -50,21 +50,21 @@ sha2 = "0.10"
sigma_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = false, features = [ "ed25519", "serde", "secp256k1", "alloc" ] } sigma_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = false, features = [ "ed25519", "serde", "secp256k1", "alloc" ] }
sqlx = { version = "0.6", features = [ "sqlite", "runtime-tokio-rustls", "offline" ] } sqlx = { version = "0.6", features = [ "sqlite", "runtime-tokio-rustls", "offline" ] }
structopt = "0.3" structopt = "0.3"
strum = { version = "0.24", features = [ "derive" ] } strum = { version = "0.26", features = [ "derive" ] }
thiserror = "1" thiserror = "1"
time = "0.3" time = "0.3"
tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs", "net" ] } tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs", "net" ] }
tokio-socks = "0.5" tokio-socks = "0.5"
tokio-tungstenite = { version = "0.15", features = [ "rustls-tls" ] } tokio-tungstenite = { version = "0.15", features = [ "rustls-tls" ] }
tokio-util = { version = "0.7", features = [ "io", "codec" ] } tokio-util = { version = "0.7", features = [ "io", "codec" ] }
toml = "0.5" toml = "0.8"
torut = { version = "0.2", default-features = false, features = [ "v3", "control" ] } torut = { version = "0.2", default-features = false, features = [ "v3", "control" ] }
tracing = { version = "0.1", features = [ "attributes" ] } tracing = { version = "0.1", features = [ "attributes" ] }
tracing-appender = "0.2" tracing-appender = "0.2"
tracing-futures = { version = "0.2", features = [ "std-future", "futures-03" ] } tracing-futures = { version = "0.2", features = [ "std-future", "futures-03" ] }
tracing-subscriber = { version = "0.3", default-features = false, features = [ "fmt", "ansi", "env-filter", "time", "tracing-log", "json" ] } tracing-subscriber = { version = "0.3", default-features = false, features = [ "fmt", "ansi", "env-filter", "time", "tracing-log", "json" ] }
url = { version = "2", features = [ "serde" ] } url = { version = "2", features = [ "serde" ] }
uuid = { version = "1.2", features = [ "serde", "v4" ] } uuid = { version = "1.7", features = [ "serde", "v4" ] }
void = "1" void = "1"
[target.'cfg(not(windows))'.dependencies] [target.'cfg(not(windows))'.dependencies]
@ -76,16 +76,17 @@ zip = "0.5"
[dev-dependencies] [dev-dependencies]
bitcoin-harness = "0.2.2" bitcoin-harness = "0.2.2"
get-port = "3" get-port = "3"
hyper = "0.14" hyper = "1.2"
mockito = "1.3.0"
monero-harness = { path = "../monero-harness" } monero-harness = { path = "../monero-harness" }
port_check = "0.1" port_check = "0.1"
proptest = "1" proptest = "1"
serde_cbor = "0.11" serde_cbor = "0.11"
serial_test = "0.10" serial_test = "3.0"
spectral = "0.6" spectral = "0.6"
tempfile = "3" tempfile = "3"
testcontainers = "0.12" testcontainers = "0.14"
[build-dependencies] [build-dependencies]
anyhow = "1" anyhow = "1"
vergen = { version = "7.5", default-features = false, features = [ "git", "build" ] } vergen = { version = "8.3", default-features = false, features = [ "build", "git", "git2" ] }

@ -1,9 +1,9 @@
use anyhow::Result; use anyhow::Result;
use vergen::{vergen, Config, SemverKind}; use vergen::EmitBuilder;
fn main() -> Result<()> { fn main() -> Result<()> {
let mut config = Config::default(); EmitBuilder::builder()
*config.git_mut().semver_kind_mut() = SemverKind::Lightweight; .git_describe(true, true, None)
.emit()?;
vergen(config) Ok(())
} }

@ -8,6 +8,7 @@ pub mod tracing;
pub use event_loop::{EventLoop, EventLoopHandle, FixedRate, KrakenRate, LatestRate}; pub use event_loop::{EventLoop, EventLoopHandle, FixedRate, KrakenRate, LatestRate};
pub use network::behaviour::{Behaviour, OutEvent}; pub use network::behaviour::{Behaviour, OutEvent};
pub use network::rendezvous::RendezvousNode;
pub use network::transport; pub use network::transport;
pub use rate::Rate; pub use rate::Rate;
pub use recovery::cancel::cancel; pub use recovery::cancel::cancel;
@ -18,4 +19,4 @@ pub use recovery::safely_abort::safely_abort;
pub use recovery::{cancel, refund}; pub use recovery::{cancel, refund};
#[cfg(test)] #[cfg(test)]
pub use network::rendezous; pub use network::rendezvous;

@ -226,7 +226,7 @@ pub enum Command {
name = "asb", name = "asb",
about = "Automated Swap Backend for swapping XMR for BTC", about = "Automated Swap Backend for swapping XMR for BTC",
author, author,
version = env!("VERGEN_GIT_SEMVER_LIGHTWEIGHT") version = env!("VERGEN_GIT_DESCRIBE")
)] )]
pub struct RawArguments { pub struct RawArguments {
#[structopt(long, help = "Swap on testnet")] #[structopt(long, help = "Swap on testnet")]

@ -134,8 +134,8 @@ pub struct Data {
pub struct Network { pub struct Network {
#[serde(deserialize_with = "addr_list::deserialize")] #[serde(deserialize_with = "addr_list::deserialize")]
pub listen: Vec<Multiaddr>, pub listen: Vec<Multiaddr>,
#[serde(default)] #[serde(default, deserialize_with = "addr_list::deserialize")]
pub rendezvous_point: Option<Multiaddr>, pub rendezvous_point: Vec<Multiaddr>,
#[serde(default, deserialize_with = "addr_list::deserialize")] #[serde(default, deserialize_with = "addr_list::deserialize")]
pub external_addresses: Vec<Multiaddr>, pub external_addresses: Vec<Multiaddr>,
} }
@ -156,7 +156,7 @@ mod addr_list {
let list: Result<Vec<_>, _> = s let list: Result<Vec<_>, _> = s
.split(',') .split(',')
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.map(|s| s.parse().map_err(de::Error::custom)) .map(|s| s.trim().parse().map_err(de::Error::custom))
.collect(); .collect();
Ok(list?) Ok(list?)
} }
@ -165,7 +165,7 @@ mod addr_list {
.iter() .iter()
.map(|v| { .map(|v| {
if let Value::String(s) = v { if let Value::String(s) = v {
s.parse().map_err(de::Error::custom) s.trim().parse().map_err(de::Error::custom)
} else { } else {
Err(de::Error::custom("expected a string")) Err(de::Error::custom("expected a string"))
} }
@ -347,10 +347,27 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
} }
let ask_spread = Decimal::from_f64(ask_spread).context("Unable to parse spread")?; let ask_spread = Decimal::from_f64(ask_spread).context("Unable to parse spread")?;
let rendezvous_point = Input::<Multiaddr>::with_theme(&ColorfulTheme::default()) let mut number = 1;
.with_prompt("Do you want to advertise your ASB instance with a rendezvous node? Enter an empty string if not.") let mut done = false;
.allow_empty(true) let mut rendezvous_points = Vec::new();
.interact_text()?; println!("ASB can register with multiple rendezvous nodes for discoverability. This can also be edited in the config file later.");
while !done {
let prompt = format!(
"Enter the address for rendezvous node ({number}). Or just hit Enter to continue."
);
let rendezvous_addr = Input::<Multiaddr>::with_theme(&ColorfulTheme::default())
.with_prompt(prompt)
.allow_empty(true)
.interact_text()?;
if rendezvous_addr.is_empty() {
done = true;
} else if rendezvous_points.contains(&rendezvous_addr) {
println!("That rendezvous address is already in the list.");
} else {
rendezvous_points.push(rendezvous_addr);
number += 1;
}
}
println!(); println!();
@ -358,11 +375,7 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
data: Data { dir: data_dir }, data: Data { dir: data_dir },
network: Network { network: Network {
listen: listen_addresses, listen: listen_addresses,
rendezvous_point: if rendezvous_point.is_empty() { rendezvous_point: rendezvous_points, // keeping the singular key name for backcompat
None
} else {
Some(rendezvous_point)
},
external_addresses: vec![], external_addresses: vec![],
}, },
bitcoin: Bitcoin { bitcoin: Bitcoin {
@ -417,7 +430,7 @@ mod tests {
}, },
network: Network { network: Network {
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws], listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws],
rendezvous_point: None, rendezvous_point: vec![],
external_addresses: vec![], external_addresses: vec![],
}, },
monero: Monero { monero: Monero {
@ -461,7 +474,7 @@ mod tests {
}, },
network: Network { network: Network {
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws], listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws],
rendezvous_point: None, rendezvous_point: vec![],
external_addresses: vec![], external_addresses: vec![],
}, },
monero: Monero { monero: Monero {
@ -515,7 +528,7 @@ mod tests {
}, },
network: Network { network: Network {
listen, listen,
rendezvous_point: None, rendezvous_point: vec![],
external_addresses, external_addresses,
}, },
monero: Monero { monero: Monero {

@ -253,8 +253,8 @@ where
channel channel
}.boxed()); }.boxed());
} }
SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::Registered { .. })) => { SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::Registered { rendezvous_node, ttl, namespace })) => {
tracing::info!("Successfully registered with rendezvous node"); tracing::info!("Successfully registered with rendezvous node: {} with namespace: {} and TTL: {:?}", rendezvous_node, namespace, ttl);
} }
SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::RegisterFailed(error))) => { SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::RegisterFailed(error))) => {
tracing::error!("Registration with rendezvous node failed: {:?}", error); tracing::error!("Registration with rendezvous node failed: {:?}", error);

@ -44,7 +44,9 @@ pub mod transport {
} }
pub mod behaviour { pub mod behaviour {
use super::*; use libp2p::swarm::behaviour::toggle::Toggle;
use super::{rendezvous::RendezvousNode, *};
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[derive(Debug)] #[derive(Debug)]
@ -108,7 +110,7 @@ pub mod behaviour {
where where
LR: LatestRate + Send + 'static, LR: LatestRate + Send + 'static,
{ {
pub rendezvous: libp2p::swarm::behaviour::toggle::Toggle<rendezous::Behaviour>, pub rendezvous: Toggle<rendezvous::Behaviour>,
pub quote: quote::Behaviour, pub quote: quote::Behaviour,
pub swap_setup: alice::Behaviour<LR>, pub swap_setup: alice::Behaviour<LR>,
pub transfer_proof: transfer_proof::Behaviour, pub transfer_proof: transfer_proof::Behaviour,
@ -132,25 +134,22 @@ pub mod behaviour {
resume_only: bool, resume_only: bool,
env_config: env::Config, env_config: env::Config,
identify_params: (identity::Keypair, XmrBtcNamespace), identify_params: (identity::Keypair, XmrBtcNamespace),
rendezvous_params: Option<(identity::Keypair, PeerId, Multiaddr, XmrBtcNamespace)>, rendezvous_nodes: Vec<RendezvousNode>,
) -> Self { ) -> Self {
let agentVersion = format!("asb/{} ({})", env!("CARGO_PKG_VERSION"), identify_params.1); let (identity, namespace) = identify_params;
let protocolVersion = "/comit/xmr/btc/1.0.0".to_string(); let agent_version = format!("asb/{} ({})", env!("CARGO_PKG_VERSION"), namespace);
let identifyConfig = IdentifyConfig::new(protocolVersion, identify_params.0.public()) let protocol_version = "/comit/xmr/btc/1.0.0".to_string();
.with_agent_version(agentVersion); let identifyConfig = IdentifyConfig::new(protocol_version, identity.public())
.with_agent_version(agent_version);
let behaviour = if rendezvous_nodes.is_empty() {
None
} else {
Some(rendezvous::Behaviour::new(identity, rendezvous_nodes))
};
Self { Self {
rendezvous: libp2p::swarm::behaviour::toggle::Toggle::from(rendezvous_params.map( rendezvous: Toggle::from(behaviour),
|(identity, rendezvous_peer_id, rendezvous_address, namespace)| {
rendezous::Behaviour::new(
identity,
rendezvous_peer_id,
rendezvous_address,
namespace,
None, // use default ttl on rendezvous point
)
},
)),
quote: quote::asb(), quote: quote::asb(),
swap_setup: alice::Behaviour::new( swap_setup: alice::Behaviour::new(
min_buy, min_buy,
@ -186,13 +185,14 @@ pub mod behaviour {
} }
} }
pub mod rendezous { pub mod rendezvous {
use super::*; use super::*;
use libp2p::swarm::dial_opts::DialOpts; use libp2p::swarm::dial_opts::DialOpts;
use libp2p::swarm::DialError; use libp2p::swarm::DialError;
use std::collections::VecDeque;
use std::pin::Pin; use std::pin::Pin;
#[derive(PartialEq)] #[derive(Clone, PartialEq)]
enum ConnectionStatus { enum ConnectionStatus {
Disconnected, Disconnected,
Dialling, Dialling,
@ -209,39 +209,59 @@ pub mod rendezous {
pub struct Behaviour { pub struct Behaviour {
inner: libp2p::rendezvous::client::Behaviour, inner: libp2p::rendezvous::client::Behaviour,
rendezvous_point: Multiaddr, rendezvous_nodes: Vec<RendezvousNode>,
rendezvous_peer_id: PeerId, to_dial: VecDeque<PeerId>,
namespace: XmrBtcNamespace, }
registration_status: RegistrationStatus,
pub struct RendezvousNode {
pub address: Multiaddr,
connection_status: ConnectionStatus, connection_status: ConnectionStatus,
registration_ttl: Option<u64>, pub peer_id: PeerId,
registration_status: RegistrationStatus,
pub registration_ttl: Option<u64>,
pub namespace: XmrBtcNamespace,
} }
impl Behaviour { impl RendezvousNode {
pub fn new( pub fn new(
identity: identity::Keypair, address: &Multiaddr,
rendezvous_peer_id: PeerId, peer_id: PeerId,
rendezvous_address: Multiaddr,
namespace: XmrBtcNamespace, namespace: XmrBtcNamespace,
registration_ttl: Option<u64>, registration_ttl: Option<u64>,
) -> Self { ) -> Self {
Self { Self {
inner: libp2p::rendezvous::client::Behaviour::new(identity), address: address.to_owned(),
rendezvous_point: rendezvous_address, connection_status: ConnectionStatus::Disconnected,
rendezvous_peer_id,
namespace, namespace,
peer_id,
registration_status: RegistrationStatus::RegisterOnNextConnection, registration_status: RegistrationStatus::RegisterOnNextConnection,
connection_status: ConnectionStatus::Disconnected,
registration_ttl, registration_ttl,
} }
} }
fn register(&mut self) { fn set_connection(&mut self, status: ConnectionStatus) {
self.inner.register( self.connection_status = status;
self.namespace.into(), }
self.rendezvous_peer_id,
self.registration_ttl, fn set_registration(&mut self, status: RegistrationStatus) {
); self.registration_status = status;
}
}
impl Behaviour {
pub fn new(identity: identity::Keypair, rendezvous_nodes: Vec<RendezvousNode>) -> Self {
Self {
inner: libp2p::rendezvous::client::Behaviour::new(identity),
rendezvous_nodes,
to_dial: VecDeque::new(),
}
}
/// Calls the rendezvous register method of the node at node_index in the Vec of rendezvous nodes
fn register(&mut self, node_index: usize) {
let node = &self.rendezvous_nodes[node_index];
self.inner
.register(node.namespace.into(), node.peer_id, node.registration_ttl);
} }
} }
@ -255,31 +275,37 @@ pub mod rendezous {
} }
fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec<Multiaddr> { fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec<Multiaddr> {
if peer_id == &self.rendezvous_peer_id { for node in self.rendezvous_nodes.iter() {
return vec![self.rendezvous_point.clone()]; if peer_id == &node.peer_id {
return vec![node.address.clone()];
}
} }
vec![] vec![]
} }
fn inject_connected(&mut self, peer_id: &PeerId) { fn inject_connected(&mut self, peer_id: &PeerId) {
if peer_id == &self.rendezvous_peer_id { for i in 0..self.rendezvous_nodes.len() {
self.connection_status = ConnectionStatus::Connected; if peer_id == &self.rendezvous_nodes[i].peer_id {
self.rendezvous_nodes[i].set_connection(ConnectionStatus::Connected);
match &self.registration_status { match &self.rendezvous_nodes[i].registration_status {
RegistrationStatus::RegisterOnNextConnection => { RegistrationStatus::RegisterOnNextConnection => {
self.register(); self.register(i);
self.registration_status = RegistrationStatus::Pending; self.rendezvous_nodes[i].set_registration(RegistrationStatus::Pending);
}
RegistrationStatus::Registered { .. } => {}
RegistrationStatus::Pending => {}
} }
RegistrationStatus::Registered { .. } => {}
RegistrationStatus::Pending => {}
} }
} }
} }
fn inject_disconnected(&mut self, peer_id: &PeerId) { fn inject_disconnected(&mut self, peer_id: &PeerId) {
if peer_id == &self.rendezvous_peer_id { for i in 0..self.rendezvous_nodes.len() {
self.connection_status = ConnectionStatus::Disconnected; let mut node = &mut self.rendezvous_nodes[i];
if peer_id == &node.peer_id {
node.connection_status = ConnectionStatus::Disconnected;
}
} }
} }
@ -298,9 +324,12 @@ pub mod rendezous {
_handler: Self::ProtocolsHandler, _handler: Self::ProtocolsHandler,
_error: &DialError, _error: &DialError,
) { ) {
if let Some(id) = peer_id { for i in 0..self.rendezvous_nodes.len() {
if id == self.rendezvous_peer_id { let mut node = &mut self.rendezvous_nodes[i];
self.connection_status = ConnectionStatus::Disconnected; if let Some(id) = peer_id {
if id == node.peer_id {
node.connection_status = ConnectionStatus::Disconnected;
}
} }
} }
} }
@ -311,62 +340,73 @@ pub mod rendezous {
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
params: &mut impl PollParameters, params: &mut impl PollParameters,
) -> Poll<NetworkBehaviourAction<Self::OutEvent, Self::ProtocolsHandler>> { ) -> Poll<NetworkBehaviourAction<Self::OutEvent, Self::ProtocolsHandler>> {
match &mut self.registration_status { if let Some(peer_id) = self.to_dial.pop_front() {
RegistrationStatus::RegisterOnNextConnection => match self.connection_status { return Poll::Ready(NetworkBehaviourAction::Dial {
ConnectionStatus::Disconnected => { opts: DialOpts::peer_id(peer_id)
self.connection_status = ConnectionStatus::Dialling; .condition(PeerCondition::Disconnected)
.build(),
return Poll::Ready(NetworkBehaviourAction::Dial {
opts: DialOpts::peer_id(self.rendezvous_peer_id) handler: Self::ProtocolsHandler::new(Duration::from_secs(30)),
.condition(PeerCondition::Disconnected) });
.build(), }
// check the status of each rendezvous node
handler: Self::ProtocolsHandler::new(Duration::from_secs(30)), for i in 0..self.rendezvous_nodes.len() {
}); let connection_status = self.rendezvous_nodes[i].connection_status.clone();
} match &mut self.rendezvous_nodes[i].registration_status {
ConnectionStatus::Dialling => {} RegistrationStatus::RegisterOnNextConnection => match connection_status {
ConnectionStatus::Connected => { ConnectionStatus::Disconnected => {
self.registration_status = RegistrationStatus::Pending; self.rendezvous_nodes[i].set_connection(ConnectionStatus::Dialling);
self.register(); self.to_dial.push_back(self.rendezvous_nodes[i].peer_id);
} }
}, ConnectionStatus::Dialling => {}
RegistrationStatus::Registered { re_register_in } => { ConnectionStatus::Connected => {
if let Poll::Ready(()) = re_register_in.poll_unpin(cx) { self.rendezvous_nodes[i].set_registration(RegistrationStatus::Pending);
match self.connection_status { self.register(i);
ConnectionStatus::Connected => { }
self.registration_status = RegistrationStatus::Pending; },
self.register(); RegistrationStatus::Registered { re_register_in } => {
} if let Poll::Ready(()) = re_register_in.poll_unpin(cx) {
ConnectionStatus::Disconnected => { match connection_status {
self.registration_status = ConnectionStatus::Connected => {
RegistrationStatus::RegisterOnNextConnection; self.rendezvous_nodes[i]
.set_registration(RegistrationStatus::Pending);
return Poll::Ready(NetworkBehaviourAction::Dial { self.register(i);
opts: DialOpts::peer_id(self.rendezvous_peer_id) }
.condition(PeerCondition::Disconnected) ConnectionStatus::Disconnected => {
.build(), self.rendezvous_nodes[i].set_registration(
handler: Self::ProtocolsHandler::new(Duration::from_secs(30)), RegistrationStatus::RegisterOnNextConnection,
}); );
self.to_dial.push_back(self.rendezvous_nodes[i].peer_id);
}
ConnectionStatus::Dialling => {}
} }
ConnectionStatus::Dialling => {}
} }
} }
RegistrationStatus::Pending => {}
} }
RegistrationStatus::Pending => {}
} }
let inner_poll = self.inner.poll(cx, params); let inner_poll = self.inner.poll(cx, params);
// reset the timer if we successfully registered // reset the timer for the specific rendezvous node if we successfully registered
if let Poll::Ready(NetworkBehaviourAction::GenerateEvent( if let Poll::Ready(NetworkBehaviourAction::GenerateEvent(
libp2p::rendezvous::client::Event::Registered { ttl, .. }, libp2p::rendezvous::client::Event::Registered {
ttl,
rendezvous_node,
..
},
)) = &inner_poll )) = &inner_poll
{ {
let half_of_ttl = Duration::from_secs(*ttl) / 2; if let Some(i) = self
.rendezvous_nodes
self.registration_status = RegistrationStatus::Registered { .iter()
re_register_in: Box::pin(tokio::time::sleep(half_of_ttl)), .position(|n| &n.peer_id == rendezvous_node)
}; {
let half_of_ttl = Duration::from_secs(*ttl) / 2;
let re_register_in = Box::pin(tokio::time::sleep(half_of_ttl));
let status = RegistrationStatus::Registered { re_register_in };
self.rendezvous_nodes[i].set_registration(status);
}
} }
inner_poll inner_poll
@ -380,6 +420,7 @@ pub mod rendezous {
use futures::StreamExt; use futures::StreamExt;
use libp2p::rendezvous; use libp2p::rendezvous;
use libp2p::swarm::SwarmEvent; use libp2p::swarm::SwarmEvent;
use std::collections::HashMap;
#[tokio::test] #[tokio::test]
async fn given_no_initial_connection_when_constructed_asb_connects_and_registers_with_rendezvous_node( async fn given_no_initial_connection_when_constructed_asb_connects_and_registers_with_rendezvous_node(
@ -387,16 +428,16 @@ pub mod rendezous {
let mut rendezvous_node = new_swarm(|_, _| { let mut rendezvous_node = new_swarm(|_, _| {
rendezvous::server::Behaviour::new(rendezvous::server::Config::default()) rendezvous::server::Behaviour::new(rendezvous::server::Config::default())
}); });
let rendezvous_address = rendezvous_node.listen_on_random_memory_address().await; let address = rendezvous_node.listen_on_random_memory_address().await;
let rendezvous_point = RendezvousNode::new(
&address,
rendezvous_node.local_peer_id().to_owned(),
XmrBtcNamespace::Testnet,
None,
);
let mut asb = new_swarm(|_, identity| { let mut asb = new_swarm(|_, identity| {
rendezous::Behaviour::new( super::rendezvous::Behaviour::new(identity, vec![rendezvous_point])
identity,
*rendezvous_node.local_peer_id(),
rendezvous_address,
XmrBtcNamespace::Testnet,
None,
)
}); });
asb.listen_on_random_memory_address().await; // this adds an external address asb.listen_on_random_memory_address().await; // this adds an external address
@ -428,16 +469,16 @@ pub mod rendezous {
rendezvous::server::Config::default().with_min_ttl(2), rendezvous::server::Config::default().with_min_ttl(2),
) )
}); });
let rendezvous_address = rendezvous_node.listen_on_random_memory_address().await; let address = rendezvous_node.listen_on_random_memory_address().await;
let rendezvous_point = RendezvousNode::new(
&address,
rendezvous_node.local_peer_id().to_owned(),
XmrBtcNamespace::Testnet,
Some(5),
);
let mut asb = new_swarm(|_, identity| { let mut asb = new_swarm(|_, identity| {
rendezous::Behaviour::new( super::rendezvous::Behaviour::new(identity, vec![rendezvous_point])
identity,
*rendezvous_node.local_peer_id(),
rendezvous_address,
XmrBtcNamespace::Testnet,
Some(5),
)
}); });
asb.listen_on_random_memory_address().await; // this adds an external address asb.listen_on_random_memory_address().await; // this adds an external address
@ -467,5 +508,62 @@ pub mod rendezous {
.unwrap() .unwrap()
.unwrap(); .unwrap();
} }
#[tokio::test]
async fn asb_registers_multiple() {
let registration_ttl = Some(10);
let mut rendezvous_nodes = Vec::new();
let mut registrations = HashMap::new();
// register with 5 rendezvous nodes
for _ in 0..5 {
let mut rendezvous = new_swarm(|_, _| {
rendezvous::server::Behaviour::new(
rendezvous::server::Config::default().with_min_ttl(2),
)
});
let address = rendezvous.listen_on_random_memory_address().await;
let id = *rendezvous.local_peer_id();
registrations.insert(id, 0);
rendezvous_nodes.push(RendezvousNode::new(
&address,
*rendezvous.local_peer_id(),
XmrBtcNamespace::Testnet,
registration_ttl,
));
tokio::spawn(async move {
loop {
rendezvous.next().await;
}
});
}
let mut asb = new_swarm(|_, identity| {
super::rendezvous::Behaviour::new(identity, rendezvous_nodes)
});
asb.listen_on_random_memory_address().await; // this adds an external address
let handle = tokio::spawn(async move {
loop {
if let SwarmEvent::Behaviour(rendezvous::client::Event::Registered {
rendezvous_node,
..
}) = asb.select_next_some().await
{
registrations
.entry(rendezvous_node)
.and_modify(|counter| *counter += 1);
}
if registrations.iter().all(|(_, &count)| count >= 4) {
break;
}
}
});
tokio::time::timeout(Duration::from_secs(30), handle)
.await
.unwrap()
.unwrap();
}
} }
} }

@ -102,6 +102,19 @@ async fn main() -> Result<()> {
match cmd { match cmd {
Command::Start { resume_only } => { Command::Start { resume_only } => {
// check and warn for duplicate rendezvous points
let mut rendezvous_addrs = config.network.rendezvous_point.clone();
let prev_len = rendezvous_addrs.len();
rendezvous_addrs.sort();
rendezvous_addrs.dedup();
let new_len = rendezvous_addrs.len();
if new_len < prev_len {
tracing::warn!(
"`rendezvous_point` config has {} duplicate entries, they are being ignored.",
prev_len - new_len
);
}
let monero_wallet = init_monero_wallet(&config, env_config).await?; let monero_wallet = init_monero_wallet(&config, env_config).await?;
let monero_address = monero_wallet.get_main_address(); let monero_address = monero_wallet.get_main_address();
tracing::info!(%monero_address, "Monero wallet address"); tracing::info!(%monero_address, "Monero wallet address");
@ -161,7 +174,7 @@ async fn main() -> Result<()> {
resume_only, resume_only,
env_config, env_config,
namespace, namespace,
config.network.rendezvous_point, &rendezvous_addrs,
)?; )?;
for listen in config.network.listen.clone() { for listen in config.network.listen.clone() {

@ -521,7 +521,7 @@ async fn init_bitcoin_wallet(
async fn init_monero_wallet( async fn init_monero_wallet(
data_dir: PathBuf, data_dir: PathBuf,
monero_daemon_address: String, monero_daemon_address: Option<String>,
env_config: Config, env_config: Config,
) -> Result<(monero::Wallet, monero::WalletRpcProcess)> { ) -> Result<(monero::Wallet, monero::WalletRpcProcess)> {
let network = env_config.monero_network; let network = env_config.monero_network;
@ -531,7 +531,7 @@ async fn init_monero_wallet(
let monero_wallet_rpc = monero::WalletRpc::new(data_dir.join("monero")).await?; let monero_wallet_rpc = monero::WalletRpc::new(data_dir.join("monero")).await?;
let monero_wallet_rpc_process = monero_wallet_rpc let monero_wallet_rpc_process = monero_wallet_rpc
.run(network, monero_daemon_address.as_str()) .run(network, monero_daemon_address)
.await?; .await?;
let monero_wallet = monero::Wallet::open_or_create( let monero_wallet = monero::Wallet::open_or_create(

@ -210,14 +210,20 @@ impl TxCancel {
}; };
// The order in which these are inserted doesn't matter // The order in which these are inserted doesn't matter
satisfier.insert(A, ::bitcoin::EcdsaSig { satisfier.insert(
sig: sig_a.into(), A,
hash_ty: EcdsaSighashType::All, ::bitcoin::EcdsaSig {
}); sig: sig_a.into(),
satisfier.insert(B, ::bitcoin::EcdsaSig { hash_ty: EcdsaSighashType::All,
sig: sig_b.into(), },
hash_ty: EcdsaSighashType::All, );
}); satisfier.insert(
B,
::bitcoin::EcdsaSig {
sig: sig_b.into(),
hash_ty: EcdsaSighashType::All,
},
);
satisfier satisfier
}; };

@ -65,14 +65,20 @@ impl TxPunish {
let B = B.try_into()?; let B = B.try_into()?;
// The order in which these are inserted doesn't matter // The order in which these are inserted doesn't matter
satisfier.insert(A, ::bitcoin::EcdsaSig { satisfier.insert(
sig: sig_a.into(), A,
hash_ty: EcdsaSighashType::All, ::bitcoin::EcdsaSig {
}); sig: sig_a.into(),
satisfier.insert(B, ::bitcoin::EcdsaSig { hash_ty: EcdsaSighashType::All,
sig: sig_b.into(), },
hash_ty: EcdsaSighashType::All, );
}); satisfier.insert(
B,
::bitcoin::EcdsaSig {
sig: sig_b.into(),
hash_ty: EcdsaSighashType::All,
},
);
satisfier satisfier
}; };

@ -87,14 +87,20 @@ impl TxRedeem {
}; };
// The order in which these are inserted doesn't matter // The order in which these are inserted doesn't matter
satisfier.insert(A, ::bitcoin::EcdsaSig { satisfier.insert(
sig: sig_a.into(), A,
hash_ty: EcdsaSighashType::All, ::bitcoin::EcdsaSig {
}); sig: sig_a.into(),
satisfier.insert(B, ::bitcoin::EcdsaSig { hash_ty: EcdsaSighashType::All,
sig: sig_b.into(), },
hash_ty: EcdsaSighashType::All, );
}); satisfier.insert(
B,
::bitcoin::EcdsaSig {
sig: sig_b.into(),
hash_ty: EcdsaSighashType::All,
},
);
satisfier satisfier
}; };

@ -70,14 +70,20 @@ impl TxRefund {
}; };
// The order in which these are inserted doesn't matter // The order in which these are inserted doesn't matter
satisfier.insert(A, ::bitcoin::EcdsaSig { satisfier.insert(
sig: sig_a.into(), A,
hash_ty: EcdsaSighashType::All, ::bitcoin::EcdsaSig {
}); sig: sig_a.into(),
satisfier.insert(B, ::bitcoin::EcdsaSig { hash_ty: EcdsaSighashType::All,
sig: sig_b.into(), },
hash_ty: EcdsaSighashType::All, );
}); satisfier.insert(
B,
::bitcoin::EcdsaSig {
sig: sig_b.into(),
hash_ty: EcdsaSighashType::All,
},
);
satisfier satisfier
}; };

@ -54,7 +54,7 @@ impl Wallet {
) -> Result<Self> { ) -> Result<Self> {
let data_dir = data_dir.as_ref(); let data_dir = data_dir.as_ref();
let wallet_dir = data_dir.join(WALLET); let wallet_dir = data_dir.join(WALLET);
let database = bdk::sled::open(&wallet_dir)?.open_tree(SLED_TREE_NAME)?; let database = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?;
let network = env_config.bitcoin_network; let network = env_config.bitcoin_network;
let wallet = match bdk::Wallet::new( let wallet = match bdk::Wallet::new(
@ -97,7 +97,7 @@ impl Wallet {
std::fs::rename(from, to)?; std::fs::rename(from, to)?;
let wallet_dir = data_dir.join(WALLET); let wallet_dir = data_dir.join(WALLET);
let database = bdk::sled::open(&wallet_dir)?.open_tree(SLED_TREE_NAME)?; let database = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?;
let wallet = bdk::Wallet::new( let wallet = bdk::Wallet::new(
bdk::template::Bip84(xprivkey, KeychainKind::External), bdk::template::Bip84(xprivkey, KeychainKind::External),
@ -738,12 +738,15 @@ impl Client {
let client = bdk::electrum_client::Client::new(electrum_rpc_url.as_str()) let client = bdk::electrum_client::Client::new(electrum_rpc_url.as_str())
.context("Failed to initialize Electrum RPC client")?; .context("Failed to initialize Electrum RPC client")?;
let blockchain = ElectrumBlockchain::from(client); let blockchain = ElectrumBlockchain::from(client);
let last_sync = Instant::now()
.checked_sub(interval)
.expect("no underflow since block time is only 600 secs");
Ok(Self { Ok(Self {
electrum, electrum,
blockchain, blockchain,
latest_block_height: BlockHeight::try_from(latest_block)?, latest_block_height: BlockHeight::try_from(latest_block)?,
last_sync: Instant::now() - interval, last_sync,
sync_interval: interval, sync_interval: interval,
script_history: Default::default(), script_history: Default::default(),
subscriptions: Default::default(), subscriptions: Default::default(),
@ -758,9 +761,10 @@ impl Client {
self.blockchain.get_tx(txid) self.blockchain.get_tx(txid)
} }
fn update_state(&mut self) -> Result<()> { fn update_state(&mut self, force_sync: bool) -> Result<()> {
let now = Instant::now(); let now = Instant::now();
if now < self.last_sync + self.sync_interval {
if !force_sync && now < self.last_sync + self.sync_interval {
return Ok(()); return Ok(());
} }
@ -780,9 +784,14 @@ impl Client {
if !self.script_history.contains_key(&script) { if !self.script_history.contains_key(&script) {
self.script_history.insert(script.clone(), vec![]); self.script_history.insert(script.clone(), vec![]);
}
self.update_state()?; // When we first subscribe to a script we want to immediately fetch its status
// Otherwise we would have to wait for the next sync interval, which can take a minute
// This would result in potentially inaccurate status updates until that next sync interval is hit
self.update_state(true)?;
} else {
self.update_state(false)?;
}
let history = self.script_history.entry(script).or_default(); let history = self.script_history.entry(script).or_default();

@ -15,6 +15,7 @@ pub use list_sellers::{list_sellers, Seller, Status as SellerStatus};
mod tests { mod tests {
use super::*; use super::*;
use crate::asb; use crate::asb;
use crate::asb::rendezvous::RendezvousNode;
use crate::cli::list_sellers::{Seller, Status}; use crate::cli::list_sellers::{Seller, Status};
use crate::network::quote; use crate::network::quote;
use crate::network::quote::BidQuote; use crate::network::quote::BidQuote;
@ -33,10 +34,8 @@ mod tests {
async fn list_sellers_should_report_all_registered_asbs_with_a_quote() { async fn list_sellers_should_report_all_registered_asbs_with_a_quote() {
let namespace = XmrBtcNamespace::Mainnet; let namespace = XmrBtcNamespace::Mainnet;
let (rendezvous_address, rendezvous_peer_id) = setup_rendezvous_point().await; let (rendezvous_address, rendezvous_peer_id) = setup_rendezvous_point().await;
let expected_seller_1 = let expected_seller_1 = setup_asb(rendezvous_peer_id, &rendezvous_address, namespace).await;
setup_asb(rendezvous_peer_id, rendezvous_address.clone(), namespace).await; let expected_seller_2 = setup_asb(rendezvous_peer_id, &rendezvous_address, namespace).await;
let expected_seller_2 =
setup_asb(rendezvous_peer_id, rendezvous_address.clone(), namespace).await;
let list_sellers = list_sellers( let list_sellers = list_sellers(
rendezvous_peer_id, rendezvous_peer_id,
@ -72,7 +71,7 @@ mod tests {
async fn setup_asb( async fn setup_asb(
rendezvous_peer_id: PeerId, rendezvous_peer_id: PeerId,
rendezvous_address: Multiaddr, rendezvous_address: &Multiaddr,
namespace: XmrBtcNamespace, namespace: XmrBtcNamespace,
) -> Seller { ) -> Seller {
let static_quote = BidQuote { let static_quote = BidQuote {
@ -81,18 +80,18 @@ mod tests {
max_quantity: bitcoin::Amount::from_sat(9001), max_quantity: bitcoin::Amount::from_sat(9001),
}; };
let mut asb = new_swarm(|_, identity| StaticQuoteAsbBehaviour { let mut asb = new_swarm(|_, identity| {
rendezvous: asb::rendezous::Behaviour::new( let rendezvous_node =
identity, RendezvousNode::new(rendezvous_address, rendezvous_peer_id, namespace, None);
rendezvous_peer_id, let rendezvous = asb::rendezvous::Behaviour::new(identity, vec![rendezvous_node]);
rendezvous_address,
namespace, StaticQuoteAsbBehaviour {
None, rendezvous,
), ping: Default::default(),
ping: Default::default(), quote: quote::asb(),
quote: quote::asb(), static_quote,
static_quote, registered: false,
registered: false, }
}); });
let asb_address = asb.listen_on_tcp_localhost().await; let asb_address = asb.listen_on_tcp_localhost().await;
@ -121,7 +120,7 @@ mod tests {
#[derive(libp2p::NetworkBehaviour)] #[derive(libp2p::NetworkBehaviour)]
#[behaviour(event_process = true)] #[behaviour(event_process = true)]
struct StaticQuoteAsbBehaviour { struct StaticQuoteAsbBehaviour {
rendezvous: asb::rendezous::Behaviour, rendezvous: asb::rendezvous::Behaviour,
// Support `Ping` as a workaround until https://github.com/libp2p/rust-libp2p/issues/2109 is fixed. // Support `Ping` as a workaround until https://github.com/libp2p/rust-libp2p/issues/2109 is fixed.
ping: libp2p::ping::Ping, ping: libp2p::ping::Ping,
quote: quote::Behaviour, quote: quote::Behaviour,

@ -14,10 +14,6 @@ use structopt::{clap, StructOpt};
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
// See: https://moneroworld.com/
pub const DEFAULT_MONERO_DAEMON_ADDRESS: &str = "node.community.rino.io:18081";
pub const DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET: &str = "stagenet.community.rino.io:38081";
// See: https://1209k.com/bitcoin-eye/ele.php?chain=btc // See: https://1209k.com/bitcoin-eye/ele.php?chain=btc
const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://blockstream.info:700"; const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://blockstream.info:700";
// See: https://1209k.com/bitcoin-eye/ele.php?chain=tbtc // See: https://1209k.com/bitcoin-eye/ele.php?chain=tbtc
@ -80,11 +76,11 @@ where
} => { } => {
let (bitcoin_electrum_rpc_url, bitcoin_target_block) = let (bitcoin_electrum_rpc_url, bitcoin_target_block) =
bitcoin.apply_defaults(is_testnet)?; bitcoin.apply_defaults(is_testnet)?;
let monero_daemon_address = monero.apply_defaults(is_testnet);
let monero_receive_address = let monero_receive_address =
validate_monero_address(monero_receive_address, is_testnet)?; validate_monero_address(monero_receive_address, is_testnet)?;
let bitcoin_change_address = let bitcoin_change_address =
validate_bitcoin_address(bitcoin_change_address, is_testnet)?; validate_bitcoin_address(bitcoin_change_address, is_testnet)?;
let monero_daemon_address = monero.monero_daemon_address;
Arguments { Arguments {
env_config: env_config_from(is_testnet), env_config: env_config_from(is_testnet),
@ -167,7 +163,7 @@ where
} => { } => {
let (bitcoin_electrum_rpc_url, bitcoin_target_block) = let (bitcoin_electrum_rpc_url, bitcoin_target_block) =
bitcoin.apply_defaults(is_testnet)?; bitcoin.apply_defaults(is_testnet)?;
let monero_daemon_address = monero.apply_defaults(is_testnet); let monero_daemon_address = monero.monero_daemon_address;
Arguments { Arguments {
env_config: env_config_from(is_testnet), env_config: env_config_from(is_testnet),
@ -254,7 +250,7 @@ pub enum Command {
bitcoin_target_block: usize, bitcoin_target_block: usize,
bitcoin_change_address: bitcoin::Address, bitcoin_change_address: bitcoin::Address,
monero_receive_address: monero::Address, monero_receive_address: monero::Address,
monero_daemon_address: String, monero_daemon_address: Option<String>,
tor_socks5_port: u16, tor_socks5_port: u16,
namespace: XmrBtcNamespace, namespace: XmrBtcNamespace,
}, },
@ -274,7 +270,7 @@ pub enum Command {
swap_id: Uuid, swap_id: Uuid,
bitcoin_electrum_rpc_url: Url, bitcoin_electrum_rpc_url: Url,
bitcoin_target_block: usize, bitcoin_target_block: usize,
monero_daemon_address: String, monero_daemon_address: Option<String>,
tor_socks5_port: u16, tor_socks5_port: u16,
namespace: XmrBtcNamespace, namespace: XmrBtcNamespace,
}, },
@ -302,7 +298,7 @@ pub enum Command {
name = "swap", name = "swap",
about = "CLI for swapping BTC for XMR", about = "CLI for swapping BTC for XMR",
author, author,
version = env!("VERGEN_GIT_SEMVER_LIGHTWEIGHT") version = env!("VERGEN_GIT_DESCRIBE")
)] )]
struct RawArguments { struct RawArguments {
// global is necessary to ensure that clap can match against testnet in subcommands // global is necessary to ensure that clap can match against testnet in subcommands
@ -436,23 +432,11 @@ enum RawCommand {
struct Monero { struct Monero {
#[structopt( #[structopt(
long = "monero-daemon-address", long = "monero-daemon-address",
help = "Specify to connect to a monero daemon of your choice: <host>:<port>" help = "Specify to connect to a monero daemon of your choice: <host>:<port>. If none is specified, we will connect to a public node."
)] )]
monero_daemon_address: Option<String>, monero_daemon_address: Option<String>,
} }
impl Monero {
fn apply_defaults(self, testnet: bool) -> String {
if let Some(address) = self.monero_daemon_address {
address
} else if testnet {
DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET.to_string()
} else {
DEFAULT_MONERO_DAEMON_ADDRESS.to_string()
}
}
}
#[derive(structopt::StructOpt, Debug)] #[derive(structopt::StructOpt, Debug)]
struct Bitcoin { struct Bitcoin {
#[structopt(long = "electrum-rpc", help = "Provide the Bitcoin Electrum RPC URL")] #[structopt(long = "electrum-rpc", help = "Provide the Bitcoin Electrum RPC URL")]
@ -1174,7 +1158,7 @@ mod tests {
bitcoin_change_address: BITCOIN_TESTNET_ADDRESS.parse().unwrap(), bitcoin_change_address: BITCOIN_TESTNET_ADDRESS.parse().unwrap(),
monero_receive_address: monero::Address::from_str(MONERO_STAGENET_ADDRESS) monero_receive_address: monero::Address::from_str(MONERO_STAGENET_ADDRESS)
.unwrap(), .unwrap(),
monero_daemon_address: DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET.to_string(), monero_daemon_address: None,
tor_socks5_port: DEFAULT_SOCKS5_PORT, tor_socks5_port: DEFAULT_SOCKS5_PORT,
namespace: XmrBtcNamespace::Testnet, namespace: XmrBtcNamespace::Testnet,
}, },
@ -1194,7 +1178,7 @@ mod tests {
bitcoin_change_address: BITCOIN_MAINNET_ADDRESS.parse().unwrap(), bitcoin_change_address: BITCOIN_MAINNET_ADDRESS.parse().unwrap(),
monero_receive_address: monero::Address::from_str(MONERO_MAINNET_ADDRESS) monero_receive_address: monero::Address::from_str(MONERO_MAINNET_ADDRESS)
.unwrap(), .unwrap(),
monero_daemon_address: DEFAULT_MONERO_DAEMON_ADDRESS.to_string(), monero_daemon_address: None,
tor_socks5_port: DEFAULT_SOCKS5_PORT, tor_socks5_port: DEFAULT_SOCKS5_PORT,
namespace: XmrBtcNamespace::Mainnet, namespace: XmrBtcNamespace::Mainnet,
}, },
@ -1212,7 +1196,7 @@ mod tests {
bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET) bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET)
.unwrap(), .unwrap(),
bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET, bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET,
monero_daemon_address: DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET.to_string(), monero_daemon_address: None,
tor_socks5_port: DEFAULT_SOCKS5_PORT, tor_socks5_port: DEFAULT_SOCKS5_PORT,
namespace: XmrBtcNamespace::Testnet, namespace: XmrBtcNamespace::Testnet,
}, },
@ -1229,7 +1213,7 @@ mod tests {
swap_id: Uuid::from_str(SWAP_ID).unwrap(), swap_id: Uuid::from_str(SWAP_ID).unwrap(),
bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(),
bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET, bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET,
monero_daemon_address: DEFAULT_MONERO_DAEMON_ADDRESS.to_string(), monero_daemon_address: None,
tor_socks5_port: DEFAULT_SOCKS5_PORT, tor_socks5_port: DEFAULT_SOCKS5_PORT,
namespace: XmrBtcNamespace::Mainnet, namespace: XmrBtcNamespace::Mainnet,
}, },

@ -350,23 +350,26 @@ mod tests {
list.sort(); list.sort();
assert_eq!(list, vec![ assert_eq!(
Seller { list,
multiaddr: "/ip4/127.0.0.1/tcp/5678".parse().unwrap(), vec![
status: Status::Online(BidQuote { Seller {
price: Default::default(), multiaddr: "/ip4/127.0.0.1/tcp/5678".parse().unwrap(),
min_quantity: Default::default(), status: Status::Online(BidQuote {
max_quantity: Default::default(), price: Default::default(),
}) min_quantity: Default::default(),
}, max_quantity: Default::default(),
Seller { })
multiaddr: Multiaddr::empty(), },
status: Status::Unreachable Seller {
}, multiaddr: Multiaddr::empty(),
Seller { status: Status::Unreachable
multiaddr: "/ip4/127.0.0.1/tcp/1234".parse().unwrap(), },
status: Status::Unreachable Seller {
}, multiaddr: "/ip4/127.0.0.1/tcp/1234".parse().unwrap(),
]) status: Status::Unreachable
},
]
)
} }
} }

@ -46,7 +46,7 @@ pub struct Regtest;
impl GetConfig for Mainnet { impl GetConfig for Mainnet {
fn get_config() -> Config { fn get_config() -> Config {
Config { Config {
bitcoin_lock_mempool_timeout: 3.std_minutes(), bitcoin_lock_mempool_timeout: 10.std_minutes(),
bitcoin_lock_confirmed_timeout: 2.std_hours(), bitcoin_lock_confirmed_timeout: 2.std_hours(),
bitcoin_finality_confirmations: 1, bitcoin_finality_confirmations: 1,
bitcoin_avg_block_time: 10.std_minutes(), bitcoin_avg_block_time: 10.std_minutes(),
@ -63,7 +63,7 @@ impl GetConfig for Mainnet {
impl GetConfig for Testnet { impl GetConfig for Testnet {
fn get_config() -> Config { fn get_config() -> Config {
Config { Config {
bitcoin_lock_mempool_timeout: 3.std_minutes(), bitcoin_lock_mempool_timeout: 10.std_minutes(),
bitcoin_lock_confirmed_timeout: 1.std_hours(), bitcoin_lock_confirmed_timeout: 1.std_hours(),
bitcoin_finality_confirmations: 1, bitcoin_finality_confirmations: 1,
bitcoin_avg_block_time: 10.std_minutes(), bitcoin_avg_block_time: 10.std_minutes(),

@ -174,11 +174,6 @@ impl Wallet {
pub async fn transfer(&self, request: TransferRequest) -> Result<TransferProof> { pub async fn transfer(&self, request: TransferRequest) -> Result<TransferProof> {
let inner = self.inner.lock().await; let inner = self.inner.lock().await;
inner
.open_wallet(self.name.clone())
.await
.with_context(|| format!("Failed to open wallet {}", self.name))?;
let TransferRequest { let TransferRequest {
public_spend_key, public_spend_key,
public_view_key, public_view_key,

@ -1,19 +1,45 @@
use ::monero::Network; use ::monero::Network;
use anyhow::{Context, Result}; use anyhow::{bail, Context, Error, Result};
use big_bytes::BigByte; use big_bytes::BigByte;
use futures::{StreamExt, TryStreamExt}; use futures::{StreamExt, TryStreamExt};
use monero_rpc::wallet::{Client, MoneroWalletRpc as _}; use monero_rpc::wallet::{Client, MoneroWalletRpc as _};
use reqwest::header::CONTENT_LENGTH; use reqwest::header::CONTENT_LENGTH;
use reqwest::Url; use reqwest::Url;
use serde::Deserialize;
use std::fmt;
use std::fmt::{Debug, Display, Formatter};
use std::io::ErrorKind; use std::io::ErrorKind;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Stdio; use std::process::Stdio;
use std::time::Duration;
use tokio::fs::{remove_file, OpenOptions}; use tokio::fs::{remove_file, OpenOptions};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, Command}; use tokio::process::{Child, Command};
use tokio_util::codec::{BytesCodec, FramedRead}; use tokio_util::codec::{BytesCodec, FramedRead};
use tokio_util::io::StreamReader; use tokio_util::io::StreamReader;
// See: https://www.moneroworld.com/#nodes, https://monero.fail
// We don't need any testnet nodes because we don't support testnet at all
const MONERO_DAEMONS: [MoneroDaemon; 17] = [
MoneroDaemon::new("xmr-node.cakewallet.com", 18081, Network::Mainnet),
MoneroDaemon::new("nodex.monerujo.io", 18081, Network::Mainnet),
MoneroDaemon::new("node.moneroworld.com", 18089, Network::Mainnet),
MoneroDaemon::new("nodes.hashvault.pro", 18081, Network::Mainnet),
MoneroDaemon::new("p2pmd.xmrvsbeast.com", 18081, Network::Mainnet),
MoneroDaemon::new("node.monerodevs.org", 18089, Network::Mainnet),
MoneroDaemon::new("xmr-node-usa-east.cakewallet.com", 18081, Network::Mainnet),
MoneroDaemon::new("xmr-node-uk.cakewallet.com", 18081, Network::Mainnet),
MoneroDaemon::new("node.community.rino.io", 18081, Network::Mainnet),
MoneroDaemon::new("testingjohnross.com", 20031, Network::Mainnet),
MoneroDaemon::new("xmr.litepay.ch", 18081, Network::Mainnet),
MoneroDaemon::new("node.trocador.app", 18089, Network::Mainnet),
MoneroDaemon::new("stagenet.xmr-tw.org", 38081, Network::Stagenet),
MoneroDaemon::new("node.monerodevs.org", 38089, Network::Stagenet),
MoneroDaemon::new("singapore.node.xmr.pm", 38081, Network::Stagenet),
MoneroDaemon::new("xmr-lux.boldsuck.org", 38081, Network::Stagenet),
MoneroDaemon::new("stagenet.community.rino.io", 38081, Network::Stagenet),
];
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
compile_error!("unsupported operating system"); compile_error!("unsupported operating system");
@ -50,6 +76,91 @@ pub struct WalletRpcProcess {
port: u16, port: u16,
} }
struct MoneroDaemon {
address: &'static str,
port: u16,
network: Network,
}
impl MoneroDaemon {
const fn new(address: &'static str, port: u16, network: Network) -> Self {
Self {
address,
port,
network,
}
}
/// Checks if the Monero daemon is available by sending a request to its `get_info` endpoint.
async fn is_available(&self, client: &reqwest::Client) -> Result<bool, Error> {
let url = format!("http://{}:{}/get_info", self.address, self.port);
let res = client
.get(url)
.send()
.await
.context("Failed to send request to get_info endpoint")?;
let json: MoneroDaemonGetInfoResponse = res
.json()
.await
.context("Failed to deserialize daemon get_info response")?;
let is_status_ok = json.status == "OK";
let is_synchronized = json.synchronized;
let is_correct_network = match self.network {
Network::Mainnet => json.mainnet,
Network::Stagenet => json.stagenet,
Network::Testnet => json.testnet,
};
Ok(is_status_ok && is_synchronized && is_correct_network)
}
}
impl Display for MoneroDaemon {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.address, self.port)
}
}
#[derive(Deserialize)]
struct MoneroDaemonGetInfoResponse {
status: String,
synchronized: bool,
mainnet: bool,
stagenet: bool,
testnet: bool,
}
/// Chooses an available Monero daemon based on the specified network.
async fn choose_monero_daemon(network: Network) -> Result<&'static MoneroDaemon, Error> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.https_only(false)
.build()?;
// We only want to check for daemons that match the specified network
let network_matching_daemons = MONERO_DAEMONS
.iter()
.filter(|daemon| daemon.network == network);
for daemon in network_matching_daemons {
match daemon.is_available(&client).await {
Ok(true) => {
tracing::debug!(%daemon, "Found available Monero daemon");
return Ok(daemon);
}
Err(err) => {
tracing::debug!(%err, %daemon, "Failed to connect to Monero daemon");
continue;
}
Ok(false) => continue,
}
}
bail!("No Monero daemon could be found. Please specify one manually or try again later.")
}
impl WalletRpcProcess { impl WalletRpcProcess {
pub fn endpoint(&self) -> Url { pub fn endpoint(&self) -> Url {
Url::parse(&format!("http://127.0.0.1:{}/json_rpc", self.port)) Url::parse(&format!("http://127.0.0.1:{}/json_rpc", self.port))
@ -153,13 +264,23 @@ impl WalletRpc {
Ok(monero_wallet_rpc) Ok(monero_wallet_rpc)
} }
pub async fn run(&self, network: Network, daemon_address: &str) -> Result<WalletRpcProcess> { pub async fn run(
&self,
network: Network,
daemon_address: Option<String>,
) -> Result<WalletRpcProcess> {
let port = tokio::net::TcpListener::bind("127.0.0.1:0") let port = tokio::net::TcpListener::bind("127.0.0.1:0")
.await? .await?
.local_addr()? .local_addr()?
.port(); .port();
let daemon_address = match daemon_address {
Some(daemon_address) => daemon_address,
None => choose_monero_daemon(network).await?.to_string(),
};
tracing::debug!( tracing::debug!(
%daemon_address,
%port, %port,
"Starting monero-wallet-rpc" "Starting monero-wallet-rpc"
); );
@ -232,7 +353,6 @@ impl WalletRpc {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
async fn extract_archive(monero_wallet_rpc: &Self) -> Result<()> { async fn extract_archive(monero_wallet_rpc: &Self) -> Result<()> {
use anyhow::bail;
use tokio_tar::Archive; use tokio_tar::Archive;
let mut options = OpenOptions::new(); let mut options = OpenOptions::new();
@ -297,3 +417,123 @@ impl WalletRpc {
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use super::*;
fn extract_host_and_port(address: String) -> (&'static str, u16) {
let parts: Vec<&str> = address.split(':').collect();
if parts.len() == 2 {
let host = parts[0].to_string();
let port = parts[1].parse::<u16>().unwrap();
let static_str_host: &'static str = Box::leak(host.into_boxed_str());
return (static_str_host, port);
}
panic!("Could not extract host and port from address: {}", address)
}
#[tokio::test]
async fn test_is_daemon_available_success() {
let mut server = mockito::Server::new();
let _ = server
.mock("GET", "/get_info")
.with_status(200)
.with_body(
r#"
{
"status": "OK",
"synchronized": true,
"mainnet": true,
"stagenet": false,
"testnet": false
}
"#,
)
.create();
let (host, port) = extract_host_and_port(server.host_with_port());
let client = reqwest::Client::new();
let result = MoneroDaemon::new(host, port, Network::Mainnet)
.is_available(&client)
.await;
assert!(result.is_ok());
assert!(result.unwrap());
}
#[tokio::test]
async fn test_is_daemon_available_wrong_network_failure() {
let mut server = mockito::Server::new();
let _ = server
.mock("GET", "/get_info")
.with_status(200)
.with_body(
r#"
{
"status": "OK",
"synchronized": true,
"mainnet": true,
"stagenet": false,
"testnet": false
}
"#,
)
.create();
let (host, port) = extract_host_and_port(server.host_with_port());
let client = reqwest::Client::new();
let result = MoneroDaemon::new(host, port, Network::Stagenet)
.is_available(&client)
.await;
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[tokio::test]
async fn test_is_daemon_available_not_synced_failure() {
let mut server = mockito::Server::new();
let _ = server
.mock("GET", "/get_info")
.with_status(200)
.with_body(
r#"
{
"status": "OK",
"synchronized": false,
"mainnet": true,
"stagenet": false,
"testnet": false
}
"#,
)
.create();
let (host, port) = extract_host_and_port(server.host_with_port());
let client = reqwest::Client::new();
let result = MoneroDaemon::new(host, port, Network::Mainnet)
.is_available(&client)
.await;
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[tokio::test]
async fn test_is_daemon_available_network_error_failure() {
let client = reqwest::Client::new();
let result = MoneroDaemon::new("does.not.exist.com", 18081, Network::Mainnet)
.is_available(&client)
.await;
assert!(result.is_err());
}
}

@ -155,13 +155,16 @@ impl ProtocolsHandler for Handler {
let env_config = self.env_config; let env_config = self.env_config;
let protocol = tokio::time::timeout(self.timeout, async move { let protocol = tokio::time::timeout(self.timeout, async move {
write_cbor_message(&mut substream, SpotPriceRequest { write_cbor_message(
btc: info.btc, &mut substream,
blockchain_network: BlockchainNetwork { SpotPriceRequest {
bitcoin: env_config.bitcoin_network, btc: info.btc,
monero: env_config.monero_network, blockchain_network: BlockchainNetwork {
bitcoin: env_config.bitcoin_network,
monero: env_config.monero_network,
},
}, },
}) )
.await?; .await?;
let xmr = Result::from(read_cbor_message::<SpotPriceResponse>(&mut substream).await?)?; let xmr = Result::from(read_cbor_message::<SpotPriceResponse>(&mut substream).await?)?;

@ -1,9 +1,9 @@
use crate::asb::LatestRate; use crate::asb::{LatestRate, RendezvousNode};
use crate::libp2p_ext::MultiAddrExt; use crate::libp2p_ext::MultiAddrExt;
use crate::network::rendezvous::XmrBtcNamespace; use crate::network::rendezvous::XmrBtcNamespace;
use crate::seed::Seed; use crate::seed::Seed;
use crate::{asb, bitcoin, cli, env, tor}; use crate::{asb, bitcoin, cli, env, tor};
use anyhow::{Context, Result}; use anyhow::Result;
use libp2p::swarm::{NetworkBehaviour, SwarmBuilder}; use libp2p::swarm::{NetworkBehaviour, SwarmBuilder};
use libp2p::{identity, Multiaddr, Swarm}; use libp2p::{identity, Multiaddr, Swarm};
use std::fmt::Debug; use std::fmt::Debug;
@ -17,22 +17,23 @@ pub fn asb<LR>(
resume_only: bool, resume_only: bool,
env_config: env::Config, env_config: env::Config,
namespace: XmrBtcNamespace, namespace: XmrBtcNamespace,
rendezvous_point: Option<Multiaddr>, rendezvous_addrs: &[Multiaddr],
) -> Result<Swarm<asb::Behaviour<LR>>> ) -> Result<Swarm<asb::Behaviour<LR>>>
where where
LR: LatestRate + Send + 'static + Debug + Clone, LR: LatestRate + Send + 'static + Debug + Clone,
{ {
let identity = seed.derive_libp2p_identity(); let identity = seed.derive_libp2p_identity();
let rendezvous_params = if let Some(address) = rendezvous_point { let rendezvous_nodes = rendezvous_addrs
let peer_id = address .iter()
.extract_peer_id() .map(|addr| {
.context("Rendezvous node address must contain peer ID")?; let peer_id = addr
.extract_peer_id()
.expect("Rendezvous node address must contain peer ID");
Some((identity.clone(), peer_id, address, namespace)) RendezvousNode::new(addr, peer_id, namespace, None)
} else { })
None .collect();
};
let behaviour = asb::Behaviour::new( let behaviour = asb::Behaviour::new(
min_buy, min_buy,
@ -41,7 +42,7 @@ where
resume_only, resume_only,
env_config, env_config,
(identity.clone(), namespace), (identity.clone(), namespace),
rendezvous_params, rendezvous_nodes,
); );
let transport = asb::transport::new(&identity)?; let transport = asb::transport::new(&identity)?;

@ -21,7 +21,7 @@ struct GlobalSpawnTokioExecutor;
impl Executor for GlobalSpawnTokioExecutor { impl Executor for GlobalSpawnTokioExecutor {
fn exec(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>) { fn exec(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>) {
let _ = tokio::spawn(future); tokio::spawn(future);
} }
} }

@ -184,29 +184,32 @@ impl State0 {
let v = self.v_a + msg.v_b; let v = self.v_a + msg.v_b;
Ok((msg.swap_id, State1 { Ok((
a: self.a, msg.swap_id,
B: msg.B, State1 {
s_a: self.s_a, a: self.a,
S_a_monero: self.S_a_monero, B: msg.B,
S_a_bitcoin: self.S_a_bitcoin, s_a: self.s_a,
S_b_monero: msg.S_b_monero, S_a_monero: self.S_a_monero,
S_b_bitcoin: msg.S_b_bitcoin, S_a_bitcoin: self.S_a_bitcoin,
v, S_b_monero: msg.S_b_monero,
v_a: self.v_a, S_b_bitcoin: msg.S_b_bitcoin,
dleq_proof_s_a: self.dleq_proof_s_a, v,
btc: self.btc, v_a: self.v_a,
xmr: self.xmr, dleq_proof_s_a: self.dleq_proof_s_a,
cancel_timelock: self.cancel_timelock, btc: self.btc,
punish_timelock: self.punish_timelock, xmr: self.xmr,
refund_address: msg.refund_address, cancel_timelock: self.cancel_timelock,
redeem_address: self.redeem_address, punish_timelock: self.punish_timelock,
punish_address: self.punish_address, refund_address: msg.refund_address,
tx_redeem_fee: self.tx_redeem_fee, redeem_address: self.redeem_address,
tx_punish_fee: self.tx_punish_fee, punish_address: self.punish_address,
tx_refund_fee: msg.tx_refund_fee, tx_redeem_fee: self.tx_redeem_fee,
tx_cancel_fee: msg.tx_cancel_fee, tx_punish_fee: self.tx_punish_fee,
})) tx_refund_fee: msg.tx_refund_fee,
tx_cancel_fee: msg.tx_cancel_fee,
},
))
} }
} }

@ -61,7 +61,7 @@ impl Seed {
let file_path = Path::new(&file_path_buf); let file_path = Path::new(&file_path_buf);
if file_path.exists() { if file_path.exists() {
return Self::from_file(&file_path); return Self::from_file(file_path);
} }
tracing::debug!("No seed file found, creating at {}", file_path.display()); tracing::debug!("No seed file found, creating at {}", file_path.display());
@ -106,11 +106,12 @@ impl Seed {
} }
fn from_pem(pem: pem::Pem) -> Result<Self, Error> { fn from_pem(pem: pem::Pem) -> Result<Self, Error> {
if pem.contents.len() != SEED_LENGTH { let contents = pem.contents();
Err(Error::IncorrectLength(pem.contents.len())) if contents.len() != SEED_LENGTH {
Err(Error::IncorrectLength(contents.len()))
} else { } else {
let mut array = [0; SEED_LENGTH]; let mut array = [0; SEED_LENGTH];
for (i, b) in pem.contents.iter().enumerate() { for (i, b) in contents.iter().enumerate() {
array[i] = *b; array[i] = *b;
} }
@ -122,10 +123,7 @@ impl Seed {
ensure_directory_exists(&seed_file)?; ensure_directory_exists(&seed_file)?;
let data = self.bytes(); let data = self.bytes();
let pem = Pem { let pem = Pem::new("SEED", data);
tag: String::from("SEED"),
contents: data.to_vec(),
};
let pem_string = encode(&pem); let pem_string = encode(&pem);
@ -224,19 +222,20 @@ VnZUNFZ4dlY=
} }
#[test] #[test]
#[should_panic]
fn seed_from_pem_fails_for_long_seed() { fn seed_from_pem_fails_for_long_seed() {
let long = "-----BEGIN SEED----- let long = "-----BEGIN SEED-----
mbKANv2qKGmNVg1qtquj6Hx1pFPelpqOfE2JaJJAMEg1FlFhNRNlFlE= MIIBPQIBAAJBAOsfi5AGYhdRs/x6q5H7kScxA0Kzzqe6WI6gf6+tc6IvKQJo5rQc
mbKANv2qKGmNVg1qtquj6Hx1pFPelpqOfE2JaJJAMEg1FlFhNRNlFlE= dWWSQ0nRGt2hOPDO+35NKhQEjBQxPh/v7n0CAwEAAQJBAOGaBAyuw0ICyENy5NsO
-----END SEED----- -----END SEED-----
"; ";
let pem = pem::parse(long).unwrap(); let pem = pem::parse(long).unwrap();
assert_eq!(pem.contents().len(), 96);
match Seed::from_pem(pem) { match Seed::from_pem(pem) {
Ok(_) => panic!("should fail for long payload"), Ok(_) => panic!("should fail for long payload"),
Err(e) => { Err(e) => {
match e { match e {
Error::IncorrectLength(_) => {} // pass Error::IncorrectLength(len) => assert_eq!(len, 96), // pass
_ => panic!("should fail with IncorrectLength error"), _ => panic!("should fail with IncorrectLength error"),
} }
} }

@ -8,7 +8,7 @@ async fn ensure_same_swap_id_for_alice_and_bob() {
harness::setup_test(SlowCancelConfig, |mut ctx| async move { harness::setup_test(SlowCancelConfig, |mut ctx| async move {
let (bob_swap, _) = ctx.bob_swap().await; let (bob_swap, _) = ctx.bob_swap().await;
let bob_swap_id = bob_swap.id; let bob_swap_id = bob_swap.id;
let _ = tokio::spawn(bob::run(bob_swap)); tokio::spawn(bob::run(bob_swap));
// once Bob's swap is spawned we can retrieve Alice's swap and assert on the // once Bob's swap is spawned we can retrieve Alice's swap and assert on the
// swap ID // swap ID

@ -1,6 +1,5 @@
use std::collections::HashMap; use std::collections::BTreeMap;
use testcontainers::core::{Container, Docker, WaitForMessage}; use testcontainers::{core::WaitFor, Image, ImageArgs};
use testcontainers::Image;
pub const RPC_USER: &str = "admin"; pub const RPC_USER: &str = "admin";
pub const RPC_PASSWORD: &str = "123"; pub const RPC_PASSWORD: &str = "123";
@ -10,57 +9,27 @@ pub const DATADIR: &str = "/home/bdk";
#[derive(Debug)] #[derive(Debug)]
pub struct Bitcoind { pub struct Bitcoind {
args: BitcoindArgs,
entrypoint: Option<String>, entrypoint: Option<String>,
volume: Option<String>, volumes: BTreeMap<String, String>,
} }
impl Image for Bitcoind { impl Image for Bitcoind {
type Args = BitcoindArgs; type Args = BitcoindArgs;
type EnvVars = HashMap<String, String>;
type Volumes = HashMap<String, String>;
type EntryPoint = str;
fn descriptor(&self) -> String { fn name(&self) -> String {
"coblox/bitcoin-core:0.21.0".to_string() "coblox/bitcoin-core".into()
} }
fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) { fn tag(&self) -> String {
container "0.21.0".into()
.logs()
.stdout
.wait_for_message("init message: Done loading")
.unwrap();
} }
fn args(&self) -> <Self as Image>::Args { fn ready_conditions(&self) -> Vec<WaitFor> {
self.args.clone() vec![WaitFor::message_on_stdout("init message: Done loading")]
} }
fn volumes(&self) -> Self::Volumes { fn volumes(&self) -> Box<dyn Iterator<Item = (&String, &String)> + '_> {
let mut volumes = HashMap::new(); Box::new(self.volumes.iter())
match self.volume.clone() {
None => {}
Some(volume) => {
volumes.insert(volume, DATADIR.to_string());
}
}
volumes
}
fn env_vars(&self) -> Self::EnvVars {
HashMap::new()
}
fn with_args(self, args: <Self as Image>::Args) -> Self {
Bitcoind { args, ..self }
}
fn with_entrypoint(self, entrypoint: &Self::EntryPoint) -> Self {
Self {
entrypoint: Some(entrypoint.to_string()),
..self
}
} }
fn entrypoint(&self) -> Option<String> { fn entrypoint(&self) -> Option<String> {
@ -71,16 +40,15 @@ impl Image for Bitcoind {
impl Default for Bitcoind { impl Default for Bitcoind {
fn default() -> Self { fn default() -> Self {
Bitcoind { Bitcoind {
args: BitcoindArgs::default(),
entrypoint: Some("/usr/bin/bitcoind".into()), entrypoint: Some("/usr/bin/bitcoind".into()),
volume: None, volumes: BTreeMap::default(),
} }
} }
} }
impl Bitcoind { impl Bitcoind {
pub fn with_volume(mut self, volume: String) -> Self { pub fn with_volume(mut self, volume: String) -> Self {
self.volume = Some(volume); self.volumes.insert(volume, DATADIR.to_string());
self self
} }
} }
@ -109,7 +77,6 @@ impl IntoIterator for BitcoindArgs {
format!("-rpcuser={}", RPC_USER), format!("-rpcuser={}", RPC_USER),
format!("-rpcpassword={}", RPC_PASSWORD), format!("-rpcpassword={}", RPC_PASSWORD),
"-printtoconsole".to_string(), "-printtoconsole".to_string(),
"-rest".to_string(),
"-fallbackfee=0.0002".to_string(), "-fallbackfee=0.0002".to_string(),
format!("-datadir={}", DATADIR), format!("-datadir={}", DATADIR),
format!("-rpcport={}", RPC_PORT), format!("-rpcport={}", RPC_PORT),
@ -120,3 +87,9 @@ impl IntoIterator for BitcoindArgs {
args.into_iter() args.into_iter()
} }
} }
impl ImageArgs for BitcoindArgs {
fn into_iterator(self) -> Box<dyn Iterator<Item = String>> {
Box::new(self.into_iter())
}
}

@ -1,8 +1,8 @@
use std::collections::BTreeMap;
use crate::harness::bitcoind; use crate::harness::bitcoind;
use bitcoin::Network; use bitcoin::Network;
use std::collections::HashMap; use testcontainers::{core::WaitFor, Image, ImageArgs};
use testcontainers::core::{Container, Docker, WaitForMessage};
use testcontainers::Image;
pub const HTTP_PORT: u16 = 60401; pub const HTTP_PORT: u16 = 60401;
pub const RPC_PORT: u16 = 3002; pub const RPC_PORT: u16 = 3002;
@ -13,50 +13,25 @@ pub struct Electrs {
args: ElectrsArgs, args: ElectrsArgs,
entrypoint: Option<String>, entrypoint: Option<String>,
wait_for_message: String, wait_for_message: String,
volume: String, volumes: BTreeMap<String, String>,
} }
impl Image for Electrs { impl Image for Electrs {
type Args = ElectrsArgs; type Args = ElectrsArgs;
type EnvVars = HashMap<String, String>; fn name(&self) -> String {
type Volumes = HashMap<String, String>; "vulpemventures/electrs".into()
type EntryPoint = str;
fn descriptor(&self) -> String {
format!("vulpemventures/electrs:{}", self.tag)
}
fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
container
.logs()
.stderr
.wait_for_message(&self.wait_for_message)
.unwrap();
} }
fn args(&self) -> <Self as Image>::Args { fn tag(&self) -> String {
self.args.clone() self.tag.clone()
} }
fn volumes(&self) -> Self::Volumes { fn ready_conditions(&self) -> Vec<WaitFor> {
let mut volumes = HashMap::new(); vec![WaitFor::message_on_stderr(self.wait_for_message.clone())]
volumes.insert(self.volume.clone(), bitcoind::DATADIR.to_string());
volumes
} }
fn env_vars(&self) -> Self::EnvVars { fn volumes(&self) -> Box<dyn Iterator<Item = (&String, &String)> + '_> {
HashMap::new() Box::new(self.volumes.iter())
}
fn with_args(self, args: <Self as Image>::Args) -> Self {
Electrs { args, ..self }
}
fn with_entrypoint(self, entrypoint: &Self::EntryPoint) -> Self {
Self {
entrypoint: Some(entrypoint.to_string()),
..self
}
} }
fn entrypoint(&self) -> Option<String> { fn entrypoint(&self) -> Option<String> {
@ -71,7 +46,7 @@ impl Default for Electrs {
args: ElectrsArgs::default(), args: ElectrsArgs::default(),
entrypoint: Some("/build/electrs".into()), entrypoint: Some("/build/electrs".into()),
wait_for_message: "Running accept thread".to_string(), wait_for_message: "Running accept thread".to_string(),
volume: uuid::Uuid::new_v4().to_string(), volumes: BTreeMap::default(),
} }
} }
} }
@ -85,7 +60,7 @@ impl Electrs {
} }
pub fn with_volume(mut self, volume: String) -> Self { pub fn with_volume(mut self, volume: String) -> Self {
self.volume = volume; self.volumes.insert(volume, bitcoind::DATADIR.to_string());
self self
} }
@ -93,6 +68,11 @@ impl Electrs {
self.args.daemon_rpc_addr = name; self.args.daemon_rpc_addr = name;
self self
} }
pub fn self_and_args(self) -> (Self, ElectrsArgs) {
let args = self.args.clone();
(self, args)
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -137,7 +117,7 @@ impl IntoIterator for ElectrsArgs {
} }
args.push("-vvvvv".to_string()); args.push("-vvvvv".to_string());
args.push(format!("--daemon-dir=={}", self.daemon_dir.as_str())); args.push(format!("--daemon-dir={}", self.daemon_dir.as_str()));
args.push(format!("--daemon-rpc-addr={}", self.daemon_rpc_addr)); args.push(format!("--daemon-rpc-addr={}", self.daemon_rpc_addr));
args.push(format!("--cookie={}", self.cookie)); args.push(format!("--cookie={}", self.cookie));
args.push(format!("--http-addr={}", self.http_addr)); args.push(format!("--http-addr={}", self.http_addr));
@ -147,3 +127,9 @@ impl IntoIterator for ElectrsArgs {
args.into_iter() args.into_iter()
} }
} }
impl ImageArgs for ElectrsArgs {
fn into_iterator(self) -> Box<dyn Iterator<Item = String>> {
Box::new(self.into_iter())
}
}

@ -28,7 +28,7 @@ use swap::seed::Seed;
use swap::{asb, bitcoin, cli, env, monero}; use swap::{asb, bitcoin, cli, env, monero};
use tempfile::{tempdir, NamedTempFile}; use tempfile::{tempdir, NamedTempFile};
use testcontainers::clients::Cli; use testcontainers::clients::Cli;
use testcontainers::{Container, Docker, RunArgs}; use testcontainers::{Container, RunnableImage};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
@ -61,10 +61,7 @@ where
let alice_starting_balances = let alice_starting_balances =
StartingBalances::new(bitcoin::Amount::ZERO, xmr_amount, Some(10)); StartingBalances::new(bitcoin::Amount::ZERO, xmr_amount, Some(10));
let electrs_rpc_port = containers let electrs_rpc_port = containers.electrs.get_host_port_ipv4(electrs::RPC_PORT);
.electrs
.get_host_port(electrs::RPC_PORT)
.expect("Could not map electrs rpc port");
let alice_seed = Seed::random().unwrap(); let alice_seed = Seed::random().unwrap();
let (alice_bitcoin_wallet, alice_monero_wallet) = init_test_wallets( let (alice_bitcoin_wallet, alice_monero_wallet) = init_test_wallets(
@ -146,25 +143,28 @@ where
async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) { async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) {
let prefix = random_prefix(); let prefix = random_prefix();
let bitcoind_name = format!("{}_{}", prefix, "bitcoind"); let bitcoind_name = format!("{}_{}", prefix, "bitcoind");
let (bitcoind, bitcoind_url) = let (_bitcoind, bitcoind_url, mapped_port) =
init_bitcoind_container(cli, prefix.clone(), bitcoind_name.clone(), prefix.clone()) init_bitcoind_container(cli, prefix.clone(), bitcoind_name.clone(), prefix.clone())
.await .await
.expect("could not init bitcoind"); .expect("could not init bitcoind");
let electrs = init_electrs_container(cli, prefix.clone(), bitcoind_name, prefix) let electrs = init_electrs_container(cli, prefix.clone(), bitcoind_name, prefix, mapped_port)
.await .await
.expect("could not init electrs"); .expect("could not init electrs");
let (monero, monerod_container, monero_wallet_rpc_containers) = let (monero, _monerod_container, _monero_wallet_rpc_containers) =
Monero::new(cli, vec![MONERO_WALLET_NAME_ALICE, MONERO_WALLET_NAME_BOB]) Monero::new(cli, vec![MONERO_WALLET_NAME_ALICE, MONERO_WALLET_NAME_BOB])
.await .await
.unwrap(); .unwrap();
(monero, Containers { (
bitcoind_url, monero,
bitcoind, Containers {
monerod_container, bitcoind_url,
monero_wallet_rpc_containers, _bitcoind,
electrs, _monerod_container,
}) _monero_wallet_rpc_containers,
electrs,
},
)
} }
async fn init_bitcoind_container( async fn init_bitcoind_container(
@ -172,29 +172,28 @@ async fn init_bitcoind_container(
volume: String, volume: String,
name: String, name: String,
network: String, network: String,
) -> Result<(Container<'_, Cli, bitcoind::Bitcoind>, Url)> { ) -> Result<(Container<'_, bitcoind::Bitcoind>, Url, u16)> {
let image = bitcoind::Bitcoind::default().with_volume(volume); let image = bitcoind::Bitcoind::default().with_volume(volume);
let image = RunnableImage::from(image)
.with_container_name(name)
.with_network(network);
let run_args = RunArgs::default().with_name(name).with_network(network); let docker = cli.run(image);
let port = docker.get_host_port_ipv4(bitcoind::RPC_PORT);
let docker = cli.run_with_args(image, run_args);
let a = docker
.get_host_port(bitcoind::RPC_PORT)
.context("Could not map bitcoind rpc port")?;
let bitcoind_url = { let bitcoind_url = {
let input = format!( let input = format!(
"http://{}:{}@localhost:{}", "http://{}:{}@localhost:{}",
bitcoind::RPC_USER, bitcoind::RPC_USER,
bitcoind::RPC_PASSWORD, bitcoind::RPC_PASSWORD,
a port
); );
Url::parse(&input).unwrap() Url::parse(&input).unwrap()
}; };
init_bitcoind(bitcoind_url.clone(), 5).await?; init_bitcoind(bitcoind_url.clone(), 5).await?;
Ok((docker, bitcoind_url.clone())) Ok((docker, bitcoind_url.clone(), bitcoind::RPC_PORT))
} }
pub async fn init_electrs_container( pub async fn init_electrs_container(
@ -202,16 +201,18 @@ pub async fn init_electrs_container(
volume: String, volume: String,
bitcoind_container_name: String, bitcoind_container_name: String,
network: String, network: String,
) -> Result<Container<'_, Cli, electrs::Electrs>> { port: u16,
let bitcoind_rpc_addr = format!("{}:{}", bitcoind_container_name, bitcoind::RPC_PORT); ) -> Result<Container<'_, electrs::Electrs>> {
let bitcoind_rpc_addr = format!("{}:{}", bitcoind_container_name, port);
let image = electrs::Electrs::default() let image = electrs::Electrs::default()
.with_volume(volume) .with_volume(volume)
.with_daemon_rpc_addr(bitcoind_rpc_addr) .with_daemon_rpc_addr(bitcoind_rpc_addr)
.with_tag("latest"); .with_tag("latest");
let image = RunnableImage::from(image.self_and_args())
.with_network(network.clone())
.with_container_name(format!("{}_electrs", network));
let run_args = RunArgs::default().with_network(network); let docker = cli.run(image);
let docker = cli.run_with_args(image, run_args);
Ok(docker) Ok(docker)
} }
@ -245,7 +246,7 @@ async fn start_alice(
resume_only, resume_only,
env_config, env_config,
XmrBtcNamespace::Testnet, XmrBtcNamespace::Testnet,
None, &[],
) )
.unwrap(); .unwrap();
swarm.listen_on(listen_address).unwrap(); swarm.listen_on(listen_address).unwrap();
@ -925,7 +926,7 @@ async fn init_bitcoind(node_url: Url, spendable_quantity: u32) -> Result<Client>
bitcoind_client bitcoind_client
.generatetoaddress(101 + spendable_quantity, reward_address.clone()) .generatetoaddress(101 + spendable_quantity, reward_address.clone())
.await?; .await?;
let _ = tokio::spawn(mine(bitcoind_client.clone(), reward_address)); tokio::spawn(mine(bitcoind_client.clone(), reward_address));
Ok(bitcoind_client) Ok(bitcoind_client)
} }
@ -949,13 +950,12 @@ pub async fn mint(node_url: Url, address: bitcoin::Address, amount: bitcoin::Amo
} }
// This is just to keep the containers alive // This is just to keep the containers alive
#[allow(dead_code)]
struct Containers<'a> { struct Containers<'a> {
bitcoind_url: Url, bitcoind_url: Url,
bitcoind: Container<'a, Cli, bitcoind::Bitcoind>, _bitcoind: Container<'a, bitcoind::Bitcoind>,
monerod_container: Container<'a, Cli, image::Monerod>, _monerod_container: Container<'a, image::Monerod>,
monero_wallet_rpc_containers: Vec<Container<'a, Cli, image::MoneroWalletRpc>>, _monero_wallet_rpc_containers: Vec<Container<'a, image::MoneroWalletRpc>>,
electrs: Container<'a, Cli, electrs::Electrs>, electrs: Container<'a, electrs::Electrs>,
} }
pub mod alice_run_until { pub mod alice_run_until {

Loading…
Cancel
Save