606: Script to list sellers and auto trigger swap + docs r=da-kami a=da-kami

Uses best price to determine the seller to swap with.

TODO:
- [x] Codebase to be tested on test-/stagenet.
- [x] Update readme
- [x] Update docs

This includes a couple of changes that I found while toying with the output and writing docs. Can drop the individual commits if necessary (but would need to adapt the docs then).

Co-authored-by: Daniel Karzel <daniel@comit.network>
Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
bors[bot] 2021-07-07 07:18:46 +00:00 committed by GitHub
commit 93a69563a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 406 additions and 146 deletions

View File

@ -34,6 +34,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The commandline interface of the CLI to combine `--seller-addr` and `--seller-peer-id`.
These two parameters have been merged into a parameter `--seller` that accepts a single [multiaddress](https://docs.libp2p.io/concepts/addressing/).
The multiaddress must end with a `/p2p` protocol defining the seller's peer ID.
- The `--data-dir` option to `--data-base-dir`.
Previously, this option determined the final data directory, regardless of the `--testnet` flag.
With `--data-base-dir`, a subdirectory (either `testnet` or `mainnet`) will be created under the given path.
This allows using the same command with or without `--testnet`.
### Removed

View File

@ -5,50 +5,33 @@ It implements the protocol described in section 3 of [this](https://arxiv.org/ab
More information about the protocol in this [presentation](https://youtu.be/Jj8rd4WOEy0) and this [blog post](https://comit.network/blog/2020/10/06/monero-bitcoin).
## Quick start - CLI
Currently, swaps are only offered in one direction with the `swap` CLI on the buying side (send BTC, receive XMR).
We are working on implementing a protocol where XMR moves first, but are currently blocked by advances on Monero itself.
You can read [this blogpost](https://comit.network/blog/2021/07/02/transaction-presigning) for more information.
From version `0.6.0` onwards the software default to running on `mainnet`.
It is recommended to try the software on testnet first, which can be achieved by providing the `--testnet` flag.
This quickstart guide assumes that you are running the software on testnet (i.e. Bitcoin testnet3 and Monero stagenet):
## Quick Start
1. Download the [latest `swap` binary release](https://github.com/comit-network/xmr-btc-swap/releases/latest) for your operating system
2. Run the binary specifying the monero address where you wish to receive monero and the connection details of the seller:
`./swap --testnet buy-xmr --receive-address <YOUR MONERO ADDRESS> --seller-peer-id <SELLERS PEER ID> --seller-addr <SELLERS MULTIADDRESS>`
You can generate a receive address using your monero wallet.
The seller will provide you their peer id and multiaddress.
We are running an `asb` instance on testnet.
You can swap with to get familiar with the `swap` CLI.
Our peer id is `12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi` and our multiaddress is `/dnsaddr/xmr-btc-asb.coblox.tech`
3. Follow the instructions printed to the terminal
1. Download the [latest `swap` binary release](https://github.com/comit-network/xmr-btc-swap/releases/latest) for your operating system.
2. Find a seller to swap with:
For running the software on mainnet you just omit the `--testnet` flag.
Running on mainnet will automatically apply sane defaults.
Be aware that this software is still early-stage.
Make sure to check `--help` and understand how the `cancel` and `refund` commands work before running on mainnet.
You are running this software at your own risk.
As always we recommend: Verify, don't trust.
All code is available in this repository.
```shell
./swap --testnet --list-sellers
```
## How it works
3. Swap with a seller:
This repository primarily hosts two components:
```shell
./swap --testnet buy-xmr --receive-address <YOUR MONERO ADDRESS> --change-address <YOUR BITCOIN CHANGE ADDRESS> --seller <SELLER MULTIADDRESS>
```
- the `swap` CLI
- the [`asb` service](/docs/asb/README.md)
For more detailed documentation on the CLI, see [this README](./docs/cli/README.md).
### swap CLI
## Becoming a Market Maker
The `swap` CLI acts in the role of Bob and swaps BTC for XMR.
See `./swap --help` for a description of all commands.
The main command is `buy-xmr` which automatically connects to an instance of `asb`.
Swapping of course needs two parties - and the CLI is only one of them: The taker that occasionally starts a swap with a market maker.
### asb service
`asb` is short for **a**utomated **s**wap **b**ackend (we are open to suggestions for better names!).
The service acts as the counter-party for the `swap` CLI in the role of Alice.
It provides the CLI with a quote and the liquidity necessary for swapping BTC into XMR.
For details on how to run the ASB please refer to the [ASB docs](/docs/asb/README.md).
If you are interested in becoming a market maker you will want to run the second binary provided in this repository: `asb` - the Automated Swap Backend.
Detailed documentation for the `asb` can be found [in this README](./docs/asb/README.md).
## Contact

6
docs/README.md Normal file
View File

@ -0,0 +1,6 @@
# Documentation
This directory hosts various pieces of documentation.
- [`swap` CLI](./cli/README.md)
- [`asb` service](./asb/README.md)

View File

@ -1,6 +1,6 @@
# XMR to BTC Atomic Swap - Automated Swap Backend (ASB)
# Automated Swap Backend (ASB)
## Quick Start ASB
## Quick Start
From version `0.6.0` onwards the software default to running on `mainnet`.
It is recommended to try the software on testnet first, which can be achieved by providing the `--testnet` flag.
@ -33,7 +33,7 @@ Public Electrum mainnet nodes can be found [here](https://1209k.com/bitcoin-eye/
The ASB is a long running daemon that acts as the trading partner to the swap CLI.
The CLI user is buying XMR (i.e. receives XMR, sends BTC), the ASB service provider is selling XMR (i.e. sends XMR, receives BTC).
The ASB can handle multiple swaps with different peers concurrently.
The ASB communicates with the CLI on various [libp2p](https://libp2p.io/)-based network protocols.
The ASB communicates with the CLI on various [libp2p-based](https://libp2p.io/) network protocols.
Both the ASB and the CLI can be run by anybody.
The CLI is designed to run one specific swap against an ASB.
@ -42,23 +42,22 @@ Since the ASB is a long running task we specify the person running an ASB as ser
### ASB discovery
Currently, there is no automated discovery for service providers running an ASB.
A service provider has to manually provide the connection details to users that will run the CLI.
The ASB daemon supports the libp2p [rendezvous-protocol](https://github.com/libp2p/specs/tree/master/rendezvous).
Usage of the rendezvous functionality is entirely optional.
[Libp2p addressing](https://docs.libp2p.io/concepts/addressing/) is used to identify a service provider by multi-address and peer-id.
The Peer-ID is printed upon startup of the ASB.
The multi-address typically consists of IP-address or URL (if DNS entry configured) of the service provider.
You can configure a rendezvous point 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 example:
When configuring a domain name for the ASB through a DNS entry, a service provider can configure it by using the [`dnsaddr` format](https://github.com/multiformats/multiaddr/blob/master/protocols/DNSADDR.md) for the TXT entry.
This will simplify the connection detail `--seller-addr` for CLI users connecting to the ASB and provides more flexibility with e.g. ports (i.e. `/dnsaddr/your.domain.tld` instead of `/dns4/your.domain.tld/tcp/port`).
```toml
[network]
rendezvous_point = "/dnsaddr/rendezvous.coblox.tech/p2p/12D3KooWQUt9DkNZxEn2R5ymJzWj15MpG6mTW84kyd8vDaRZi46o"
external_addresses = ["/dns4/example.com/tcp/9939"]
```
Each service provider running an ASB can decide how/where to share these connection details.
![Service Provider scenarios](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/comit-network/xmr-btc-swap/d2cf45d8b9f0c2e180cd85aa034f370965adc11c/docs/asb/diagrams/cli-asb-overview.puml)
Eventually, more elaborate discovery mechanisms can be added.
The **CLI** user can specify a service providers's multiaddress and peer-id with `--seller-addr` and `--seller-peer-id`, see `./swap --help` for details.
For more information on the concept of multiaddresses, check out the libp2p documentation [here](https://docs.libp2p.io/concepts/addressing/).
In particular, you may be interested in setting up your ASB to be reachable via a [`/dnsaddr`](https://github.com/multiformats/multiaddr/blob/master/protocols/DNSADDR.md) multiaddress.
`/dnsaddr` addresses provide you with flexibility over the port and also allow you to register two addresses with transports (with and without websockets for example) under the same name.
### Setup Details
@ -96,28 +95,37 @@ The Bitcoin wallet is created upon initial startup and stored in the data folder
#### Market Making
For market making the ASB offers the following parameters in the config:
```toml
[maker]
min_buy_btc = 0.0001
max_buy_btc = 0.0001
ask_spread = 0.02
price_ticker_ws_url = "wss://ws.kraken.com"
```
The minimum and maximum amount as well as a spread, that is added on top of the price fetched from a central exchange, can be configured.
In order to be able to trade, the ASB must define a price to be able to agree on the amounts to be swapped with a CLI.
Currently we use a spot-price mode, i.e. the ASB dictates the price to the CLI.
A CLI can connect to the ASB at any time and request a quote for buying XMR.
The ASB then returns the current price and the maximum amount tradeable.
The maximum amount tradeable can be configured with the `--max-buy-btc` parameter.
The `XMR<>BTC` price is currently determined by the price from the central exchange Kraken.
Upon startup the ASB connects to the Kraken price websocket and listens on the stream for price updates.
You can plug in a different price ticker websocket using the the `price_ticker_ws_url` configuration option.
You will have to make sure that the format returned is the same as the format used by Kraken.
Currently the spot price is equal to the market price on Kraken.
Currently, we use a spot-price model, i.e. the ASB dictates the price to the CLI.
A CLI can connect to the ASB at any time and request a quote for buying XMR.
The ASB then returns the current price and the minimum and maximum amount tradeable.
#### Swap Execution
Swap execution within the ASB is automated.
Incoming swaps request will be automatically processed and the swap will execute automatically.
Incoming swaps request will be automatically processed, and the swap will execute automatically.
Swaps where Bob does not act, so Alice cannot redeem, will be automatically refunded or punished.
When the ASB is restarted unfinished swaps will be resumed automatically.
If the ASB is restarted unfinished swaps will be resumed automatically.
The refund scenario is a scenario where the CLI refunds the Bitcoin.
The ASB can then refund the Monero which will be automatically transferred to the `asb-wallet`.
The ASB can then refund the Monero which will be automatically transferred back to the `asb-wallet`.
The punish scenario is a scenario where the CLI does not refund and hence the ASB cannot refund the Monero.
After a second timelock expires the ASB will automatically punish the CLI user by taking the Bitcoin.
@ -128,7 +136,6 @@ All claimed Bitcoin ends up in the internal Bitcoin wallet of the ASB.
The ASB offers a commands to withdraw Bitcoin and check the balance, run `./asb --help` for details.
If the ASB has insufficient Monero funds to accept a swap the swap setup is rejected.
Note that currently there is no specific error sent back to the CLI for such kind of cases, so a user might not know why the swap execution was rejected.
Note that there is currently no notification service implemented for low funds.
The ASB provider has to monitor Monero funds to make sure the ASB still has liquidity.

141
docs/cli/README.md Normal file
View File

@ -0,0 +1,141 @@
# Swap CLI
The CLI defaults to **mainnet** (from version 0.6.0 onwards).
For testing and to familiarise yourself with the tool, we recommend you to try it on testnet first.
To do that, pass the `--testnet` flag with the actual command:
```shell
swap --testnet <SUBCOMMAND>
```
The two main commands of the CLI are:
- `buy-xmr`: for swapping BTC to XMR with a particular seller
- `list-sellers`: for discovering available sellers through a rendezvous point
Running `swap --help` gives us roughly the following output:
```
swap 0.8.0
The COMIT guys <hello@comit.network>
CLI for swapping BTC for XMR
USAGE:
swap [FLAGS] [OPTIONS] <SUBCOMMAND>
FLAGS:
--debug Activate debug logging
-h, --help Prints help information
-j, --json Outputs all logs in JSON format instead of plain text
--testnet Swap on testnet and assume testnet defaults for data-dir and the blockchain related parameters
-V, --version Prints version information
OPTIONS:
--data-base-dir <data> The base data directory to be used for mainnet / testnet specific data like database, wallets etc
SUBCOMMANDS:
buy-xmr Start a BTC for XMR swap
list-sellers Discover and list sellers (i.e. ASB providers)
cancel Try to cancel an ongoing swap (expert users only)
help Prints this message or the help of the given subcommand(s)
history Show a list of past, ongoing and completed swaps
refund Try to cancel a swap and refund the BTC (expert users only)
resume Resume a swap
```
## Swapping BTC for XMR
Running `swap buy-xmr --help` gives us roughly the following output:
```
swap-buy-xmr 0.8.0
Start a BTC for XMR swap
USAGE:
swap buy-xmr [FLAGS] [OPTIONS] --change-address <bitcoin-change-address> --receive-address <monero-receive-address> --seller <seller>
FLAGS:
-h, --help Prints help information
--testnet Swap on testnet and assume testnet defaults for data-dir and the blockchain related parameters
-V, --version Prints version information
OPTIONS:
--change-address <bitcoin-change-address> The bitcoin address where any form of change or excess funds should be sent to
--receive-address <monero-receive-address> The monero address where you would like to receive monero
--seller <seller> The seller's address. Must include a peer ID part, i.e. `/p2p/`
--electrum-rpc <bitcoin-electrum-rpc-url> Provide the Bitcoin Electrum RPC URL
--bitcoin-target-block <bitcoin-target-block> Estimate Bitcoin fees such that transactions are confirmed within the specified number of blocks
--monero-daemon-address <monero-daemon-address> Specify to connect to a monero daemon of your choice: <host>:<port>
--tor-socks5-port <tor-socks5-port> Your local Tor socks5 proxy port [default: 9050]
```
This command has three core options:
- `--change-address`: A Bitcoin address you control. Will be used for refunds of any kind.
- `--receive-address`: A Monero address you control. This is where you will receive the Monero after the swap.
- `--seller`: The multiaddress of the seller you want to swap with.
## Discovering sellers
Running `swap list-sellers --help` gives us roughly the following output:
```
swap-list-sellers 0.8.0
Discover and list sellers (i.e. ASB providers)
USAGE:
swap list-sellers [FLAGS] [OPTIONS]
FLAGS:
-h, --help Prints help information
--testnet Swap on testnet and assume testnet defaults for data-dir and the blockchain related parameters
-V, --version Prints version information
OPTIONS:
--rendezvous-point <rendezvous-point> Address of the rendezvous point you want to use to discover ASBs [default: /dnsaddr/rendezvous.coblox.tech/p2p/12D3KooWQUt9DkNZxEn2R5ymJzWj15MpG6mTW84kyd8vDaRZi46o]
--tor-socks5-port <tor-socks5-port> Your local Tor socks5 proxy port [default: 9050]
```
This command only takes optional parameters and can be run as-is:
Running `swap --testnet list-sellers` will give you something like:
```
Connected to rendezvous point, discovering nodes in 'xmr-btc-swap-testnet' namespace ...
Discovered peer 12D3KooWPZ69DRp4wbGB3wJsxxsg1XW1EVZ2evtVwcARCF3a1nrx at /dns4/ac4hgzmsmekwekjbdl77brufqqbylddugzze4tel6qsnlympgmr46iid.onion/tcp/8765
+----------------+----------------+----------------+--------+----------------------------------------------------------------------------------------------------------------------------------------+
| PRICE | MIN_QUANTITY | MAX_QUANTITY | STATUS | ADDRESS |
+====================================================================================================================================================================================================+
| 0.00665754 BTC | 0.00010000 BTC | 0.00100000 BTC | Online | /dns4/ac4hgzmsmekwekjbdl77brufqqbylddugzze4tel6qsnlympgmr46iid.onion/tcp/8765/p2p/12D3KooWPZ69DRp4wbGB3wJsxxsg1XW1EVZ2evtVwcARCF3a1nrx |
+----------------+----------------+----------------+--------+----------------------------------------------------------------------------------------------------------------------------------------+
```
or this if a node is not reachable:
```
Connected to rendezvous point, discovering nodes in 'xmr-btc-swap-testnet' namespace ...
Discovered peer 12D3KooWPZ69DRp4wbGB3wJsxxsg1XW1EVZ2evtVwcARCF3a1nrx at /dns4/ac4hgzmsmekwekjbdl77brufqqbylddugzze4tel6qsnlympgmr46iid.onion/tcp/8765
+-------+--------------+--------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------+
| PRICE | MIN_QUANTITY | MAX_QUANTITY | STATUS | ADDRESS |
+============================================================================================================================================================================================+
| ??? | ??? | ??? | Unreachable | /dns4/ac4hgzmsmekwekjbdl77brufqqbylddugzze4tel6qsnlympgmr46iid.onion/tcp/8765/p2p/12D3KooWPZ69DRp4wbGB3wJsxxsg1XW1EVZ2evtVwcARCF3a1nrx |
+-------+--------------+--------------+-------------+----------------------------------------------------------------------------------------------------------------------------------------+
```
## Automating discover and swapping
The `buy-xmr` and `list-sellers` command have been designed to be composed.
[This script](./discover_and_take.sh) is example of what can be done.
Deciding on the seller to use is non-trivial to automate which is why it is not implemented as part of the tool.
## Tor
By default, the CLI will look for Tor at the default socks port `9050` and automatically route all traffic with a seller through Tor.
This allows swapping with sellers that are only reachable with an onion address.
Disclaimer:
Communication with public blockchain explorers (Electrum, public XMR nodes) currently goes through clearnet.
For complete anonymity it is recommended to run your own blockchain nodes.
Use `swap buy-xmr --help` to see configuration options.

28
docs/cli/discover_and_take.sh Executable file
View File

@ -0,0 +1,28 @@
#!/bin/bash
CLI_PATH=$1
YOUR_MONERO_ADDR=$2
YOUR_BITCOIN_ADDR=$3
CLI_LIST_SELLERS="$CLI_PATH --testnet --json --debug list-sellers"
echo "Requesting sellers with command: $CLI_LIST_SELLERS"
echo
BEST_SELLER=$($CLI_LIST_SELLERS | jq -s -c 'min_by(.status .Online .price)' | jq -r '.multiaddr, (.status .Online .price), (.status .Online .min_quantity), (.status .Online .max_quantity)')
read ADDR PRICE MIN MAX < <(echo $BEST_SELLER)
echo
echo "Seller with best price:"
echo " multiaddr : $ADDR"
echo " price : $PRICE sat"
echo " min_quantity: $MIN sat"
echo " max_quantity: $MAX sat"
echo
CLI_SWAP="$CLI_PATH --testnet --debug buy-xmr --receive-address $YOUR_MONERO_ADDR --change-address $YOUR_BITCOIN_ADDR --seller $ADDR"
echo "Starting swap with best seller using command $CLI_SWAP"
echo
$CLI_SWAP

View File

@ -24,7 +24,7 @@ use std::sync::Arc;
use std::time::Duration;
use swap::bitcoin::TxLock;
use swap::cli::command::{parse_args_and_apply_defaults, Arguments, Command, ParseResult};
use swap::cli::{list_sellers, EventLoop};
use swap::cli::{list_sellers, EventLoop, SellerStatus};
use swap::database::Database;
use swap::env::Config;
use swap::libp2p_ext::MultiAddrExt;
@ -272,11 +272,11 @@ async fn main() -> Result<()> {
cli::refund(swap_id, Arc::new(bitcoin_wallet), db, force).await??;
}
Command::ListSellers {
rendezvous_node_addr,
rendezvous_point,
namespace,
tor_socks5_port,
} => {
let rendezvous_node_peer_id = rendezvous_node_addr
let rendezvous_node_peer_id = rendezvous_point
.extract_peer_id()
.context("Rendezvous node address must contain peer ID")?;
@ -285,14 +285,15 @@ async fn main() -> Result<()> {
.context("Failed to read in seed file")?;
let identity = seed.derive_libp2p_identity();
let sellers = list_sellers(
let mut sellers = list_sellers(
rendezvous_node_peer_id,
rendezvous_node_addr,
rendezvous_point,
namespace,
tor_socks5_port,
identity,
)
.await?;
sellers.sort();
if json {
for seller in sellers {
@ -301,15 +302,37 @@ async fn main() -> Result<()> {
} else {
let mut table = Table::new();
table.set_header(vec!["PRICE", "MIN_QUANTITY", "MAX_QUANTITY", "ADDRESS"]);
table.set_header(vec![
"PRICE",
"MIN_QUANTITY",
"MAX_QUANTITY",
"STATUS",
"ADDRESS",
]);
for seller in sellers {
table.add_row(vec![
seller.quote.price.to_string(),
seller.quote.min_quantity.to_string(),
seller.quote.max_quantity.to_string(),
seller.multiaddr.to_string(),
]);
let row = match seller.status {
SellerStatus::Online(quote) => {
vec![
quote.price.to_string(),
quote.min_quantity.to_string(),
quote.max_quantity.to_string(),
"Online".to_owned(),
seller.multiaddr.to_string(),
]
}
SellerStatus::Unreachable => {
vec![
"???".to_owned(),
"???".to_owned(),
"???".to_owned(),
"Unreachable".to_owned(),
seller.multiaddr.to_string(),
]
}
};
table.add_row(row);
}
println!("{}", table);

View File

@ -10,14 +10,14 @@ pub mod transport;
pub use behaviour::{Behaviour, OutEvent};
pub use cancel::cancel;
pub use event_loop::{EventLoop, EventLoopHandle};
pub use list_sellers::list_sellers;
pub use list_sellers::{list_sellers, Seller, Status as SellerStatus};
pub use refund::refund;
#[cfg(test)]
mod tests {
use super::*;
use crate::asb;
use crate::cli::list_sellers::Seller;
use crate::cli::list_sellers::{Seller, Status};
use crate::network::quote;
use crate::network::quote::BidQuote;
use crate::network::rendezvous::XmrBtcNamespace;
@ -122,7 +122,7 @@ mod tests {
Seller {
multiaddr: asb_address.with(Protocol::P2p(asb_peer_id.into())),
quote: static_quote,
status: Status::Online(static_quote),
}
}

View File

@ -188,7 +188,7 @@ where
},
},
RawCommand::ListSellers {
rendezvous_node_addr,
rendezvous_point,
tor: Tor { tor_socks5_port },
} => Arguments {
env_config: env_config_from(is_testnet),
@ -196,7 +196,7 @@ where
json,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::ListSellers {
rendezvous_node_addr,
rendezvous_point,
namespace: rendezvous_namespace_from(is_testnet),
tor_socks5_port,
},
@ -238,7 +238,7 @@ pub enum Command {
bitcoin_target_block: usize,
},
ListSellers {
rendezvous_node_addr: Multiaddr,
rendezvous_point: Multiaddr,
namespace: XmrBtcNamespace,
tor_socks5_port: u16,
},
@ -246,38 +246,38 @@ pub enum Command {
#[derive(structopt::StructOpt, Debug)]
#[structopt(name = "swap", about = "CLI for swapping BTC for XMR", author)]
pub struct RawArguments {
struct RawArguments {
// global is necessary to ensure that clap can match against testnet in subcommands
#[structopt(
long,
help = "Swap on testnet and assume testnet defaults for data-dir and the blockchain related parameters",
global = true
)]
pub testnet: bool,
testnet: bool,
#[structopt(
long = "--data-dir",
help = "Provide the data directory path to be used to store application data using testnet and mainnet as subfolder"
long = "--data-base-dir",
help = "The base data directory to be used for mainnet / testnet specific data like database, wallets etc"
)]
pub data: Option<PathBuf>,
data: Option<PathBuf>,
#[structopt(long, help = "Activate debug logging.")]
pub debug: bool,
#[structopt(long, help = "Activate debug logging")]
debug: bool,
#[structopt(
short,
long = "json",
help = "Changes the log messages to json vs plain-text. This can be helpful to simplify automated log analyses."
help = "Outputs all logs in JSON format instead of plain text"
)]
pub json: bool,
json: bool,
#[structopt(subcommand)]
pub cmd: RawCommand,
cmd: RawCommand,
}
#[derive(structopt::StructOpt, Debug)]
pub enum RawCommand {
/// Start a XMR for BTC swap
enum RawCommand {
/// Start a BTC for XMR swap
BuyXmr {
#[structopt(flatten)]
seller: Seller,
@ -303,7 +303,7 @@ pub enum RawCommand {
#[structopt(flatten)]
tor: Tor,
},
/// Show a list of past ongoing and completed swaps
/// Show a list of past, ongoing and completed swaps
History,
/// Resume a swap
Resume {
@ -330,7 +330,7 @@ pub enum RawCommand {
#[structopt(flatten)]
bitcoin: Bitcoin,
},
/// Try to cancel a swap and refund my BTC (expert users only)
/// Try to cancel a swap and refund the BTC (expert users only)
Refund {
#[structopt(flatten)]
swap_id: SwapId,
@ -341,13 +341,14 @@ pub enum RawCommand {
#[structopt(flatten)]
bitcoin: Bitcoin,
},
/// Discover and list sellers (i.e. ASB providers)
ListSellers {
#[structopt(
long,
help = "The multiaddr (including peer-id) of a rendezvous node that sellers register with",
help = "Address of the rendezvous point you want to use to discover ASBs",
default_value = DEFAULT_RENDEZVOUS_ADDRESS
)]
rendezvous_node_addr: Multiaddr,
rendezvous_point: Multiaddr,
#[structopt(flatten)]
tor: Tor,
@ -355,75 +356,66 @@ pub enum RawCommand {
}
#[derive(structopt::StructOpt, Debug)]
pub struct Monero {
struct Monero {
#[structopt(
long = "monero-daemon-address",
help = "Specify to connect to a monero daemon of your choice: <host>:<port>"
)]
pub monero_daemon_address: Option<String>,
monero_daemon_address: Option<String>,
}
#[derive(structopt::StructOpt, Debug)]
pub struct Bitcoin {
struct Bitcoin {
#[structopt(long = "electrum-rpc", help = "Provide the Bitcoin Electrum RPC URL")]
pub bitcoin_electrum_rpc_url: Option<Url>,
bitcoin_electrum_rpc_url: Option<Url>,
#[structopt(
long = "bitcoin-target-block",
help = "Use for fee estimation, decides within how many blocks the Bitcoin transactions should be confirmed."
help = "Estimate Bitcoin fees such that transactions are confirmed within the specified number of blocks"
)]
pub bitcoin_target_block: Option<usize>,
bitcoin_target_block: Option<usize>,
}
#[derive(structopt::StructOpt, Debug)]
pub struct Tor {
struct Tor {
#[structopt(
long = "tor-socks5-port",
help = "Your local Tor socks5 proxy port",
default_value = DEFAULT_TOR_SOCKS5_PORT
)]
pub tor_socks5_port: u16,
tor_socks5_port: u16,
}
#[derive(structopt::StructOpt, Debug)]
pub struct SwapId {
struct SwapId {
#[structopt(
long = "swap-id",
help = "The swap id can be retrieved using the history subcommand"
)]
pub swap_id: Uuid,
swap_id: Uuid,
}
#[derive(structopt::StructOpt, Debug)]
pub struct Seller {
struct Seller {
#[structopt(
long,
help = "The seller's address. Must include a peer ID part, i.e. `/p2p/`"
)]
pub seller: Multiaddr,
seller: Multiaddr,
}
mod data {
use super::*;
pub fn data_dir_from(arg_dir: Option<PathBuf>, testnet: bool) -> Result<PathBuf> {
let dir = if let Some(dir) = arg_dir {
dir
} else if testnet {
testnet_default()?
} else {
mainnet_default()?
let base_dir = match arg_dir {
Some(custom_base_dir) => custom_base_dir,
None => os_default()?,
};
Ok(dir)
}
let sub_directory = if testnet { "testnet" } else { "mainnet" };
fn testnet_default() -> Result<PathBuf> {
Ok(os_default()?.join("testnet"))
}
fn mainnet_default() -> Result<PathBuf> {
Ok(os_default()?.join("mainnet"))
Ok(base_dir.join(sub_directory))
}
fn os_default() -> Result<PathBuf> {
@ -699,7 +691,7 @@ mod tests {
let raw_ars = vec![
BINARY_NAME,
"--data-dir",
"--data-base-dir",
data_dir,
"buy-xmr",
"--change-address",
@ -716,14 +708,14 @@ mod tests {
args,
ParseResult::Arguments(
Arguments::buy_xmr_mainnet_defaults()
.with_data_dir(PathBuf::from_str(data_dir).unwrap())
.with_data_dir(PathBuf::from_str(data_dir).unwrap().join("mainnet"))
)
);
let raw_ars = vec![
BINARY_NAME,
"--testnet",
"--data-dir",
"--data-base-dir",
data_dir,
"buy-xmr",
"--change-address",
@ -740,13 +732,13 @@ mod tests {
args,
ParseResult::Arguments(
Arguments::buy_xmr_testnet_defaults()
.with_data_dir(PathBuf::from_str(data_dir).unwrap())
.with_data_dir(PathBuf::from_str(data_dir).unwrap().join("testnet"))
)
);
let raw_ars = vec![
BINARY_NAME,
"--data-dir",
"--data-base-dir",
data_dir,
"resume",
"--swap-id",
@ -759,14 +751,14 @@ mod tests {
args,
ParseResult::Arguments(
Arguments::resume_mainnet_defaults()
.with_data_dir(PathBuf::from_str(data_dir).unwrap())
.with_data_dir(PathBuf::from_str(data_dir).unwrap().join("mainnet"))
)
);
let raw_ars = vec![
BINARY_NAME,
"--testnet",
"--data-dir",
"--data-base-dir",
data_dir,
"resume",
"--swap-id",
@ -779,7 +771,7 @@ mod tests {
args,
ParseResult::Arguments(
Arguments::resume_testnet_defaults()
.with_data_dir(PathBuf::from_str(data_dir).unwrap())
.with_data_dir(PathBuf::from_str(data_dir).unwrap().join("testnet"))
)
);
}

View File

@ -11,6 +11,7 @@ use libp2p::swarm::SwarmEvent;
use libp2p::{identity, rendezvous, Multiaddr, PeerId, Swarm};
use serde::Serialize;
use serde_with::{serde_as, DisplayFromStr};
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::time::Duration;
@ -52,11 +53,17 @@ pub async fn list_sellers(
}
#[serde_as]
#[derive(Debug, Serialize, PartialEq, Eq, Hash)]
#[derive(Debug, Serialize, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct Seller {
pub status: Status,
#[serde_as(as = "DisplayFromStr")]
pub multiaddr: Multiaddr,
pub quote: BidQuote,
}
#[derive(Debug, Serialize, PartialEq, Eq, Hash, Copy, Clone, Ord, PartialOrd)]
pub enum Status {
Online(BidQuote),
Unreachable,
}
#[derive(Debug)]
@ -90,7 +97,7 @@ struct Behaviour {
#[derive(Debug)]
enum QuoteStatus {
Pending,
Received(BidQuote),
Received(Status),
}
#[derive(Debug)]
@ -104,7 +111,8 @@ struct EventLoop {
rendezvous_peer_id: PeerId,
rendezvous_addr: Multiaddr,
namespace: XmrBtcNamespace,
asb_address: HashMap<PeerId, Multiaddr>,
reachable_asb_address: HashMap<PeerId, Multiaddr>,
unreachable_asb_address: HashMap<PeerId, Multiaddr>,
asb_quote_status: HashMap<PeerId, QuoteStatus>,
state: State,
}
@ -121,7 +129,8 @@ impl EventLoop {
rendezvous_peer_id,
rendezvous_addr,
namespace,
asb_address: Default::default(),
reachable_asb_address: Default::default(),
unreachable_asb_address: Default::default(),
asb_quote_status: Default::default(),
state: State::WaitForDiscovery,
}
@ -147,7 +156,7 @@ impl EventLoop {
);
} else {
let address = endpoint.get_remote_address();
self.asb_address.insert(peer_id, address.clone());
self.reachable_asb_address.insert(peer_id, address.clone());
}
}
SwarmEvent::UnreachableAddr { peer_id, error, address, .. } => {
@ -166,9 +175,16 @@ impl EventLoop {
address,
error
);
self.unreachable_asb_address.insert(peer_id, address.clone());
// if a different peer than the rendezvous node is unreachable (i.e. a seller) we remove that seller from the quote status state
self.asb_quote_status.remove(&peer_id);
match self.asb_quote_status.entry(peer_id) {
Entry::Occupied(mut entry) => {
entry.insert(QuoteStatus::Received(Status::Unreachable));
},
_ => {
tracing::debug!(%peer_id, %error, "Connection error with unexpected peer")
}
}
}
}
SwarmEvent::Behaviour(OutEvent::Rendezvous(
@ -205,7 +221,7 @@ impl EventLoop {
RequestResponseEvent::Message { peer, message } => {
match message {
RequestResponseMessage::Response { response, .. } => {
if self.asb_quote_status.insert(peer, QuoteStatus::Received(response)).is_none() {
if self.asb_quote_status.insert(peer, QuoteStatus::Received(Status::Online(response))).is_none() {
tracing::error!(%peer, "Received bid quote from unexpected peer, this record will be removed!");
self.asb_quote_status.remove(&peer);
}
@ -247,15 +263,26 @@ impl EventLoop {
.iter()
.map(|(peer_id, quote_status)| match quote_status {
QuoteStatus::Pending => Err(StillPending {}),
QuoteStatus::Received(quote) => {
QuoteStatus::Received(Status::Online(quote)) => {
let address = self
.asb_address
.reachable_asb_address
.get(&peer_id)
.expect("if we got a quote we must have stored an address");
Ok(Seller {
multiaddr: address.clone(),
quote: *quote,
status: Status::Online(*quote),
})
}
QuoteStatus::Received(Status::Unreachable) => {
let address = self
.unreachable_asb_address
.get(&peer_id)
.expect("if we got a quote we must have stored an address");
Ok(Seller {
multiaddr: address.clone(),
status: Status::Unreachable,
})
}
})
@ -279,3 +306,51 @@ impl From<PingEvent> for OutEvent {
OutEvent::Ping(event)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sellers_sort_with_unreachable_coming_last() {
let mut list = vec![
Seller {
multiaddr: "/ip4/127.0.0.1/tcp/1234".parse().unwrap(),
status: Status::Unreachable,
},
Seller {
multiaddr: Multiaddr::empty(),
status: Status::Unreachable,
},
Seller {
multiaddr: "/ip4/127.0.0.1/tcp/5678".parse().unwrap(),
status: Status::Online(BidQuote {
price: Default::default(),
min_quantity: Default::default(),
max_quantity: Default::default(),
}),
},
];
list.sort();
assert_eq!(list, vec![
Seller {
multiaddr: "/ip4/127.0.0.1/tcp/5678".parse().unwrap(),
status: Status::Online(BidQuote {
price: Default::default(),
min_quantity: Default::default(),
max_quantity: Default::default(),
})
},
Seller {
multiaddr: Multiaddr::empty(),
status: Status::Unreachable
},
Seller {
multiaddr: "/ip4/127.0.0.1/tcp/1234".parse().unwrap(),
status: Status::Unreachable
},
])
}
}

View File

@ -55,7 +55,8 @@ pub fn init(debug: bool, json: bool, dir: impl AsRef<Path>, swap_id: Option<Uuid
.with_env_filter(format!("swap={}", level))
.with_writer(std::io::stderr)
.with_ansi(is_terminal)
.with_timer(ChronoLocal::with_format("%F %T".to_owned()))
.with_level(false)
.without_time()
.with_target(false)
.init();

View File

@ -24,7 +24,7 @@ impl ProtocolName for BidQuoteProtocol {
}
/// Represents a quote for buying XMR.
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct BidQuote {
/// The price at which the maker is willing to buy at.
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]